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
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { delimiter, dirname, join } from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import { bundledGentleAiPaths, gentleAiIsolationEnv } from "../lib/gentle-ai-binary.ts";
|
|
7
|
+
import { resetRunningBundledPrefixForTesting, runningBundledPrefix } from "../lib/gentle-shell-bundled.ts";
|
|
8
|
+
import { engramPinVersion } from "../scripts/installer-downloads.mjs";
|
|
9
|
+
|
|
10
|
+
// S9: gentle-pi's own gentle-ai runs in a bundled install keep Gentle AI's
|
|
11
|
+
// state, persona and Engram inside the prefix. Only this package inside the
|
|
12
|
+
// prefix's versions/ honors the launcher's GENTLE_SHELL_PREFIX.
|
|
13
|
+
|
|
14
|
+
const ID = "4.1.0-1.1.0";
|
|
15
|
+
|
|
16
|
+
// A valid bundled install: the version's marker, and `current` naming it (a
|
|
17
|
+
// relative symlink on POSIX, a pointer file on Windows), as bundled-install.mjs writes them.
|
|
18
|
+
function bundledPrefix(t: test.TestContext, { marked = true, current = ID } = {}) {
|
|
19
|
+
const prefix = realpathSync(mkdtempSync(join(tmpdir(), "gentle-ai-isolation-")));
|
|
20
|
+
t.after(() => rmSync(prefix, { recursive: true, force: true }));
|
|
21
|
+
const version = join(prefix, "versions", ID);
|
|
22
|
+
const packageRoot = join(version, "node_modules", "gentle-pi");
|
|
23
|
+
mkdirSync(packageRoot, { recursive: true });
|
|
24
|
+
if (marked) writeFileSync(join(version, ".gentle-shell-version"), `${ID}\n`);
|
|
25
|
+
if (process.platform === "win32") writeFileSync(join(prefix, "current"), `${current}\n`);
|
|
26
|
+
else symlinkSync(`versions/${current}`, join(prefix, "current"));
|
|
27
|
+
const engram = join(prefix, "runtime", `engram-${engramPinVersion}`, process.platform === "win32" ? "engram.exe" : "engram");
|
|
28
|
+
mkdirSync(join(engram, ".."), { recursive: true });
|
|
29
|
+
writeFileSync(engram, "engram", { mode: 0o700 });
|
|
30
|
+
return { prefix, packageRoot, engram };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// B2a: gentle-pi's bundled config folder is <prefix>/config/gentle-ai, so a
|
|
34
|
+
// Gentle AI 4.0.0 given GENTLE_PI_CONFIG_HOME=<prefix>/config (its parent, the
|
|
35
|
+
// way 4.0.0 reads it) writes the background-subagents policy into it.
|
|
36
|
+
test("bundledGentleAiPaths names the prefix's Gentle AI home, gentle-pi config folder and its parent, and pinned Engram", () => {
|
|
37
|
+
assert.deepEqual(bundledGentleAiPaths("/home/ana/.gentle-shell", "linux"), {
|
|
38
|
+
home: "/home/ana/.gentle-shell/gentle-ai",
|
|
39
|
+
piConfigParent: "/home/ana/.gentle-shell/config",
|
|
40
|
+
piConfig: "/home/ana/.gentle-shell/config/gentle-ai",
|
|
41
|
+
engram: `/home/ana/.gentle-shell/runtime/engram-${engramPinVersion}/engram`,
|
|
42
|
+
});
|
|
43
|
+
assert.equal(bundledGentleAiPaths("C:\\Users\\Ana\\AppData\\Local\\gentle-shell", "win32").engram,
|
|
44
|
+
`C:\\Users\\Ana\\AppData\\Local\\gentle-shell\\runtime\\engram-${engramPinVersion}\\engram.exe`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("gentleAiIsolationEnv adds the four Gentle AI variables for this package inside a bundled prefix", (t) => {
|
|
48
|
+
const { prefix, packageRoot, engram } = bundledPrefix(t);
|
|
49
|
+
const base = { PATH: "/usr/bin", GENTLE_SHELL_PREFIX: prefix };
|
|
50
|
+
const env = gentleAiIsolationEnv(base, { packageRoot });
|
|
51
|
+
assert.deepEqual(env, {
|
|
52
|
+
...base,
|
|
53
|
+
GENTLE_AI_HOME: join(prefix, "gentle-ai"),
|
|
54
|
+
GENTLE_AI_PI_CONFIG_DIR: join(prefix, "config", "gentle-ai"),
|
|
55
|
+
GENTLE_AI_ENGRAM_BIN: engram,
|
|
56
|
+
GENTLE_AI_NO_SELF_UPDATE: "1",
|
|
57
|
+
GENTLE_PI_CONFIG_HOME: join(prefix, "config"),
|
|
58
|
+
// #2105: Gentle AI 4.0.0 ignores GENTLE_AI_ENGRAM_BIN and looks `engram` up on PATH
|
|
59
|
+
// first; with the pinned one first it never falls back to `brew tap`.
|
|
60
|
+
PATH: `${dirname(engram)}${delimiter}/usr/bin`,
|
|
61
|
+
});
|
|
62
|
+
assert.equal("GENTLE_AI_HOME" in base, false, "the caller's environment is not changed");
|
|
63
|
+
// Without the published Engram, Gentle AI gets no Engram to use, and PATH stays as it was.
|
|
64
|
+
rmSync(engram);
|
|
65
|
+
const without = gentleAiIsolationEnv(base, { packageRoot });
|
|
66
|
+
assert.equal("GENTLE_AI_ENGRAM_BIN" in without, false);
|
|
67
|
+
assert.equal(without.PATH, "/usr/bin");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("gentleAiIsolationEnv returns the environment unchanged outside a bundled install", (t) => {
|
|
71
|
+
const { prefix, packageRoot } = bundledPrefix(t);
|
|
72
|
+
const unchanged = (env: NodeJS.ProcessEnv, root = packageRoot) => assert.equal(gentleAiIsolationEnv(env, { packageRoot: root }), env);
|
|
73
|
+
unchanged({ PATH: "/usr/bin" });
|
|
74
|
+
unchanged({ GENTLE_SHELL_PREFIX: "" });
|
|
75
|
+
unchanged({ GENTLE_SHELL_PREFIX: "relative/prefix" });
|
|
76
|
+
unchanged({ GENTLE_SHELL_PREFIX: join(prefix, "missing") });
|
|
77
|
+
// A gentle-pi outside the prefix's versions (the user's own, run from the
|
|
78
|
+
// agent's shell, which inherits GENTLE_SHELL_PREFIX) keeps its own Gentle AI.
|
|
79
|
+
const outside = join(prefix, "elsewhere", "gentle-pi");
|
|
80
|
+
mkdirSync(outside, { recursive: true });
|
|
81
|
+
unchanged({ GENTLE_SHELL_PREFIX: prefix }, outside);
|
|
82
|
+
unchanged({ GENTLE_SHELL_PREFIX: prefix }, join(prefix, "versions"));
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// A2: only a valid bundled install, never a folder that merely sits under a versions/.
|
|
86
|
+
test("gentleAiIsolationEnv gives nothing to an nvm gentle-pi or an unmarked version", (t) => {
|
|
87
|
+
const nvm = realpathSync(mkdtempSync(join(tmpdir(), "gentle-ai-isolation-nvm-")));
|
|
88
|
+
t.after(() => rmSync(nvm, { recursive: true, force: true }));
|
|
89
|
+
const nvmPackage = join(nvm, ".nvm", "versions", "node", "v24.21.0", "lib", "node_modules", "gentle-pi");
|
|
90
|
+
mkdirSync(nvmPackage, { recursive: true });
|
|
91
|
+
const nvmEnv = { GENTLE_SHELL_PREFIX: join(nvm, ".nvm") };
|
|
92
|
+
assert.equal(gentleAiIsolationEnv(nvmEnv, { packageRoot: nvmPackage }), nvmEnv);
|
|
93
|
+
const { prefix: unmarked, packageRoot: unmarkedRoot } = bundledPrefix(t, { marked: false });
|
|
94
|
+
const unmarkedEnv = { GENTLE_SHELL_PREFIX: unmarked };
|
|
95
|
+
assert.equal(gentleAiIsolationEnv(unmarkedEnv, { packageRoot: unmarkedRoot }), unmarkedEnv);
|
|
96
|
+
// A valid install, but the variable names another folder.
|
|
97
|
+
const { packageRoot } = bundledPrefix(t);
|
|
98
|
+
const other = { GENTLE_SHELL_PREFIX: nvm };
|
|
99
|
+
assert.equal(gentleAiIsolationEnv(other, { packageRoot }), other);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// The answer is remembered per package root and GENTLE_SHELL_PREFIX value: one
|
|
103
|
+
// process gets each env's own answer, and a `current` that moves while Pi runs
|
|
104
|
+
// does not change a remembered one.
|
|
105
|
+
test("runningBundledPrefix answers per env in one process and keeps a remembered answer when current moves", (t) => {
|
|
106
|
+
resetRunningBundledPrefixForTesting();
|
|
107
|
+
t.after(resetRunningBundledPrefixForTesting);
|
|
108
|
+
const { prefix, packageRoot } = bundledPrefix(t);
|
|
109
|
+
const other = realpathSync(mkdtempSync(join(tmpdir(), "gentle-ai-isolation-other-")));
|
|
110
|
+
t.after(() => rmSync(other, { recursive: true, force: true }));
|
|
111
|
+
assert.equal(runningBundledPrefix({ GENTLE_SHELL_PREFIX: prefix }, { packageRoot }), prefix);
|
|
112
|
+
assert.equal(runningBundledPrefix({ GENTLE_SHELL_PREFIX: other }, { packageRoot }), undefined);
|
|
113
|
+
assert.equal(runningBundledPrefix({}, { packageRoot }), undefined);
|
|
114
|
+
assert.equal(runningBundledPrefix({ GENTLE_SHELL_PREFIX: prefix }, { packageRoot }), prefix);
|
|
115
|
+
// An upgrade switches current to another version while this one runs.
|
|
116
|
+
rmSync(join(prefix, "current"));
|
|
117
|
+
if (process.platform === "win32") writeFileSync(join(prefix, "current"), "4.2.0-1.1.0\n");
|
|
118
|
+
else symlinkSync("versions/4.2.0-1.1.0", join(prefix, "current"));
|
|
119
|
+
// The version stops being a finished install (its marker is gone).
|
|
120
|
+
rmSync(join(prefix, "versions", ID, ".gentle-shell-version"));
|
|
121
|
+
assert.equal(runningBundledPrefix({ GENTLE_SHELL_PREFIX: prefix }, { packageRoot }), prefix, "remembered");
|
|
122
|
+
resetRunningBundledPrefixForTesting();
|
|
123
|
+
assert.equal(runningBundledPrefix({ GENTLE_SHELL_PREFIX: prefix }, { packageRoot }), undefined, "decided again after a reset");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// N1: a subagent started after `gentle-shell upgrade` moved `current` runs from
|
|
127
|
+
// the previous version, which the prefix still keeps: it stays bundled.
|
|
128
|
+
test("a kept, marked version that current no longer names is still bundled for its own runs", (t) => {
|
|
129
|
+
resetRunningBundledPrefixForTesting();
|
|
130
|
+
t.after(resetRunningBundledPrefixForTesting);
|
|
131
|
+
const { prefix, packageRoot } = bundledPrefix(t, { current: "4.2.0-1.1.0" });
|
|
132
|
+
const env = { GENTLE_SHELL_PREFIX: prefix };
|
|
133
|
+
assert.equal(runningBundledPrefix(env, { packageRoot }), prefix);
|
|
134
|
+
assert.equal(gentleAiIsolationEnv(env, { packageRoot }).GENTLE_AI_HOME, join(prefix, "gentle-ai"));
|
|
135
|
+
});
|
|
@@ -32,7 +32,7 @@ const nativeBinaryGate = requireNativeBinary({
|
|
|
32
32
|
digestsPinned: releaseDigestsPinned,
|
|
33
33
|
env: process.env,
|
|
34
34
|
});
|
|
35
|
-
if (
|
|
35
|
+
if (nativeBinaryGate.run === false) console.log(`gentle-ai-binary: ${nativeBinaryGate.reason}`);
|
|
36
36
|
const verifiedBinaryTest = nativeBinaryGate.run && process.platform !== "win32" ? test : test.skip;
|
|
37
37
|
|
|
38
38
|
interface PinnedBinaryIsolation {
|
|
@@ -44,6 +44,7 @@ interface PinnedBinaryIsolation {
|
|
|
44
44
|
let pinnedBinaryIsolation: PinnedBinaryIsolation | undefined;
|
|
45
45
|
|
|
46
46
|
test.beforeEach((t) => {
|
|
47
|
+
assert.ok("after" in t);
|
|
47
48
|
const home = mkdtempSync(join(tmpdir(), "gentle-pi-pinned-binary-home-"));
|
|
48
49
|
const environment: GentleAiDevBinaryEnvironment = { env: { ...process.env }, home };
|
|
49
50
|
delete environment.env.GENTLE_PI_CONFIG_HOME;
|
|
@@ -97,7 +98,7 @@ async function writeWindowsSourceBinary(packageRoot: string): Promise<{ binaryPa
|
|
|
97
98
|
method: "go-sumdb-source-build",
|
|
98
99
|
package: "github.com/gentleman-programming/gentle-ai/v4/cmd/gentle-ai",
|
|
99
100
|
module: "github.com/gentleman-programming/gentle-ai/v4",
|
|
100
|
-
tag: "v4.
|
|
101
|
+
tag: "v4.1.1",
|
|
101
102
|
architecture: process.arch === "x64" ? "x64" : "arm64",
|
|
102
103
|
binarySha256: createHash("sha256").update(binary).digest("hex"),
|
|
103
104
|
moduleChecksum: GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM,
|
|
@@ -174,6 +175,22 @@ test("runtime validates a Windows Go SumDB source manifest and rejects tampering
|
|
|
174
175
|
assert.doesNotMatch(gentleAiBinaryPathForTest(packageRoot), /(^|[/\\])PATH($|[/\\])/i);
|
|
175
176
|
});
|
|
176
177
|
|
|
178
|
+
// Every native call resolved the binary by reading and hashing all of it; on
|
|
179
|
+
// Windows each read is also scanned by antivirus. The digest is reused only
|
|
180
|
+
// while the file keeps its identity (device, inode, size, mtime and ctime).
|
|
181
|
+
test("runtime reuses the binary digest while the file is unchanged and re-hashes after any rewrite", async () => {
|
|
182
|
+
const packageRoot = await mkdtemp(join(tmpdir(), "gentle-pi-binary-digest-cache-"));
|
|
183
|
+
const { binaryPath } = await writeWindowsSourceBinary(packageRoot);
|
|
184
|
+
let reads = 0;
|
|
185
|
+
const countingRead = (path: string) => { reads++; return readFileSync(path); };
|
|
186
|
+
assert.equal(resolveGentleAiBinary(packageRoot, "win32", countingRead), binaryPath);
|
|
187
|
+
assert.equal(resolveGentleAiBinary(packageRoot, "win32", countingRead), binaryPath);
|
|
188
|
+
assert.equal(reads, 1, "an unchanged binary is hashed once");
|
|
189
|
+
await writeFile(binaryPath, "tampered Windows source build");
|
|
190
|
+
assert.throws(() => resolveGentleAiBinary(packageRoot, "win32", countingRead), /package-local-binary-missing/);
|
|
191
|
+
assert.equal(reads, 2, "a rewritten binary is hashed again, even with the same size");
|
|
192
|
+
});
|
|
193
|
+
|
|
177
194
|
function gentleAiBinaryPathForTest(packageRoot: string): string {
|
|
178
195
|
return join(packageRoot, ".gentle-ai", RUNTIME_DIRECTORY, "gentle-ai.exe");
|
|
179
196
|
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import test, { type TestContext } from "node:test";
|
|
6
|
+
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
8
|
+
import { bindSessionRepositoryPreparation } from "../lib/bounded-writer-admission.ts";
|
|
9
|
+
import { GENTLE_AI_TIMING_ENTRY } from "../lib/gentle-ai-elapsed-store.ts";
|
|
10
|
+
import { REVIEW_SIDEBAR_EVENT } from "../lib/review-sidebar-state.ts";
|
|
11
|
+
import { YOLO_STATUS_KEY } from "../lib/yolo-session-policy.ts";
|
|
12
|
+
|
|
13
|
+
// gentle-shell#1690: the package is forwarded to delegated rpc children
|
|
14
|
+
// (GENTLE_PI_AGENTS_CHILD=1, where ctx.hasUI is true). Parent-owned startup
|
|
15
|
+
// work and shared-state writes must not run there; each case pairs the child
|
|
16
|
+
// with a parent control so the guard cannot pass vacuously.
|
|
17
|
+
|
|
18
|
+
type Handler = (event: unknown, ctx: ExtensionContext) => unknown;
|
|
19
|
+
|
|
20
|
+
function isolate(t: TestContext): { root: string; cwd: string; agentHome: string } {
|
|
21
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "gentle-pi-child-guards-")));
|
|
22
|
+
const cwd = join(root, "project");
|
|
23
|
+
const agentHome = join(root, "agent-home");
|
|
24
|
+
const configHome = join(root, "config");
|
|
25
|
+
for (const path of [cwd, agentHome, configHome]) mkdirSync(path, { recursive: true });
|
|
26
|
+
const previous = { agentHome: process.env.GENTLE_PI_AGENT_HOME, configHome: process.env.GENTLE_PI_CONFIG_HOME };
|
|
27
|
+
process.env.GENTLE_PI_AGENT_HOME = agentHome;
|
|
28
|
+
process.env.GENTLE_PI_CONFIG_HOME = configHome;
|
|
29
|
+
t.after(() => {
|
|
30
|
+
if (previous.agentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
|
|
31
|
+
else process.env.GENTLE_PI_AGENT_HOME = previous.agentHome;
|
|
32
|
+
if (previous.configHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
33
|
+
else process.env.GENTLE_PI_CONFIG_HOME = previous.configHome;
|
|
34
|
+
rmSync(root, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
return { root, cwd, agentHome };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function harness(child: boolean, cwd: string) {
|
|
40
|
+
const handlers = new Map<string, Handler>();
|
|
41
|
+
const entries: Array<{ type: string; customType: string; data: unknown }> = [];
|
|
42
|
+
const nativeAccesses: string[] = [];
|
|
43
|
+
const sweeps: string[] = [];
|
|
44
|
+
const prompts: string[] = [];
|
|
45
|
+
const statuses: Array<[string, string | undefined]> = [];
|
|
46
|
+
const emitted: Array<{ name: string; payload: unknown }> = [];
|
|
47
|
+
const tools = new Map<string, ToolDefinition>();
|
|
48
|
+
const pi = {
|
|
49
|
+
on: (name: string, handler: Handler) => { handlers.set(name, handler); },
|
|
50
|
+
events: { on() {}, emit: (name: string, payload: unknown) => { emitted.push({ name, payload }); } },
|
|
51
|
+
appendEntry: (customType: string, data: unknown) => { entries.push({ type: "custom", customType, data }); },
|
|
52
|
+
getThinkingLevel: () => "medium",
|
|
53
|
+
registerTool: (tool: ToolDefinition) => { tools.set(tool.name, tool); },
|
|
54
|
+
registerCommand() {}, registerShortcut() {}, registerMessageRenderer() {},
|
|
55
|
+
} as unknown as ExtensionAPI;
|
|
56
|
+
// Any read of the native CLI means the session reached review negotiation
|
|
57
|
+
// or repository preparation.
|
|
58
|
+
const nativeReviewCli = new Proxy({}, {
|
|
59
|
+
get: (_target, key) => { nativeAccesses.push(String(key)); return undefined; },
|
|
60
|
+
});
|
|
61
|
+
const candidateViews = { sweepOrphans: (path: string) => { sweeps.push(path); }, cleanupAll() {} };
|
|
62
|
+
createGentleAiExtension({
|
|
63
|
+
processEnv: { GENTLE_PI_AGENTS_CHILD: child ? "1" : "0", GENTLE_AI_TELEMETRY: "0" },
|
|
64
|
+
nativeReviewCli: nativeReviewCli as never,
|
|
65
|
+
candidateViews: candidateViews as never,
|
|
66
|
+
childStandingReviewPermissionClient: { requestAuthorization: async () => false, close() {} },
|
|
67
|
+
})(pi);
|
|
68
|
+
const sessionManager = { getSessionId: () => `session-${child ? "child" : "parent"}`, getCwd: () => cwd, getEntries: () => entries, getBranch: () => entries };
|
|
69
|
+
const ctx = {
|
|
70
|
+
cwd,
|
|
71
|
+
mode: "rpc",
|
|
72
|
+
hasUI: true,
|
|
73
|
+
sessionManager,
|
|
74
|
+
ui: {
|
|
75
|
+
notify() {},
|
|
76
|
+
setStatus: (key: string, text?: string) => { statuses.push([key, text]); },
|
|
77
|
+
confirm: async (title: string) => { prompts.push(`confirm:${title}`); return false; },
|
|
78
|
+
select: async (title: string) => { prompts.push(`select:${title}`); return undefined; },
|
|
79
|
+
},
|
|
80
|
+
} as unknown as ExtensionContext;
|
|
81
|
+
return { handlers, entries, nativeAccesses, sweeps, prompts, statuses, emitted, tools, ctx, sessionManager };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function legacySettings(cwd: string): string {
|
|
85
|
+
const path = join(cwd, ".pi", "settings.json");
|
|
86
|
+
mkdirSync(join(cwd, ".pi"), { recursive: true });
|
|
87
|
+
writeFileSync(path, `${JSON.stringify({ subagents: { agentOverrides: { worker: "openai/gpt-5" } } }, null, 2)}\n`);
|
|
88
|
+
return path;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
test("child session_start keeps local resets but skips parent-owned startup work", async (t) => {
|
|
92
|
+
const { cwd, agentHome } = isolate(t);
|
|
93
|
+
const settingsPath = legacySettings(cwd);
|
|
94
|
+
const before = readFileSync(settingsPath, "utf8");
|
|
95
|
+
const h = harness(true, cwd);
|
|
96
|
+
const timings = () => h.entries.filter((entry) => entry.customType === GENTLE_AI_TIMING_ENTRY);
|
|
97
|
+
await h.handlers.get("tool_execution_start")!({ toolName: "gentle_review", toolCallId: "before" }, h.ctx);
|
|
98
|
+
assert.equal(timings().length, 0, "no elapsed-timing ledger exists before session_start");
|
|
99
|
+
await h.handlers.get("session_start")!({ reason: "startup" }, h.ctx);
|
|
100
|
+
await h.handlers.get("tool_execution_start")!({ toolName: "gentle_review", toolCallId: "after" }, h.ctx);
|
|
101
|
+
assert.equal(timings().length, 1, "the child still creates its elapsed-timing ledger");
|
|
102
|
+
assert.deepEqual(readdirSync(agentHome), [], "a child must not install package assets");
|
|
103
|
+
assert.equal(readFileSync(settingsPath, "utf8"), before, "a child must not migrate project model overrides");
|
|
104
|
+
assert.deepEqual(h.sweeps, [], "a child must not sweep candidate views");
|
|
105
|
+
assert.deepEqual(h.nativeAccesses, [], "a child must not negotiate review status or bind repository preparation");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("parent session_start still runs the startup work skipped in children", async (t) => {
|
|
109
|
+
const { cwd, agentHome } = isolate(t);
|
|
110
|
+
const settingsPath = legacySettings(cwd);
|
|
111
|
+
const before = readFileSync(settingsPath, "utf8");
|
|
112
|
+
const h = harness(false, cwd);
|
|
113
|
+
await h.handlers.get("session_start")!({ reason: "startup" }, h.ctx);
|
|
114
|
+
assert.notDeepEqual(readdirSync(agentHome), [], "the parent installs package assets");
|
|
115
|
+
assert.notEqual(readFileSync(settingsPath, "utf8"), before, "the parent migrates legacy project model overrides");
|
|
116
|
+
assert.deepEqual(h.sweeps, [cwd]);
|
|
117
|
+
assert.ok(h.nativeAccesses.length > 0, "the parent negotiates review status");
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("child session_start re-arms the reminder session for its own session manager", async (t) => {
|
|
121
|
+
const { cwd } = isolate(t);
|
|
122
|
+
mkdirSync(join(cwd, "src"), { recursive: true });
|
|
123
|
+
writeFileSync(join(cwd, "src", "a.ts"), "export {};\n");
|
|
124
|
+
const h = harness(true, cwd);
|
|
125
|
+
const next = { ...h.sessionManager, getSessionId: () => "session-child-next" };
|
|
126
|
+
const nextCtx = { ...h.ctx, sessionManager: next } as unknown as ExtensionContext;
|
|
127
|
+
const write = (ctx: ExtensionContext, toolCallId: string) => h.handlers.get("tool_result")!({ toolName: "write", toolCallId, isError: false, input: { path: "src/a.ts" } }, ctx);
|
|
128
|
+
const mutations = () => h.entries.filter((entry) => (entry.data as { kind?: string })?.kind === "mutation");
|
|
129
|
+
await h.handlers.get("session_start")!({ reason: "startup" }, h.ctx);
|
|
130
|
+
await h.handlers.get("session_shutdown")!({ reason: "new" }, h.ctx);
|
|
131
|
+
await write(h.ctx, "after-shutdown");
|
|
132
|
+
assert.equal(mutations().length, 0, "a shut-down session records nothing");
|
|
133
|
+
await h.handlers.get("session_start")!({ reason: "new" }, nextCtx);
|
|
134
|
+
await write(h.ctx, "stale-manager");
|
|
135
|
+
assert.equal(mutations().length, 0, "the reminder manager moved to the new session");
|
|
136
|
+
await write(nextCtx, "current-manager");
|
|
137
|
+
assert.equal(mutations().length, 1, "the restarted child records its own mutation");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("child session_start re-arms YOLO and the review sidebar for its own session", async (t) => {
|
|
141
|
+
const { cwd } = isolate(t);
|
|
142
|
+
const h = harness(true, cwd);
|
|
143
|
+
const next = { ...h.sessionManager, getSessionId: () => "session-child-next" };
|
|
144
|
+
const nextCtx = { ...h.ctx, sessionManager: next } as unknown as ExtensionContext;
|
|
145
|
+
const yoloClears = () => h.statuses.filter(([key, text]) => key === YOLO_STATUS_KEY && text === undefined).length;
|
|
146
|
+
const sidebarSessions = () => h.emitted.filter((event) => event.name === REVIEW_SIDEBAR_EVENT)
|
|
147
|
+
.map((event) => (event.payload as { sessionId: string }).sessionId);
|
|
148
|
+
// Any non-assess call passes through the sidebar wrapper, which publishes
|
|
149
|
+
// before running; the native outcome itself is irrelevant here.
|
|
150
|
+
const capture = async (ctx: ExtensionContext, toolCallId: string) => {
|
|
151
|
+
try { await h.tools.get("gentle_review_capture")!.execute(toolCallId, {}, undefined, undefined, ctx as never); } catch { /* outcome irrelevant */ }
|
|
152
|
+
};
|
|
153
|
+
await h.handlers.get("session_start")!({ reason: "startup" }, h.ctx);
|
|
154
|
+
await h.handlers.get("session_shutdown")!({ reason: "new" }, h.ctx);
|
|
155
|
+
await capture(h.ctx, "after-shutdown");
|
|
156
|
+
assert.deepEqual(sidebarSessions(), [], "a shut-down sidebar publishes nothing");
|
|
157
|
+
const clearsBeforeRestart = yoloClears();
|
|
158
|
+
await h.handlers.get("session_start")!({ reason: "new" }, nextCtx);
|
|
159
|
+
assert.equal(yoloClears(), clearsBeforeRestart + 1, "the child session_start resets YOLO and clears its indicator");
|
|
160
|
+
await capture(h.ctx, "stale-session");
|
|
161
|
+
assert.deepEqual(sidebarSessions(), [], "the sidebar moved to the new session");
|
|
162
|
+
await capture(nextCtx, "current-session");
|
|
163
|
+
assert.ok(sidebarSessions().length > 0, "the restarted child sidebar publishes again");
|
|
164
|
+
assert.ok(sidebarSessions().every((id) => id === "session-child-next"), JSON.stringify(sidebarSessions()));
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
async function writeToolResult(child: boolean, t: TestContext) {
|
|
168
|
+
const { cwd } = isolate(t);
|
|
169
|
+
mkdirSync(join(cwd, "src"), { recursive: true });
|
|
170
|
+
writeFileSync(join(cwd, "src", "a.ts"), "export {};\n");
|
|
171
|
+
const h = harness(child, cwd);
|
|
172
|
+
await h.handlers.get("session_start")!({ reason: "startup" }, h.ctx);
|
|
173
|
+
// Bind an observable preparation on the same session owner the parent
|
|
174
|
+
// session_start would have bound, so only the tool_result guard decides.
|
|
175
|
+
const preparations: string[] = [];
|
|
176
|
+
const unbind = bindSessionRepositoryPreparation(h.sessionManager, cwd, async (root) => { preparations.push(root); return false; }, () => true);
|
|
177
|
+
t.after(unbind);
|
|
178
|
+
await h.handlers.get("tool_result")!({ toolName: "write", toolCallId: "call-1", isError: false, input: { path: "src/a.ts" } }, h.ctx);
|
|
179
|
+
const mutations = h.entries.filter((entry) => (entry.data as { kind?: string })?.kind === "mutation");
|
|
180
|
+
return { cwd, preparations, mutations };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
test("child tool_result records the mutation but never prepares the bound repository", async (t) => {
|
|
184
|
+
const { preparations, mutations } = await writeToolResult(true, t);
|
|
185
|
+
assert.equal(mutations.length, 1, "the child keeps its own mutation receipt");
|
|
186
|
+
assert.deepEqual(preparations, []);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("parent tool_result still prepares the bound repository", async (t) => {
|
|
190
|
+
const { cwd, preparations, mutations } = await writeToolResult(false, t);
|
|
191
|
+
assert.equal(mutations.length, 1);
|
|
192
|
+
assert.deepEqual(preparations, [cwd]);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("child confirm-class guardrail commands ask the parent instead of running or blocking headlessly", async (t) => {
|
|
196
|
+
const { cwd } = isolate(t);
|
|
197
|
+
const h = harness(true, cwd);
|
|
198
|
+
const result = await h.handlers.get("tool_call")!({ toolName: "bash", input: { command: "pi remove some-package" } }, h.ctx) as { block?: boolean; reason?: string } | undefined;
|
|
199
|
+
assert.equal(h.prompts.length, 1, JSON.stringify(h.prompts));
|
|
200
|
+
assert.equal(result?.block, true, "the declined confirmation blocks the command");
|
|
201
|
+
assert.doesNotMatch(result?.reason ?? "", /requires interactive confirmation/);
|
|
202
|
+
});
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { chmodSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { chmodSync, existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { mkdtemp } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
-
import test from "node:test";
|
|
7
|
+
import test, { afterEach, beforeEach } from "node:test";
|
|
8
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
10
10
|
import {
|
|
@@ -18,6 +18,28 @@ import {
|
|
|
18
18
|
// version, and its content digest — the maintainer must never wonder which
|
|
19
19
|
// gentle-ai actually answered.
|
|
20
20
|
|
|
21
|
+
// These tests model a principal session unless a case explicitly selects a child.
|
|
22
|
+
// Never let startup resolve profiles from the launching operator's config home.
|
|
23
|
+
let restoreSessionEnvironment: (() => void) | undefined;
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
const home = await mkdtemp(join(tmpdir(), "gentle-pi-dev-config-home-"));
|
|
26
|
+
const previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
|
|
27
|
+
const previousChild = process.env.GENTLE_PI_AGENTS_CHILD;
|
|
28
|
+
process.env.GENTLE_PI_CONFIG_HOME = home;
|
|
29
|
+
delete process.env.GENTLE_PI_AGENTS_CHILD;
|
|
30
|
+
restoreSessionEnvironment = () => {
|
|
31
|
+
if (previousConfigHome === undefined) delete process.env.GENTLE_PI_CONFIG_HOME;
|
|
32
|
+
else process.env.GENTLE_PI_CONFIG_HOME = previousConfigHome;
|
|
33
|
+
if (previousChild === undefined) delete process.env.GENTLE_PI_AGENTS_CHILD;
|
|
34
|
+
else process.env.GENTLE_PI_AGENTS_CHILD = previousChild;
|
|
35
|
+
rmSync(home, { recursive: true, force: true });
|
|
36
|
+
};
|
|
37
|
+
});
|
|
38
|
+
afterEach(() => {
|
|
39
|
+
restoreSessionEnvironment?.();
|
|
40
|
+
restoreSessionEnvironment = undefined;
|
|
41
|
+
});
|
|
42
|
+
|
|
21
43
|
interface CommandRegistration {
|
|
22
44
|
handler: (args: string, ctx: ExtensionContext) => Promise<void>;
|
|
23
45
|
}
|
|
@@ -58,6 +80,8 @@ async function withDevOverride<T>(callback: (state: { devBinary: string; sha256:
|
|
|
58
80
|
return await callback({ devBinary, sha256, home });
|
|
59
81
|
} finally {
|
|
60
82
|
setGentleAiDevBinaryEnvironmentForTesting(undefined);
|
|
83
|
+
rmSync(home, { recursive: true, force: true });
|
|
84
|
+
rmSync(bin, { recursive: true, force: true });
|
|
61
85
|
}
|
|
62
86
|
}
|
|
63
87
|
|
|
@@ -164,25 +188,109 @@ test("gentle:dev-binary registers, reports, and clears the persistent override",
|
|
|
164
188
|
}
|
|
165
189
|
});
|
|
166
190
|
|
|
167
|
-
|
|
191
|
+
interface SessionStartHarness {
|
|
192
|
+
sessionStart: (event: unknown, ctx: ExtensionContext) => Promise<void>;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function sessionStartHarness(): Promise<SessionStartHarness> {
|
|
196
|
+
const handlers = new Map<string, (event: unknown, ctx: ExtensionContext) => Promise<void>>();
|
|
197
|
+
const pi = {
|
|
198
|
+
on(name: string, handler: (event: unknown, ctx: ExtensionContext) => Promise<void>) {
|
|
199
|
+
handlers.set(name, handler);
|
|
200
|
+
},
|
|
201
|
+
registerCommand() {},
|
|
202
|
+
registerTool() {},
|
|
203
|
+
} as unknown as ExtensionAPI;
|
|
204
|
+
createGentleAiExtension({ nativeReviewCli: null })(pi);
|
|
205
|
+
const sessionStart = handlers.get("session_start");
|
|
206
|
+
assert.equal(typeof sessionStart, "function");
|
|
207
|
+
return { sessionStart: sessionStart! };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// The 🌹 dev-binary card (gentle-shell) owns the startup notice when it can
|
|
211
|
+
// render; the session-start toast is only the fallback for when the card is
|
|
212
|
+
// unavailable (shell disabled, no UI).
|
|
213
|
+
test("session start defers the active-override announcement to the shell card", async () => {
|
|
168
214
|
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
215
|
+
const previousShell = process.env.GENTLE_PI_SHELL;
|
|
169
216
|
process.env.GENTLE_PI_AGENT_HOME = await mkdtemp(join(tmpdir(), "gentle-pi-dev-agent-home-"));
|
|
217
|
+
delete process.env.GENTLE_PI_SHELL;
|
|
170
218
|
try {
|
|
171
219
|
await withDevOverride(async ({ devBinary, sha256 }) => {
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
220
|
+
const { sessionStart } = await sessionStartHarness();
|
|
221
|
+
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-dev-cwd-"));
|
|
222
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
223
|
+
await sessionStart({}, contextFor(cwd, notifications));
|
|
224
|
+
const expected = `Gentle AI dev binary override active (unpinned, field-test only): ${devBinary} 9.9.9-dev+surface sha256:${sha256.slice(0, 16)}`;
|
|
225
|
+
const announcement = notifications.find((entry) => entry.message.includes(expected));
|
|
226
|
+
assert.equal(announcement, undefined, JSON.stringify(notifications));
|
|
227
|
+
});
|
|
228
|
+
} finally {
|
|
229
|
+
if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
|
|
230
|
+
else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
|
|
231
|
+
if (previousShell === undefined) delete process.env.GENTLE_PI_SHELL;
|
|
232
|
+
else process.env.GENTLE_PI_SHELL = previousShell;
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test("a child session keeps exactly one warning fallback even when the shell is enabled", async (t) => {
|
|
237
|
+
const previousShell = process.env.GENTLE_PI_SHELL;
|
|
238
|
+
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
239
|
+
const home = await mkdtemp(join(tmpdir(), "gentle-pi-dev-child-home-"));
|
|
240
|
+
process.env.GENTLE_PI_AGENT_HOME = home;
|
|
241
|
+
process.env.GENTLE_PI_AGENTS_CHILD = "1";
|
|
242
|
+
process.env.GENTLE_PI_SHELL = "on";
|
|
243
|
+
t.after(() => {
|
|
244
|
+
if (previousShell === undefined) delete process.env.GENTLE_PI_SHELL;
|
|
245
|
+
else process.env.GENTLE_PI_SHELL = previousShell;
|
|
246
|
+
if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
|
|
247
|
+
else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
|
|
248
|
+
rmSync(home, { recursive: true, force: true });
|
|
249
|
+
});
|
|
250
|
+
await withDevOverride(async ({ devBinary, sha256 }) => {
|
|
251
|
+
const { sessionStart } = await sessionStartHarness();
|
|
252
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
253
|
+
await sessionStart({}, contextFor(home, notifications));
|
|
254
|
+
const expected = `Gentle AI dev binary override active (unpinned, field-test only): ${devBinary} 9.9.9-dev+surface sha256:${sha256.slice(0, 16)}`;
|
|
255
|
+
assert.equal(notifications.length, 1, JSON.stringify(notifications));
|
|
256
|
+
assert.ok(notifications[0]!.message.includes(expected));
|
|
257
|
+
assert.equal(notifications[0]!.severity, "warning");
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("session start stays silent in headless contexts even with an active override", async () => {
|
|
262
|
+
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
263
|
+
const previousShell = process.env.GENTLE_PI_SHELL;
|
|
264
|
+
process.env.GENTLE_PI_AGENT_HOME = await mkdtemp(join(tmpdir(), "gentle-pi-dev-agent-home-"));
|
|
265
|
+
delete process.env.GENTLE_PI_SHELL;
|
|
266
|
+
try {
|
|
267
|
+
await withDevOverride(async () => {
|
|
268
|
+
const { sessionStart } = await sessionStartHarness();
|
|
269
|
+
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-dev-cwd-"));
|
|
270
|
+
const notifications: Array<{ message: string; severity: string }> = [];
|
|
271
|
+
const headless = { ...contextFor(cwd, notifications), hasUI: false } as unknown as ExtensionContext;
|
|
272
|
+
await sessionStart({}, headless);
|
|
273
|
+
assert.equal(notifications.length, 0, JSON.stringify(notifications));
|
|
274
|
+
});
|
|
275
|
+
} finally {
|
|
276
|
+
if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
|
|
277
|
+
else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
|
|
278
|
+
if (previousShell === undefined) delete process.env.GENTLE_PI_SHELL;
|
|
279
|
+
else process.env.GENTLE_PI_SHELL = previousShell;
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("session start keeps the loud toast as fallback when the shell card is unavailable", async () => {
|
|
284
|
+
const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
|
|
285
|
+
const previousShell = process.env.GENTLE_PI_SHELL;
|
|
286
|
+
process.env.GENTLE_PI_AGENT_HOME = await mkdtemp(join(tmpdir(), "gentle-pi-dev-agent-home-"));
|
|
287
|
+
process.env.GENTLE_PI_SHELL = "off";
|
|
288
|
+
try {
|
|
289
|
+
await withDevOverride(async ({ devBinary, sha256 }) => {
|
|
290
|
+
const { sessionStart } = await sessionStartHarness();
|
|
183
291
|
const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-dev-cwd-"));
|
|
184
292
|
const notifications: Array<{ message: string; severity: string }> = [];
|
|
185
|
-
await sessionStart
|
|
293
|
+
await sessionStart({}, contextFor(cwd, notifications));
|
|
186
294
|
const expected = `Gentle AI dev binary override active (unpinned, field-test only): ${devBinary} 9.9.9-dev+surface sha256:${sha256.slice(0, 16)}`;
|
|
187
295
|
const announcement = notifications.find((entry) => entry.message.includes(expected));
|
|
188
296
|
assert.ok(announcement, JSON.stringify(notifications));
|
|
@@ -191,5 +299,7 @@ test("session start announces the active override once, loudly", async () => {
|
|
|
191
299
|
} finally {
|
|
192
300
|
if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
|
|
193
301
|
else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
|
|
302
|
+
if (previousShell === undefined) delete process.env.GENTLE_PI_SHELL;
|
|
303
|
+
else process.env.GENTLE_PI_SHELL = previousShell;
|
|
194
304
|
}
|
|
195
305
|
});
|