gentle-pi 4.0.0 → 4.2.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 +157 -18
- package/assets/agents/gentle-ai-explore.md +11 -4
- package/assets/agents/gentle-ai-verify.md +23 -4
- package/assets/agents/gentle-ai-worker.md +6 -6
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +70 -163
- package/assets/orchestrator-memory.md +11 -4
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +29 -25
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +708 -22
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +399 -0
- package/docs/delegated-verification.md +3 -5
- package/docs/gentle-agents-activity.md +513 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +90 -33
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +1 -1
- package/docs/permission-modes.md +127 -0
- package/docs/prompt-history.md +1 -1
- package/docs/readme-reference.md +310 -47
- package/docs/render-cache-performance.md +28 -0
- package/docs/session-profile-format.md +39 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/yolo-mode.md +11 -10
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/child-context.ts +11 -2
- package/extensions/child-safety.ts +4 -2
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +718 -87
- package/extensions/gentle-ai.ts +991 -263
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +271 -80
- package/extensions/gentle-todo.ts +123 -18
- package/extensions/history/index.ts +11 -6
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +91 -21
- package/extensions/skill-registry.ts +175 -46
- package/extensions/startup-banner.ts +11 -2
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +40 -1
- package/lib/agents-runner.ts +90 -15
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +459 -0
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +484 -0
- package/lib/auto-permission-tool-context.ts +61 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +83 -10
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +14 -4
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +104 -28
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-shell-bundled.ts +535 -0
- package/lib/gentle-shell-launcher.ts +75 -24
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +6 -1
- package/lib/nan-provider.ts +33 -11
- package/lib/native-review-cli.ts +194 -73
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase.ts +1 -1
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/review-candidate-view-owner.ts +8 -6
- package/lib/review-candidate-view.ts +3 -2
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-risk-assessment.ts +77 -51
- package/lib/review-sidebar-state.ts +2 -1
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +22 -7
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +261 -0
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-worktree-registry.ts +74 -8
- package/lib/shell-bar.ts +41 -11
- package/lib/shell-changes-view.ts +40 -13
- package/lib/shell-changes.ts +88 -71
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-sidebar-layout.ts +5 -1
- package/lib/shell-todo.ts +81 -26
- package/lib/shell-usage.ts +4 -3
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/vim-editor-adapter.ts +27 -5
- package/lib/visual-customization-policy.ts +20 -9
- package/lib/visual-customize-view.ts +171 -20
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +198 -121
- package/package.json +5 -5
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +536 -0
- package/runtime/gentle-shell-launcher.mjs +74 -23
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +191 -70
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +77 -51
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +7 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +79 -32
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/pi-update.mjs +223 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +22 -8
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +23 -5
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +82 -18
- package/skills/gentle-ai/SKILL.md +12 -11
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +12 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-protocol.test.ts +57 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +89 -4
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +276 -0
- package/tests/auto-mode-sdk.test.ts +84 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +634 -0
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +454 -0
- package/tests/autonomous-guard.test.ts +86 -16
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +52 -1
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +34 -2
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +12 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +107 -2
- package/tests/command-palette.test.ts +5 -5
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -2
- package/tests/devbinary/native-review-parity.devtest.ts +1 -1
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +316 -0
- package/tests/generic-agent-tools.test.ts +16 -1
- package/tests/gentle-agents.test.ts +1895 -76
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +19 -2
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-installer.test.ts +548 -58
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +6 -6
- package/tests/gentle-ai.test.ts +999 -31
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +263 -19
- package/tests/gentle-shell-bundled.test.ts +1036 -0
- package/tests/gentle-shell-launcher.test.ts +251 -25
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +611 -100
- package/tests/gentle-todo.test.ts +317 -14
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/inprocess-reviewer.test.ts +20 -0
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2159 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +395 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +28 -18
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +236 -1
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +1 -1
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +14 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
- package/tests/odd-routing-contract.test.ts +80 -23
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +29 -6
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +6 -5
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/package-manifest.test.ts +105 -21
- package/tests/packed-runner-owned-path.test.ts +35 -1
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +4 -12
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/pi-update-launcher.test.ts +87 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/provider-defect-handoff.test.ts +4 -3
- package/tests/quiet-tool-rendering.test.ts +160 -3
- package/tests/rdd-aware-verification-contract.test.ts +29 -41
- package/tests/rdd-status-line.test.ts +52 -0
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/review-agent-end-preflight.test.ts +62 -10
- package/tests/review-candidate-view.test.ts +101 -12
- package/tests/review-contract-prompt.test.ts +33 -4
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +255 -1
- package/tests/review-controller-workspace-root.test.ts +43 -2
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +5 -3
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +21 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +5 -4
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-risk-assessment.test.ts +109 -33
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +83 -5
- package/tests/runtime-harness.mjs +42 -15
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +65 -5
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +394 -0
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +57 -2
- package/tests/shell-bar.test.ts +40 -0
- package/tests/shell-changes-view.test.ts +85 -34
- package/tests/shell-changes.test.ts +79 -62
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +27 -2
- package/tests/shell-todo.test.ts +132 -11
- package/tests/shell-usage-view.test.ts +1 -1
- package/tests/shell-usage.test.ts +35 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +311 -5
- package/tests/startup-banner.test.ts +117 -3
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +27 -1
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter.test.ts +47 -21
- package/tests/visual-customization-policy.test.ts +32 -3
- package/tests/visual-customize-view.test.ts +288 -10
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +18 -3
- package/tests/yolo-mode-runtime.test.ts +37 -10
- package/tests/yolo-mode.test.ts +38 -5
- package/tests/yolo-session-policy.test.ts +1 -1
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
|
-
import { mkdtempSync, readFileSync, rmSync, symlinkSync } from "node:fs";
|
|
2
|
+
import { mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import test from "node:test";
|
|
@@ -14,10 +14,14 @@ import {
|
|
|
14
14
|
NativeReviewCliV216,
|
|
15
15
|
NativeReviewIntegrationError,
|
|
16
16
|
createNodeExecFileAdapter,
|
|
17
|
+
createWindowsProcessTreeKiller,
|
|
18
|
+
windowsTaskkillPath,
|
|
19
|
+
gentleAiProcessEnvironment,
|
|
17
20
|
isNativeReviewUnachievableVerbRefused,
|
|
18
21
|
type ExecFileAdapter,
|
|
19
22
|
type NativeTargetStatusRequest,
|
|
20
23
|
} from "../lib/native-review-cli.ts";
|
|
24
|
+
import { setBundledInstallForTesting } from "../lib/gentle-shell-bundled.ts";
|
|
21
25
|
|
|
22
26
|
const fixture = (name: string): Record<string, unknown> => JSON.parse(
|
|
23
27
|
readFileSync(join(process.cwd(), "tests", "fixtures", "devbinary", name), "utf8"),
|
|
@@ -418,6 +422,63 @@ test("native START queries STATUS then executes only the provider-rendered order
|
|
|
418
422
|
]);
|
|
419
423
|
});
|
|
420
424
|
|
|
425
|
+
// Captured from the released gentle-ai 4.0.0 binary, which predates START
|
|
426
|
+
// lens selection (verify-always-rdd-high S8).
|
|
427
|
+
const LENS_SELECTION_UNSUPPORTED_FAILURE = {
|
|
428
|
+
schema: "gentle-ai.review-integration.failure/v2", contract: "gentle-ai.review-integration/v2", operation: "review.status",
|
|
429
|
+
phase: "preflight", code: "invalid_request", message: "The negotiated review request is invalid.", mutation_outcome: "not_started",
|
|
430
|
+
authority_applicability: "not_evaluated", retry_safe: true, replayability: "not_replayable", required_inputs: [],
|
|
431
|
+
next_action: "correct_request", cause: "flag provided but not defined: -lenses",
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
test("native START preflights the agent lens selection through STATUS", async () => {
|
|
435
|
+
const tokens = [
|
|
436
|
+
"--contract=gentle-ai.review-integration/v2", "--cwd=/repo", `--target=${TARGET}`,
|
|
437
|
+
"--projection=workspace", "--agent=pi", "--consent=relay",
|
|
438
|
+
"--lenses=review-risk,review-reliability", "--lenses-reason=changes token parsing",
|
|
439
|
+
];
|
|
440
|
+
const queue = queuedAdapter([
|
|
441
|
+
{ stdout: JSON.stringify(negotiatedStartStatus(TARGET, tokens)) },
|
|
442
|
+
{ stdout: JSON.stringify(fixture("start-v3-zero-lens-closed.captured.json")) },
|
|
443
|
+
]);
|
|
444
|
+
await client(queue.adapter).start({ cwd: "/repo", targetIdentity: TARGET, lenses: ["review-risk", "review-reliability"], lensesReason: "changes token parsing" });
|
|
445
|
+
assert.deepEqual(queue.calls.map((call) => call.arguments), [
|
|
446
|
+
["review", "status", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--projection", "workspace", "--agent", "pi",
|
|
447
|
+
"--lenses", "review-risk,review-reliability", "--lenses-reason", "changes token parsing", "--next-transition"],
|
|
448
|
+
["review", "start", ...tokens],
|
|
449
|
+
]);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
test("native START falls back to the tier default when the provider predates lens selection", async () => {
|
|
453
|
+
const tokens = [
|
|
454
|
+
"--contract=gentle-ai.review-integration/v2", "--cwd=/repo", `--target=${TARGET}`,
|
|
455
|
+
"--projection=workspace", "--agent=pi", "--consent=relay",
|
|
456
|
+
];
|
|
457
|
+
const queue = queuedAdapter([
|
|
458
|
+
{ stdout: JSON.stringify(LENS_SELECTION_UNSUPPORTED_FAILURE), exitCode: 1 },
|
|
459
|
+
{ stdout: JSON.stringify(negotiatedStartStatus(TARGET, tokens)) },
|
|
460
|
+
{ stdout: JSON.stringify(fixture("start-v3-zero-lens-closed.captured.json")) },
|
|
461
|
+
]);
|
|
462
|
+
const result = await client(queue.adapter).start({ cwd: "/repo", targetIdentity: TARGET, lenses: ["review-risk"], lensesReason: "changes token parsing" });
|
|
463
|
+
assert.equal(result.action, "closed");
|
|
464
|
+
assert.deepEqual(queue.calls.map((call) => call.arguments), [
|
|
465
|
+
["review", "status", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--projection", "workspace", "--agent", "pi",
|
|
466
|
+
"--lenses", "review-risk", "--lenses-reason", "changes token parsing", "--next-transition"],
|
|
467
|
+
["review", "status", "--contract", "gentle-ai.review-integration/v2", "--cwd", "/repo", "--projection", "workspace", "--agent", "pi", "--next-transition"],
|
|
468
|
+
["review", "start", ...tokens],
|
|
469
|
+
]);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("native START never retries a selection refusal that is not an unknown lens flag", async () => {
|
|
473
|
+
const refusal = { ...LENS_SELECTION_UNSUPPORTED_FAILURE, cause: 'review status --lenses names unknown lens "security"' };
|
|
474
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify(refusal), exitCode: 1 }]);
|
|
475
|
+
await assert.rejects(
|
|
476
|
+
() => client(queue.adapter).start({ cwd: "/repo", targetIdentity: TARGET, lenses: ["security"], lensesReason: "why" }),
|
|
477
|
+
(error: unknown) => error instanceof NativeReviewIntegrationError && error.failureEnvelope.cause === refusal.cause,
|
|
478
|
+
);
|
|
479
|
+
assert.equal(queue.calls.length, 1);
|
|
480
|
+
});
|
|
481
|
+
|
|
421
482
|
test("native START relays provider-owned intended-untracked selection and transition argv verbatim", async () => {
|
|
422
483
|
const selectionValue = JSON.stringify(["new.ts", "nested/other.ts"]);
|
|
423
484
|
const selectionTokens = ["--selection-kind=untracked", "--selection-json={{value}}", "--selection-proof=provider-issued"];
|
|
@@ -495,6 +556,85 @@ test("native consent completion decodes START/v4 and replays its provider argv u
|
|
|
495
556
|
assert.deepEqual(queue.calls[0]?.arguments, consent.choices[0].invocation.split(" ").slice(1));
|
|
496
557
|
});
|
|
497
558
|
|
|
559
|
+
// Builds a Pi-bound consent/v3 envelope whose choices carry `--lenses-reason`
|
|
560
|
+
// rendered exactly as `rendered`, plus the argv Pi must replay for the granted
|
|
561
|
+
// choice when that rendering splits into `reason`.
|
|
562
|
+
function consentWithLensReason(rendered: string, reason: string): { consent: ReturnType<typeof decodeReviewConsentV3>; expected: readonly string[] } {
|
|
563
|
+
const rawConsent = fixture("consent-v3.captured.json");
|
|
564
|
+
rawConsent.agent = "pi";
|
|
565
|
+
const expected: string[][] = [];
|
|
566
|
+
for (const choice of rawConsent.choices as Record<string, unknown>[]) {
|
|
567
|
+
const bound = String(choice.invocation)
|
|
568
|
+
.replace(/--cwd \S+ --target/, "--cwd /repo --target")
|
|
569
|
+
.replace(" --consent ", " --agent pi --lenses-reason REASON --consent ");
|
|
570
|
+
choice.invocation = bound.replace("REASON", () => rendered);
|
|
571
|
+
expected.push(bound.split(" ").slice(1).map((word) => word === "REASON" ? reason : word));
|
|
572
|
+
}
|
|
573
|
+
const consent = decodeReviewConsentV3(rawConsent, "pi");
|
|
574
|
+
assert.ok(consent.choices[0].invocation.includes(` --lenses-reason ${rendered} `));
|
|
575
|
+
return { consent, expected: expected[0]! };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
// Answers the granted choice and proves Pi launched the package binary exactly
|
|
579
|
+
// once with the exact argv, through execFile and never through a shell.
|
|
580
|
+
async function assertLensReasonReplay(rendered: string, reason: string): Promise<void> {
|
|
581
|
+
const { consent, expected } = consentWithLensReason(rendered, reason);
|
|
582
|
+
const queue = queuedAdapter([{ stdout: JSON.stringify(reviewingStartV4(consent.targetIdentity)) }]);
|
|
583
|
+
const answered = await client(queue.adapter).answerConsent({ cwd: "/repo", consent, answer: "granted" });
|
|
584
|
+
assert.equal(answered.kind, "started");
|
|
585
|
+
assert.equal(queue.calls.length, 1);
|
|
586
|
+
assert.equal(queue.calls[0]?.file, "/package/.gentle-ai/gentle-ai");
|
|
587
|
+
assert.deepEqual(queue.calls[0]?.arguments, expected);
|
|
588
|
+
const replayed = queue.calls[0]?.arguments ?? [];
|
|
589
|
+
const reasonIndex = replayed.indexOf("--lenses-reason");
|
|
590
|
+
assert.deepEqual(replayed.slice(reasonIndex, reasonIndex + 2), ["--lenses-reason", reason]);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// gentle-ai renders a lens reason with POSIX shell quoting, so an apostrophe
|
|
594
|
+
// arrives as `'...'\''...'`. Pi splits it into exact argv and never runs a shell.
|
|
595
|
+
test("native consent answer replays a POSIX-quoted apostrophe lens reason as one exact argument", async () => {
|
|
596
|
+
await assertLensReasonReplay("'touched the package'\\''s parser'", "touched the package's parser");
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
test("native consent answer treats a backslash before an apostrophe outside quotes as an escaped apostrophe", async () => {
|
|
600
|
+
await assertLensReasonReplay("package\\'s", "package's");
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
test("native consent answer treats a backslash before a double quote outside quotes as an escaped double quote", async () => {
|
|
604
|
+
await assertLensReasonReplay('say\\"x\\"', 'say"x"');
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
test("native consent answer joins a token across a backslash-escaped space outside quotes", async () => {
|
|
608
|
+
await assertLensReasonReplay("touched\\ parser", "touched parser");
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test("native consent answer keeps a backslash before any other character verbatim outside quotes", async () => {
|
|
612
|
+
await assertLensReasonReplay("C:\\repo", "C:\\repo");
|
|
613
|
+
await assertLensReasonReplay("\\\\server\\share", "\\\\server\\share");
|
|
614
|
+
await assertLensReasonReplay("touched\\nparser", "touched\\nparser");
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
test("native consent answer keeps single quotes literal around double quotes and backslashes", async () => {
|
|
618
|
+
await assertLensReasonReplay("'say \"x\" and \\y'", 'say "x" and \\y');
|
|
619
|
+
await assertLensReasonReplay("'touched the package\\'", "touched the package\\");
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
test("native consent answer keeps backslashes verbatim inside double quotes", async () => {
|
|
623
|
+
await assertLensReasonReplay('"C:\\repo\\"', "C:\\repo\\");
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
test("native consent answer still rejects an unterminated quote before launching the provider", async () => {
|
|
627
|
+
for (const rendered of ["'touched the package'\\''s parser", "'touched the package", '"say x']) {
|
|
628
|
+
const { consent } = consentWithLensReason(rendered, "unused");
|
|
629
|
+
const queue = queuedAdapter([]);
|
|
630
|
+
await assert.rejects(
|
|
631
|
+
() => client(queue.adapter).answerConsent({ cwd: "/repo", consent, answer: "granted" }),
|
|
632
|
+
(error: unknown) => error instanceof TypeError && error.message === "Native consent invocation has invalid quoting",
|
|
633
|
+
);
|
|
634
|
+
assert.deepEqual(queue.calls, []);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
498
638
|
test("native START refuses invalid committed-range and target bindings before STATUS", async () => {
|
|
499
639
|
for (const request of [
|
|
500
640
|
{ cwd: "/repo", baseRef: "origin/main" },
|
|
@@ -665,6 +805,76 @@ test("node execFile adapter passes AbortSignal to child_process", async () => {
|
|
|
665
805
|
await assert.rejects(pending, (error: unknown) => error instanceof Error && error.name === "AbortError");
|
|
666
806
|
});
|
|
667
807
|
|
|
808
|
+
// On Windows, killing gentle-ai alone leaves its git children running against
|
|
809
|
+
// the repository index; abort and timeout must take the whole tree down.
|
|
810
|
+
test("node execFile adapter kills the whole process tree on Windows abort and timeout", async () => {
|
|
811
|
+
const killed: number[] = [];
|
|
812
|
+
const killProcessTree = (pid: number) => { killed.push(pid); process.kill(pid); };
|
|
813
|
+
const adapter = createNodeExecFileAdapter({ platform: "win32", killProcessTree });
|
|
814
|
+
const request = { file: process.execPath, arguments: ["-e", "setTimeout(() => {}, 10_000)"], cwd: process.cwd(), maxBufferBytes: 1024 };
|
|
815
|
+
|
|
816
|
+
const controller = new AbortController();
|
|
817
|
+
const aborted = adapter({ ...request, timeoutMs: 30_000, signal: controller.signal });
|
|
818
|
+
setTimeout(() => controller.abort(), 50);
|
|
819
|
+
await assert.rejects(aborted, (error: unknown) => error instanceof Error && error.name === "AbortError");
|
|
820
|
+
assert.equal(killed.length, 1, "abort kills the tree once");
|
|
821
|
+
|
|
822
|
+
const timedOut = await adapter({ ...request, timeoutMs: 50 });
|
|
823
|
+
assert.equal(timedOut.timedOut, true);
|
|
824
|
+
assert.equal(killed.length, 2, "timeout kills the tree once");
|
|
825
|
+
|
|
826
|
+
const preAborted = new AbortController();
|
|
827
|
+
preAborted.abort();
|
|
828
|
+
await assert.rejects(adapter({ ...request, timeoutMs: 30_000, signal: preAborted.signal }), (error: unknown) => error instanceof Error && error.name === "AbortError");
|
|
829
|
+
assert.equal(killed.length, 2, "an already-aborted call spawns nothing");
|
|
830
|
+
|
|
831
|
+
const finished = await adapter({ ...request, arguments: ["-p", "'done'"], timeoutMs: 30_000 });
|
|
832
|
+
assert.deepEqual([finished.exitCode, finished.stdout.trim(), finished.timedOut], [0, "done", false]);
|
|
833
|
+
assert.equal(killed.length, 2, "a call that finishes kills nothing");
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
// taskkill can be unavailable (AppLocker, a broken PATH): the deadline must
|
|
837
|
+
// still hold, through a direct kill of gentle-ai itself.
|
|
838
|
+
test("node execFile adapter still enforces the timeout on Windows when the tree kill does nothing", async () => {
|
|
839
|
+
const adapter = createNodeExecFileAdapter({ platform: "win32", killProcessTree: () => {} });
|
|
840
|
+
const startedAt = Date.now();
|
|
841
|
+
const result = await adapter({ file: process.execPath, arguments: ["-e", "setTimeout(() => {}, 5_000)"], cwd: process.cwd(), maxBufferBytes: 1024, timeoutMs: 100 });
|
|
842
|
+
assert.equal(result.timedOut, true);
|
|
843
|
+
assert.ok(Date.now() - startedAt < 3_000, `settled after ${Date.now() - startedAt}ms`);
|
|
844
|
+
});
|
|
845
|
+
|
|
846
|
+
// A descendant that inherited gentle-ai's stdout/stderr keeps the pipes open
|
|
847
|
+
// after gentle-ai itself exits; the deadline must still hold.
|
|
848
|
+
test("node execFile adapter enforces the Windows timeout when a descendant holds the inherited pipes", async () => {
|
|
849
|
+
const directory = mkdtempSync(join(tmpdir(), "gentle-pi-tree-kill-"));
|
|
850
|
+
const pidFile = join(directory, "descendant.pid");
|
|
851
|
+
const script = `const { spawn } = require("node:child_process"); const d = spawn(process.execPath, ["-e", "setTimeout(() => {}, 30000)"], { stdio: "inherit" }); require("node:fs").writeFileSync(${JSON.stringify(pidFile)}, String(d.pid)); d.unref();`;
|
|
852
|
+
try {
|
|
853
|
+
const adapter = createNodeExecFileAdapter({ platform: "win32", killProcessTree: () => {} });
|
|
854
|
+
const startedAt = Date.now();
|
|
855
|
+
const pending = adapter({ file: process.execPath, arguments: ["-e", script], cwd: process.cwd(), maxBufferBytes: 1024, timeoutMs: 300 });
|
|
856
|
+
const result = await Promise.race([pending, new Promise<"unsettled">((resolve) => setTimeout(() => resolve("unsettled"), 6_000).unref())]);
|
|
857
|
+
assert.notEqual(result, "unsettled", "the call settles while the descendant still holds the pipes");
|
|
858
|
+
if (result === "unsettled") return;
|
|
859
|
+
assert.equal(result.timedOut, true);
|
|
860
|
+
assert.ok(Date.now() - startedAt < 4_000, `settled after ${Date.now() - startedAt}ms`);
|
|
861
|
+
} finally {
|
|
862
|
+
try { process.kill(Number(readFileSync(pidFile, "utf8"))); } catch { /* Already gone. */ }
|
|
863
|
+
rmSync(directory, { recursive: true, force: true });
|
|
864
|
+
}
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
// A bare "taskkill" resolves from the current directory first on Windows, so a
|
|
868
|
+
// repository shipping its own taskkill.exe would run on any abort or timeout.
|
|
869
|
+
test("Windows tree kill runs taskkill by its absolute System32 path from a trusted cwd", () => {
|
|
870
|
+
const calls: Array<{ file: string; args: readonly string[]; cwd: string }> = [];
|
|
871
|
+
const kill = createWindowsProcessTreeKiller({ SystemRoot: "D:\\Windows" }, (file, args, options) => { calls.push({ file, args, cwd: options.cwd }); return { on() {} }; });
|
|
872
|
+
kill(42);
|
|
873
|
+
assert.deepEqual(calls, [{ file: "D:\\Windows\\System32\\taskkill.exe", args: ["/pid", "42", "/T", "/F"], cwd: "D:\\Windows\\System32" }]);
|
|
874
|
+
assert.equal(windowsTaskkillPath({}), "C:\\Windows\\System32\\taskkill.exe");
|
|
875
|
+
assert.equal(windowsTaskkillPath({ SystemRoot: "relative\\dir" }), "C:\\Windows\\System32\\taskkill.exe", "a relative SystemRoot is never trusted");
|
|
876
|
+
});
|
|
877
|
+
|
|
668
878
|
test("capture-result receives the controller AbortSignal without an automatic mutation timeout", async () => {
|
|
669
879
|
const controller = new AbortController();
|
|
670
880
|
controller.abort();
|
|
@@ -909,3 +1119,28 @@ test("acknowledge-approved keeps its fail-closed stderr and typed refusal discip
|
|
|
909
1119
|
(error: unknown) => error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.EMPTY_OUTPUT,
|
|
910
1120
|
);
|
|
911
1121
|
});
|
|
1122
|
+
|
|
1123
|
+
// S9: this package running from a bundled install (the testing overlay stands
|
|
1124
|
+
// in for its location; the detection itself is tested with real installs in
|
|
1125
|
+
// gentle-ai-binary-isolation.test.ts) and the launcher's GENTLE_SHELL_PREFIX.
|
|
1126
|
+
function bundledIsolation(t: { after: (fn: () => void) => void }): { prefix: string; env: NodeJS.ProcessEnv } {
|
|
1127
|
+
const prefix = realpathSync(mkdtempSync(join(tmpdir(), "gentle-ai-isolation-site-")));
|
|
1128
|
+
setBundledInstallForTesting({ root: prefix });
|
|
1129
|
+
t.after(() => {
|
|
1130
|
+
setBundledInstallForTesting(undefined);
|
|
1131
|
+
rmSync(prefix, { recursive: true, force: true });
|
|
1132
|
+
});
|
|
1133
|
+
return { prefix, env: { PATH: "/usr/bin", GENTLE_SHELL_PREFIX: prefix } };
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
test("the central gentle-ai environment carries Gentle AI's isolation variables only in a bundled install", (t) => {
|
|
1137
|
+
const { prefix, env } = bundledIsolation(t);
|
|
1138
|
+
const bundled = gentleAiProcessEnvironment(env);
|
|
1139
|
+
assert.equal(bundled.GENTLE_AI_HOME, join(prefix, "gentle-ai"));
|
|
1140
|
+
assert.equal(bundled.GENTLE_AI_PI_CONFIG_DIR, join(prefix, "config", "gentle-ai"));
|
|
1141
|
+
assert.equal(bundled.GENTLE_PI_CONFIG_HOME, join(prefix, "config"), "the parent Gentle AI 4.0.0 reads it as");
|
|
1142
|
+
assert.equal(bundled.GENTLE_AI_NO_SELF_UPDATE, "1");
|
|
1143
|
+
assert.equal(bundled.GENTLE_PI_REVIEW_RELAY_CONTRACT, "gentle-pi.review-relay/v1");
|
|
1144
|
+
const ordinary = gentleAiProcessEnvironment({ PATH: "/usr/bin" });
|
|
1145
|
+
assert.deepEqual(ordinary, { PATH: "/usr/bin", GENTLE_PI_REVIEW_RELAY_CONTRACT: "gentle-pi.review-relay/v1" });
|
|
1146
|
+
});
|
|
@@ -87,11 +87,11 @@ function queuedAdapter(outputs: readonly Record<string, unknown>[]): { adapter:
|
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
function client(adapter: ExecFileAdapter): NativeReviewCliV216 {
|
|
90
|
-
return new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined
|
|
90
|
+
return new NativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined);
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
function runtimeClient(adapter: ExecFileAdapter): RuntimeNativeReviewCliV216 {
|
|
94
|
-
return new RuntimeNativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined
|
|
94
|
+
return new RuntimeNativeReviewCliV216(adapter, "/package/.gentle-ai/gentle-ai", 30_000, 1024 * 1024, async () => undefined);
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
test("negotiated ordinary START executes the complete STATUS-rendered relay vector and preserves the consent envelope", async () => {
|
|
@@ -44,7 +44,7 @@ const resolvedBinary = (() => {
|
|
|
44
44
|
}
|
|
45
45
|
})();
|
|
46
46
|
const nativeBinaryGate = requireNativeBinary({ resolvedBinary, digestsPinned: true, env: process.env });
|
|
47
|
-
if (
|
|
47
|
+
if (nativeBinaryGate.run === false) console.log(`native-review-parity-runtime: ${nativeBinaryGate.reason}`);
|
|
48
48
|
const test = nativeBinaryGate.run ? baseTest : baseTest.skip;
|
|
49
49
|
const binary = resolvedBinary ?? "";
|
|
50
50
|
|
|
@@ -181,8 +181,6 @@ interface ParityRuntime {
|
|
|
181
181
|
interface ParityRuntimeOptions {
|
|
182
182
|
candidateViews?: CandidateViewRegistry;
|
|
183
183
|
pendingReviewConsentRegistry?: PendingReviewConsentRegistry;
|
|
184
|
-
now?: () => number;
|
|
185
|
-
scheduleTimer?: (callback: () => void, delayMs: number) => { unref: () => void };
|
|
186
184
|
}
|
|
187
185
|
|
|
188
186
|
function parityRuntime(nativeReviewCli: NativeReviewCli | null, options: ParityRuntimeOptions = {}): ParityRuntime {
|
|
@@ -193,8 +191,6 @@ function parityRuntime(nativeReviewCli: NativeReviewCli | null, options: ParityR
|
|
|
193
191
|
nativeReviewCli,
|
|
194
192
|
candidateViews: options.candidateViews ?? new CandidateViewRegistry(),
|
|
195
193
|
pendingReviewConsentRegistry: options.pendingReviewConsentRegistry ?? new PendingReviewConsentRegistry(),
|
|
196
|
-
now: options.now,
|
|
197
|
-
scheduleTimer: options.scheduleTimer,
|
|
198
194
|
} as unknown as Parameters<typeof createGentleAiExtension>[0];
|
|
199
195
|
__testing.createGentleAiExtension(dependencies)({
|
|
200
196
|
on(name: string, handler: RegisteredEvent) { events.set(name, handler); },
|
|
@@ -330,7 +326,7 @@ async function answerConsent(runtime: ParityRuntime, cwd: string, binding: unkno
|
|
|
330
326
|
// that names itself and its exit. It still carries the current negotiated
|
|
331
327
|
// STATUS as reconciliation context, but it never reads as a healthy
|
|
332
328
|
// pre-start "ready".
|
|
333
|
-
function assertStaleConsentBinding(outcome: Record<string, unknown>, binding: unknown, code: "consent-binding-unknown" | "consent-binding-
|
|
329
|
+
function assertStaleConsentBinding(outcome: Record<string, unknown>, binding: unknown, code: "consent-binding-unknown" | "consent-binding-already-consumed"): void {
|
|
334
330
|
assert.equal(outcome.status, "blocked");
|
|
335
331
|
assert.equal(outcome.outcome, "consent-binding-stale");
|
|
336
332
|
assert.equal(outcome.consent_binding, binding);
|
|
@@ -677,7 +673,7 @@ test("unavailable and ambiguous consent follow-ups never replay a consumed bindi
|
|
|
677
673
|
assert.equal(statusCalls, callsBeforeStaleReconciliation + 1, "already-consumed binding reconciliation performs exactly one additional STATUS call");
|
|
678
674
|
});
|
|
679
675
|
|
|
680
|
-
test("concurrent answers atomically claim consent before review-mode gating
|
|
676
|
+
test("concurrent answers atomically claim consent before review-mode gating", async (t) => {
|
|
681
677
|
const cwd = repository(t);
|
|
682
678
|
const fixture = consentNative(cwd);
|
|
683
679
|
const candidateViews = new CandidateViewRegistry();
|
|
@@ -687,7 +683,6 @@ test("concurrent answers atomically claim consent before review-mode gating and
|
|
|
687
683
|
cleanupCalls += 1;
|
|
688
684
|
cleanup(token);
|
|
689
685
|
};
|
|
690
|
-
const scheduled: Array<() => void> = [];
|
|
691
686
|
let releaseModeGate: (() => void) | undefined;
|
|
692
687
|
const modeGate = new Promise<void>((resolve) => { releaseModeGate = resolve; });
|
|
693
688
|
let modeCalls = 0;
|
|
@@ -702,13 +697,12 @@ test("concurrent answers atomically claim consent before review-mode gating and
|
|
|
702
697
|
let nativeAnswerCalls = 0;
|
|
703
698
|
fixture.native.answerConsent = async (request) => {
|
|
704
699
|
nativeAnswerCalls += 1;
|
|
705
|
-
assert.equal(cleanupCalls, 0, "
|
|
700
|
+
assert.equal(cleanupCalls, 0, "candidate authority remains live before the provider answer");
|
|
706
701
|
await providerGate;
|
|
707
702
|
return await nativeAnswer(request);
|
|
708
703
|
};
|
|
709
704
|
const runtime = parityRuntime(fixture.native, {
|
|
710
705
|
candidateViews,
|
|
711
|
-
scheduleTimer: (callback) => { scheduled.push(callback); return { unref() {} }; },
|
|
712
706
|
});
|
|
713
707
|
const blocked = await beginConsent(runtime, cwd);
|
|
714
708
|
fixture.native.reviewMode = async () => {
|
|
@@ -726,9 +720,7 @@ test("concurrent answers atomically claim consent before review-mode gating and
|
|
|
726
720
|
assert.equal(modeCalls, 1, "the first answer pauses at the asynchronous review-mode gate");
|
|
727
721
|
const second = answerConsent(runtime, cwd, blocked.consent_binding, "declined");
|
|
728
722
|
await new Promise<void>((resolve) => setImmediate(resolve));
|
|
729
|
-
assert.equal(
|
|
730
|
-
scheduled[0]!();
|
|
731
|
-
assert.equal(cleanupCalls, 0, "a queued expiry callback is a harmless no-op after the answer claim");
|
|
723
|
+
assert.equal(cleanupCalls, 0, "claimed consent retains candidate authority until the answer settles");
|
|
732
724
|
|
|
733
725
|
releaseModeGate!();
|
|
734
726
|
releaseProvider!();
|
|
@@ -741,47 +733,19 @@ test("concurrent answers atomically claim consent before review-mode gating and
|
|
|
741
733
|
assert.equal(cleanupCalls, 1, "only the answered binding cleans candidate authority");
|
|
742
734
|
});
|
|
743
735
|
|
|
744
|
-
test("
|
|
736
|
+
test("unanswered consent remains reusable after the former deadline and answers only once", async (t) => {
|
|
745
737
|
const cwd = repository(t);
|
|
746
738
|
const fixture = consentNative(cwd);
|
|
747
|
-
const
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const scheduled: Array<() => void> = [];
|
|
751
|
-
const runtime = parityRuntime(fixture.native, {
|
|
752
|
-
pendingReviewConsentRegistry: registry,
|
|
753
|
-
now: () => now,
|
|
754
|
-
scheduleTimer: (callback) => { scheduled.push(callback); return { unref() {} }; },
|
|
755
|
-
});
|
|
739
|
+
const runtime = parityRuntime(fixture.native);
|
|
740
|
+
let now = 1_000;
|
|
741
|
+
t.mock.method(Date, "now", () => now);
|
|
756
742
|
const first = await beginConsent(runtime, cwd);
|
|
743
|
+
now += 24 * 60 * 60 * 1000;
|
|
757
744
|
const reused = await beginConsent(runtime, cwd);
|
|
758
745
|
assert.equal(reused.consent_binding, first.consent_binding);
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
// session-local disposition remains only to classify this stale answer.
|
|
763
|
-
scheduled[0]!();
|
|
764
|
-
const timerExpired = await answerConsent(runtime, cwd, first.consent_binding, "declined");
|
|
765
|
-
assert.equal(timerExpired.operation, "answer-consent");
|
|
766
|
-
assertStaleConsentBinding(timerExpired, first.consent_binding, "consent-binding-expired");
|
|
767
|
-
assert.match(String((timerExpired.diagnostics as { message?: unknown }).message), /expired after 10 minutes/);
|
|
768
|
-
const second = await beginConsent(runtime, cwd);
|
|
769
|
-
assert.notEqual(second.consent_binding, first.consent_binding);
|
|
770
|
-
assert.equal(scheduled.length, 2);
|
|
771
|
-
assertStaleConsentBinding(await answerConsent(runtime, cwd, first.consent_binding, "declined"), first.consent_binding, "consent-binding-expired");
|
|
772
|
-
|
|
773
|
-
// No second callback runs: START synchronously prunes this unused binding
|
|
774
|
-
// before it can reuse the frozen candidate, and its answer stays typed expiry.
|
|
775
|
-
now += 10 * 60 * 1000;
|
|
776
|
-
const third = await beginConsent(runtime, cwd);
|
|
777
|
-
assert.notEqual(third.consent_binding, second.consent_binding);
|
|
778
|
-
assertStaleConsentBinding(await answerConsent(runtime, cwd, second.consent_binding, "declined"), second.consent_binding, "consent-binding-expired");
|
|
779
|
-
|
|
780
|
-
const reloaded = parityRuntime(fixture.native, { pendingReviewConsentRegistry: new PendingReviewConsentRegistry() });
|
|
781
|
-
assertStaleConsentBinding(await answerConsent(reloaded, cwd, first.consent_binding, "declined"), first.consent_binding, "consent-binding-unknown");
|
|
782
|
-
const afterReload = await beginConsent(reloaded, cwd);
|
|
783
|
-
assert.notEqual(afterReload.consent_binding, third.consent_binding);
|
|
784
|
-
assert.equal(fixture.starts.count, 5);
|
|
746
|
+
const answered = await answerConsent(runtime, cwd, first.consent_binding, "declined");
|
|
747
|
+
assert.equal(answered.outcome, "consent-declined-this-candidate");
|
|
748
|
+
assertStaleConsentBinding(await answerConsent(runtime, cwd, first.consent_binding, "declined"), first.consent_binding, "consent-binding-already-consumed");
|
|
785
749
|
});
|
|
786
750
|
|
|
787
751
|
test("native START maps only provider facts and omits absent evidence", async (t) => {
|
|
@@ -89,3 +89,24 @@ test("an empty file is named before it is described", () => {
|
|
|
89
89
|
test("a reason with no path contributes no phrase", () => {
|
|
90
90
|
assert.deepEqual(nativeRiskEvidencePhrases("high", [{ code: "empty_content" }]), []);
|
|
91
91
|
});
|
|
92
|
+
|
|
93
|
+
// Field truth from gentle-ai main (4.1.0-dev): one committed high-risk
|
|
94
|
+
// candidate's consent/v3 `risk_evidence` beside its START/v4 `risk_reasons`.
|
|
95
|
+
test("dangerous sink and agent escalation reasons render the gentle-ai phrases", () => {
|
|
96
|
+
assert.deepEqual(
|
|
97
|
+
nativeRiskEvidencePhrases("high", [
|
|
98
|
+
{ code: "dangerous_sink", signal: "dangerous_sink", path: "auth/session.py" },
|
|
99
|
+
{ code: "hot_path", signal: "auth", path: "auth/session.py" },
|
|
100
|
+
{ code: "process_boundary", signal: "shell_process", path: "auth/session.py" },
|
|
101
|
+
]),
|
|
102
|
+
[
|
|
103
|
+
"a dangerous code pattern in auth/session.py",
|
|
104
|
+
"authentication in auth/session.py",
|
|
105
|
+
"code that starts other processes in auth/session.py",
|
|
106
|
+
],
|
|
107
|
+
);
|
|
108
|
+
assert.deepEqual(
|
|
109
|
+
nativeRiskEvidencePhrases("high", [{ code: "agent_escalation", signal: "agent_escalation" }]),
|
|
110
|
+
["the agent that made this change flagged it as high risk"],
|
|
111
|
+
);
|
|
112
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
// gentle-shell#1731 T13 (evidence L24): in bench task x4 the user wrote "I may
|
|
7
|
+
// stop you between the two and pick the second part up later"; every Gentle
|
|
8
|
+
// cell delivered only the first part and paused by itself. Hedged delivery
|
|
9
|
+
// wording asks for resumable notes and separate commits, never a stop, and
|
|
10
|
+
// the close step must not leave tracked tasks pending without a user stop.
|
|
11
|
+
|
|
12
|
+
const REPO_ROOT = join(import.meta.dirname, "..");
|
|
13
|
+
const read = (relative: string): string => readFileSync(join(REPO_ROOT, relative), "utf8");
|
|
14
|
+
|
|
15
|
+
const tracking = read("assets/orchestrator-tracking.md");
|
|
16
|
+
const core = read("assets/orchestrator.md");
|
|
17
|
+
|
|
18
|
+
function authorizationSection(): string {
|
|
19
|
+
const start = tracking.indexOf("#### Authorization and progress");
|
|
20
|
+
assert.ok(start !== -1, "tracking module must keep its Authorization and progress section");
|
|
21
|
+
const next = tracking.indexOf("\n#### ", start + 1);
|
|
22
|
+
return tracking.slice(start, next === -1 ? undefined : next);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
test("T13: conditional change intent is narrowed to whether a change is authorized at all", () => {
|
|
26
|
+
const section = authorizationSection();
|
|
27
|
+
assert.ok(
|
|
28
|
+
section.includes(
|
|
29
|
+
"Ambiguous or conditional change intent (unclear whether a change is authorized at all) requires one clarification; stop and wait.",
|
|
30
|
+
),
|
|
31
|
+
"the clarification clause must be scoped to authorization itself",
|
|
32
|
+
);
|
|
33
|
+
assert.ok(
|
|
34
|
+
!section.includes("Ambiguous or conditional change intent requires one clarification"),
|
|
35
|
+
"the unscoped clarification clause must be gone",
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("T13: hedged delivery wording asks for resumable notes and separate commits, not a stop", () => {
|
|
40
|
+
const section = authorizationSection();
|
|
41
|
+
for (const clause of [
|
|
42
|
+
"Hedged delivery wording (the user may stop you, review commits one at a time, or pick the work up later)",
|
|
43
|
+
"asks for resumable notes and separate commits, not a stop",
|
|
44
|
+
"complete every authorized task in the current turn unless the user explicitly says to wait",
|
|
45
|
+
]) {
|
|
46
|
+
assert.ok(section.includes(clause), `tracking module is missing: ${clause}`);
|
|
47
|
+
}
|
|
48
|
+
const hedge = section.slice(section.indexOf("Hedged delivery wording"));
|
|
49
|
+
const sentence = hedge.slice(0, hedge.indexOf(". ") + 1);
|
|
50
|
+
assert.doesNotMatch(sentence, /\bask (?:one|a|the user)\b|question/i, "the hedge rule must not add a product or design question");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("T13: the Close gate requires continuing or quoting the user's explicit stop", () => {
|
|
54
|
+
const section = authorizationSection();
|
|
55
|
+
assert.ok(
|
|
56
|
+
section.includes(
|
|
57
|
+
"Close gate: before the final answer, if any tracked task in the feature document or `todo` is still pending, continue it or quote the exact user sentence that orders the stop.",
|
|
58
|
+
),
|
|
59
|
+
"tracking module must carry the Close gate",
|
|
60
|
+
);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("T13: budgets hold and the pinned human-control line is untouched", () => {
|
|
64
|
+
const bytes = Buffer.byteLength(tracking, "utf8");
|
|
65
|
+
assert.ok(bytes <= 12_600, `orchestrator-tracking.md is ${bytes} B, over its 12,600 B budget`);
|
|
66
|
+
assert.ok(core.includes("- Preserve human control: user decisions beat agent momentum."), "core human-control line must stay verbatim");
|
|
67
|
+
assert.ok(core.includes("4. **Track**") && core.includes("`orchestrator-tracking.md`"), "Track must still load the tracking module");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("T13: the always-on harness narrows conditional intent and gates Close on pending tasks", () => {
|
|
71
|
+
// The harness ODD steps are injected on every request, including the small
|
|
72
|
+
// path that never loads the tracking module, so the narrowing must live there too.
|
|
73
|
+
const harness = read("extensions/gentle-ai.ts");
|
|
74
|
+
assert.ok(
|
|
75
|
+
harness.includes("Ambiguous or conditional change intent (unclear whether a change is authorized at all) gets one clarification; stop and wait."),
|
|
76
|
+
"harness Authorize step must narrow conditional intent",
|
|
77
|
+
);
|
|
78
|
+
assert.ok(!harness.includes("Ambiguous or conditional change intent gets one clarification"), "the broad harness wording must be gone");
|
|
79
|
+
assert.ok(
|
|
80
|
+
harness.includes("A user saying they may stop you or resume later asks for notes and separate commits, not a stop."),
|
|
81
|
+
"harness must say hedged delivery wording is not a stop",
|
|
82
|
+
);
|
|
83
|
+
assert.ok(
|
|
84
|
+
harness.includes("Never end with a tracked task pending unless you quote the user's explicit stop."),
|
|
85
|
+
"harness Close step must gate pending tracked tasks",
|
|
86
|
+
);
|
|
87
|
+
});
|