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
|
@@ -13,7 +13,7 @@ function harness(env: NodeJS.ProcessEnv = {}, mode: "tui" | "print" = "tui", shu
|
|
|
13
13
|
let session = "first";
|
|
14
14
|
let finish!: () => void;
|
|
15
15
|
let signal!: AbortSignal;
|
|
16
|
-
const sent: RuntimeMetricBucket[][] = [];
|
|
16
|
+
const sent: (readonly RuntimeMetricBucket[])[] = [];
|
|
17
17
|
const launches: unknown[] = [];
|
|
18
18
|
const ctx: any = { cwd: "/fixture", mode, model,
|
|
19
19
|
sessionManager: { getSessionId: () => session, getEntries: () => assert.fail("no reconstruction") } };
|
|
@@ -2,8 +2,11 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { EventEmitter } from "node:events";
|
|
3
3
|
import { PassThrough } from "node:stream";
|
|
4
4
|
import { test } from "node:test";
|
|
5
|
-
import { readFileSync, realpathSync } from "node:fs";
|
|
5
|
+
import { mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
|
|
6
|
+
import { tmpdir } from "node:os";
|
|
7
|
+
import { join } from "node:path";
|
|
6
8
|
import { setGentleAiDevBinaryEnvironmentForTesting } from "../lib/gentle-ai-binary.ts";
|
|
9
|
+
import { setBundledInstallForTesting } from "../lib/gentle-shell-bundled.ts";
|
|
7
10
|
import * as native from "../lib/runtime-metrics-native.ts";
|
|
8
11
|
import { parseAgentClass, type RuntimeMetricBucket } from "../lib/runtime-metrics.ts";
|
|
9
12
|
import { createHash } from "node:crypto";
|
|
@@ -273,3 +276,30 @@ test("spawn failure silently discards", async () => {
|
|
|
273
276
|
spawn: () => { calls++; throw new Error("private error"); } }), "discarded");
|
|
274
277
|
assert.equal(calls, 1);
|
|
275
278
|
});
|
|
279
|
+
|
|
280
|
+
// S9: this package running from a bundled install (the testing overlay stands
|
|
281
|
+
// in for its location; the detection itself is tested with real installs in
|
|
282
|
+
// gentle-ai-binary-isolation.test.ts) and the launcher's GENTLE_SHELL_PREFIX.
|
|
283
|
+
function bundledIsolation(t: { after: (fn: () => void) => void }): { prefix: string; env: NodeJS.ProcessEnv } {
|
|
284
|
+
const prefix = realpathSync(mkdtempSync(join(tmpdir(), "gentle-ai-isolation-site-")));
|
|
285
|
+
setBundledInstallForTesting({ root: prefix });
|
|
286
|
+
t.after(() => {
|
|
287
|
+
setBundledInstallForTesting(undefined);
|
|
288
|
+
rmSync(prefix, { recursive: true, force: true });
|
|
289
|
+
});
|
|
290
|
+
return { prefix, env: { PATH: "/usr/bin", GENTLE_SHELL_PREFIX: prefix } };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
test("the one-shot send runs gentle-ai with Gentle AI's isolation variables only in a bundled install", async (t) => {
|
|
294
|
+
const { prefix, env } = bundledIsolation(t);
|
|
295
|
+
for (const [given, home] of [[env, join(prefix, "gentle-ai")], [{ PATH: "/usr/bin" }, undefined]] as const) {
|
|
296
|
+
const f = fixture();
|
|
297
|
+
const pending = native.sendNativeRuntimeEvent([], "/fixture", { ...f.deps, env: given });
|
|
298
|
+
const options = f.calls[0][2] as { env: NodeJS.ProcessEnv };
|
|
299
|
+
assert.equal(options.env.GENTLE_AI_HOME, home);
|
|
300
|
+
if (home === undefined) assert.equal(options.env, given, "the environment is passed unchanged");
|
|
301
|
+
else assert.equal(options.env.GENTLE_AI_NO_SELF_UPDATE, "1");
|
|
302
|
+
f.close();
|
|
303
|
+
assert.equal(await pending, "stored");
|
|
304
|
+
}
|
|
305
|
+
});
|
|
@@ -2,11 +2,16 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import test from "node:test";
|
|
3
3
|
import { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { GentlePromptEditor } from "../extensions/gentle-shell.ts";
|
|
5
|
-
import { SelectionEngine } from "../lib/selection-engine.ts";
|
|
5
|
+
import { SelectionEngine, withReverseSpan } from "../lib/selection-engine.ts";
|
|
6
6
|
import { decodePrintableKey } from "../lib/pi-tui-keys.ts";
|
|
7
|
-
import { CURSOR_MARKER, visibleWidth, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
7
|
+
import { CURSOR_MARKER, stripTerminalSequences as stripAnsi, TuiMainScreen, visibleWidth, type TuiMouseEvent } from "@earendil-works/pi-tui";
|
|
8
8
|
import { CARD_STYLE, cardStyle, setCardStyle } from "../lib/shell-card.ts";
|
|
9
|
-
import {
|
|
9
|
+
import { withPromptHint } from "../lib/shell-prompt.ts";
|
|
10
|
+
|
|
11
|
+
function resolveFakeCursors(rows: string[], hardware = false): string[] {
|
|
12
|
+
const host = TuiMainScreen.prototype as unknown as { resolveFakeCursors(rows: string[]): string[] };
|
|
13
|
+
return host.resolveFakeCursors.call({ showHardwareCursor: hardware }, rows);
|
|
14
|
+
}
|
|
10
15
|
|
|
11
16
|
// Native selection engine tests: drive a real CustomEditor through the
|
|
12
17
|
// SelectionEngine the same way GentlePromptEditor wires it — engine.handleInput
|
|
@@ -45,6 +50,30 @@ const click = (x: number, y: number, width: number): TuiMouseEvent => ({
|
|
|
45
50
|
type: "click", button: "left", x, y, width, height: 30, screenX: x, screenY: y, shift: false, alt: false, ctrl: false,
|
|
46
51
|
});
|
|
47
52
|
|
|
53
|
+
test("legacy and APC cursors preserve native selection and empty hints", () => {
|
|
54
|
+
const legacy = `${CURSOR_MARKER}\x1b[7ma\x1b[0mbc`;
|
|
55
|
+
assert.equal(withReverseSpan(legacy, 0, 3), `\x1b[7m${CURSOR_MARKER}\x1b[7ma\x1b[0m\x1b[7mbc\x1b[27m`);
|
|
56
|
+
const apc = `${CURSOR_MARKER}\x1b_pi:fc\x07a\x1b_pi:/fc\x07bc`;
|
|
57
|
+
const selected = withReverseSpan(apc, 0, 3);
|
|
58
|
+
assert.ok(selected.includes(`${CURSOR_MARKER}\x1b_pi:fc\x07`));
|
|
59
|
+
for (const hardware of [false, true]) {
|
|
60
|
+
const resolved = resolveFakeCursors([selected], hardware)[0];
|
|
61
|
+
assert.ok(resolved.includes("\x1b[7mbc\x1b[27m"));
|
|
62
|
+
assert.equal(stripAnsi(resolved), "abc");
|
|
63
|
+
}
|
|
64
|
+
// Outside selection the host still suppresses only the focused fake cursor.
|
|
65
|
+
assert.equal(resolveFakeCursors([apc], true)[0], `${CURSOR_MARKER}abc`);
|
|
66
|
+
assert.ok(resolveFakeCursors([apc.replace(CURSOR_MARKER, "")], true)[0].includes("\x1b[7ma\x1b[27m"));
|
|
67
|
+
for (const cursor of ["\x1b[7m \x1b[0m", "\x1b_pi:fc\x07 \x1b_pi:/fc\x07"]) {
|
|
68
|
+
const row = `${CURSOR_MARKER}${cursor}${" ".repeat(30)}`;
|
|
69
|
+
const hinted = withPromptHint(row, "type here", (_role, text) => text);
|
|
70
|
+
assert.ok(hinted.startsWith(`${CURSOR_MARKER}${cursor}`));
|
|
71
|
+
assert.ok(hinted.includes("type here"));
|
|
72
|
+
assert.equal(visibleWidth(hinted), visibleWidth(row));
|
|
73
|
+
assert.equal(withPromptHint(row + "draft", "type here", (_role, text) => text), row + "draft");
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
48
77
|
test("T2 float prompt native cursor, selection, replacement and mouse share geometry", () => {
|
|
49
78
|
const previous = cardStyle();
|
|
50
79
|
setCardStyle(CARD_STYLE.FLOAT);
|
|
@@ -56,11 +85,12 @@ test("T2 float prompt native cursor, selection, replacement and mouse share geom
|
|
|
56
85
|
let rows = editor.render(80);
|
|
57
86
|
assert.match(rows[0], /\x1b\[35m▎\x1b\[39m/);
|
|
58
87
|
assert.equal(visibleWidth(rows[1].split(CURSOR_MARKER)[0]), 6);
|
|
59
|
-
assert.ok(rows[1].includes(`${CURSOR_MARKER}\
|
|
88
|
+
assert.ok(rows[1].includes(`${CURSOR_MARKER}\x1b_pi:fc\x07d\x1b_pi:/fc\x07`));
|
|
89
|
+
assert.ok(resolveFakeCursors(rows)[1].includes(`${CURSOR_MARKER}\x1b[7md\x1b[27m`));
|
|
60
90
|
editor.handleInput("\x1b[1;2H");
|
|
61
91
|
rows = editor.render(80);
|
|
62
92
|
assert.equal(stripAnsi(rows[1].replaceAll(CURSOR_MARKER, "")).trim(), "▎ abcd");
|
|
63
|
-
assert.ok(rows[1].includes(`${CURSOR_MARKER}\
|
|
93
|
+
assert.ok(rows[1].includes(`${CURSOR_MARKER}\x1b_pi:fc\x07a\x1b_pi:/fc\x07\x1b[7mbc\x1b[27md`), "native selection inversion survives the deferred software cursor reset");
|
|
64
94
|
assert.match(rows[2], /3 chars selected/);
|
|
65
95
|
assert.doesNotMatch(rows[2], /╯/);
|
|
66
96
|
assert.ok(rows[2].includes("\x1b[44m"));
|
|
@@ -168,6 +198,36 @@ test("alt+a selects all; backspace replaces the whole text", () => {
|
|
|
168
198
|
assert.equal(cursorOf(editor).line, 0);
|
|
169
199
|
});
|
|
170
200
|
|
|
201
|
+
test("alt+e selects all; backspace replaces the whole text", () => {
|
|
202
|
+
const editor = makeEditor();
|
|
203
|
+
const engine = new SelectionEngine(editor);
|
|
204
|
+
const native = (d: string) => editor.handleInput(d);
|
|
205
|
+
editor.setText("abc\ndef");
|
|
206
|
+
engine.handleInput("\x1be", native);
|
|
207
|
+
engine.handleInput(BACKSPACE, native);
|
|
208
|
+
assert.equal(editor.getText(), "");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// gentle-shell#1565: a registered extension shortcut owns its chord before
|
|
212
|
+
// any selection key, matching CustomEditor's own precedence.
|
|
213
|
+
test("selection chords yield to a claiming shortcut and keep selecting otherwise", () => {
|
|
214
|
+
const editor = makeEditor();
|
|
215
|
+
const engine = new SelectionEngine(editor);
|
|
216
|
+
const forwarded: string[] = [];
|
|
217
|
+
const native = (d: string) => { forwarded.push(d); editor.handleInput(d); };
|
|
218
|
+
const probed: string[] = [];
|
|
219
|
+
const shortcut = (d: string) => { probed.push(d); return d === ALT_A; };
|
|
220
|
+
editor.setText("abc");
|
|
221
|
+
engine.handleInput(ALT_A, native, shortcut);
|
|
222
|
+
assert.deepEqual(probed, [ALT_A]);
|
|
223
|
+
assert.deepEqual(forwarded, []);
|
|
224
|
+
engine.handleInput(BACKSPACE, native, shortcut);
|
|
225
|
+
assert.equal(editor.getText(), "ab", "a claimed chord must not select");
|
|
226
|
+
engine.handleInput("\x1be", native, shortcut);
|
|
227
|
+
engine.handleInput(BACKSPACE, native, shortcut);
|
|
228
|
+
assert.equal(editor.getText(), "", "an unclaimed chord still selects");
|
|
229
|
+
});
|
|
230
|
+
|
|
171
231
|
test("movement collapses the selection; later delete behaves natively", () => {
|
|
172
232
|
const editor = makeEditor();
|
|
173
233
|
const engine = new SelectionEngine(editor);
|
|
@@ -4,7 +4,7 @@ import { mkdtemp, writeFile, rm, realpath } from "node:fs/promises";
|
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
import { installSessionChangeCapture, publishForeignSessionChange } from "../lib/session-change-capture.ts";
|
|
7
|
-
import { SessionChanges, SESSION_CHANGE_ENTRY
|
|
7
|
+
import { SessionChanges, SESSION_CHANGE_ENTRY } from "../lib/session-changes.ts";
|
|
8
8
|
|
|
9
9
|
async function fixture(run: (f: any) => Promise<void>, child = false) {
|
|
10
10
|
const root = await realpath(await mkdtemp(join(tmpdir(), "change-capture-")));
|
|
@@ -50,8 +50,9 @@ test("successful writes capture only the exact tool target and persist for reloa
|
|
|
50
50
|
assert.equal(entries[0].customType,SESSION_CHANGE_ENTRY);
|
|
51
51
|
await writeFile(join(root,"file"),"later human output\n");
|
|
52
52
|
const changes = new SessionChanges("session",entries);
|
|
53
|
-
assert.match(
|
|
54
|
-
assert.doesNotMatch(
|
|
53
|
+
assert.match(entries[0].data.evidence.after.text,/agent output/);
|
|
54
|
+
assert.doesNotMatch(entries[0].data.evidence.after.text,/later human/);
|
|
55
|
+
assert.equal(changes.model.files.length,1);
|
|
55
56
|
}));
|
|
56
57
|
|
|
57
58
|
test("failed and stale-session tool outcomes never add session changes", async () => fixture(async ({root,fire,entries,switchSession}) => {
|
|
@@ -78,9 +79,9 @@ test("child carries bounded evidence in the existing tool-result details transpo
|
|
|
78
79
|
assert.deepEqual(entries,[]);
|
|
79
80
|
},true));
|
|
80
81
|
|
|
81
|
-
test("
|
|
82
|
-
const events:
|
|
83
|
-
pi.events.on(
|
|
82
|
+
test("capture caps bound session entries without a Changes notice", async () => fixture(async ({root,entries,fire,pi}) => {
|
|
83
|
+
const events: unknown[] = [];
|
|
84
|
+
pi.events.on("gentle-pi:session-change", (data: unknown) => events.push(data));
|
|
84
85
|
for (let i = 0; i < 80; i++) {
|
|
85
86
|
const path = `f${i}`;
|
|
86
87
|
const content = "x".repeat(60 * 1024) + "\n";
|
|
@@ -90,8 +91,7 @@ test("the capture-limit warning is emitted once, not on every later write", asyn
|
|
|
90
91
|
await fire("tool_result",{...event,isError:false});
|
|
91
92
|
await fire("tool_execution_end",{toolCallId:`w${i}`,toolName:"write",isError:false});
|
|
92
93
|
}
|
|
93
|
-
const
|
|
94
|
-
assert.
|
|
95
|
-
assert.
|
|
96
|
-
assert.equal(events.length,80);
|
|
94
|
+
const persisted = entries.filter(entry => entry.customType === SESSION_CHANGE_ENTRY).length;
|
|
95
|
+
assert.ok(persisted > 0 && persisted < 80, `the session byte cap stops persisting evidence (${persisted})`);
|
|
96
|
+
assert.deepEqual(events, [], "Changes reads Git; capture emits no refresh or limit notice");
|
|
97
97
|
}));
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import shell, { shellGitRunner } from "../extensions/gentle-shell.ts";
|
|
8
|
+
import { SESSION_CHANGE_ENTRY } from "../lib/session-changes.ts";
|
|
9
|
+
import { SESSION_WORKTREE_ENTRY } from "../lib/session-worktree-registry.ts";
|
|
5
10
|
|
|
6
|
-
|
|
11
|
+
// /gentle:changes shows the current Git state (working tree vs HEAD, untracked
|
|
12
|
+
// included) of the session's repository and of every worktree its subagents
|
|
13
|
+
// used. Captured write/edit evidence only names roots; Git names the files.
|
|
14
|
+
|
|
15
|
+
type Runner = (root: string) => (args: string[]) => Promise<{ stdout: string; code: number }>;
|
|
16
|
+
function fixture(entries: any[] = [], options: { root?: string; sessionId?: string; gitRunner?: Runner; pollMs?: number } = {}) {
|
|
7
17
|
const handlers = new Map<string, Function[]>(), listeners = new Map<string, Function[]>();
|
|
8
18
|
const commands = new Map<string, any>(), widgets = new Map<string, any>();
|
|
9
19
|
let gitCalls = 0;
|
|
@@ -11,55 +21,239 @@ function fixture(entries: any[] = []) {
|
|
|
11
21
|
events: { on: (key, fn) => { listeners.set(key,[...(listeners.get(key) ?? []), fn]); return () => {}; }, emit: (key,data) => listeners.get(key)?.forEach(fn=>fn(data)) },
|
|
12
22
|
appendEntry: (customType,data) => entries.push({type:"custom",customType,data}), registerTool() {}, registerShortcut() {}, registerMessageRenderer() {},
|
|
13
23
|
registerCommand: (key, registration) => commands.set(key,registration) };
|
|
14
|
-
const notices: string[] = [];
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
24
|
+
const notices: string[] = [], levels: string[] = [];
|
|
25
|
+
const root = options.root ?? "/repo";
|
|
26
|
+
const sessionId = options.sessionId ?? "session";
|
|
27
|
+
const overlay: { view?: any; close?: () => void } = {};
|
|
28
|
+
const ctx: any = { hasUI:true, cwd:root, sessionManager:{getSessionId:()=>sessionId,getEntries:()=>entries},
|
|
29
|
+
ui: { setFooter() {}, getEditorComponent:()=>({}), setWorkingVisible() {}, setWidget:(key,value)=>value===undefined?widgets.delete(key):widgets.set(key,value), notify:(text,level)=>{ notices.push(text); levels.push(level); },
|
|
30
|
+
custom:(factory:any)=>new Promise(resolve=>{ overlay.close=()=>resolve(null); overlay.view=factory({terminal:{rows:40,columns:200},requestRender(){}},{fg:(_c:string,t:string)=>t,bold:(t:string)=>t},{},resolve); }) } };
|
|
31
|
+
shell(pi,{GENTLE_PI_SHELL_CHANGES_POLL_MS:String(options.pollMs ?? 60_000)}, {resolveWorktree:()=>({root,commonDir:root+"/.git"}),devBinary:()=>undefined,
|
|
32
|
+
gitRunner:(cwd)=>async(args)=>{gitCalls++; return options.gitRunner ? options.gitRunner(cwd)(args) : await new Promise<any>(()=>{});} });
|
|
19
33
|
const fire=async(key,event={})=>{for(const fn of handlers.get(key)??[]) await fn(event,ctx);};
|
|
20
|
-
|
|
34
|
+
// Opens the panel and closes it again: its groups (root, branch, files)
|
|
35
|
+
// and the rendered group rows.
|
|
36
|
+
const panel=async()=>{
|
|
37
|
+
overlay.view=undefined;
|
|
38
|
+
const opened=commands.get("gentle:changes").handler("",ctx);
|
|
39
|
+
for(let i=0;i<200&&!overlay.view;i++){ if(await Promise.race([opened.then(()=>true),new Promise(r=>setTimeout(()=>r(false),5))])) break; }
|
|
40
|
+
if(!overlay.view) return undefined;
|
|
41
|
+
const text=overlay.view.render(200).join("\n");
|
|
42
|
+
const trees=(overlay.view.trees as any[]).map(tree=>({root:tree.root as string,branch:tree.branch as string|undefined,files:tree.model.files.map((file:any)=>file.path) as string[],added:tree.model.added as number,deleted:tree.model.deleted as number}));
|
|
43
|
+
overlay.close!(); await opened;
|
|
44
|
+
return {text,trees,files:trees.flatMap(tree=>tree.files)};
|
|
45
|
+
};
|
|
46
|
+
return {pi,ctx,entries,notices,levels,commands,widgets,fire,panel,overlay,gitCalls:()=>gitCalls};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function tempRepo(t: { after(fn: () => void): void }, name = "gentle-changes-") {
|
|
50
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), name)));
|
|
51
|
+
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
52
|
+
const git = (...args: string[]) => execFileSync("git", ["-C", root, "-c", "user.name=test", "-c", "user.email=test@example.com", "-c", "commit.gpgsign=false", ...args], { encoding: "utf8" });
|
|
53
|
+
git("init", "-q", "--initial-branch=main");
|
|
54
|
+
writeFileSync(join(root, "file.ts"), "base\n");
|
|
55
|
+
git("add", "file.ts"); git("commit", "-q", "-m", "base");
|
|
56
|
+
return { root, git };
|
|
21
57
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
58
|
+
const sessionRoot = (root: string, evidence = "session:cwd", sessionId = "session") => ({ type: "custom", customType: SESSION_WORKTREE_ENTRY, data: { sessionId, root, evidence } });
|
|
59
|
+
const captured = (root: string, id: string, path = "file.ts", sessionId = "session") => ({ type: "custom", customType: SESSION_CHANGE_ENTRY,
|
|
60
|
+
data: { sessionId, evidence: { id, root, path, before: { kind: "text", text: "base\n" }, after: { kind: "text", text: "agent\n" } } } });
|
|
61
|
+
const settle = async () => { for (let i = 0; i < 40; i++) await new Promise(resolve => setTimeout(resolve, 5)); };
|
|
62
|
+
const real: Runner = (cwd) => shellGitRunner(cwd);
|
|
63
|
+
|
|
64
|
+
test("Gentle Shell startup never waits for a repository scan", async () => {
|
|
65
|
+
const f = fixture([sessionRoot("/repo"), captured("/repo", "a")]);
|
|
66
|
+
await Promise.race([f.fire("session_start"), new Promise((_, reject) => setTimeout(() => reject(new Error("startup blocked by Git inventory")), 100))]);
|
|
67
|
+
assert.equal(f.gitCalls(), 0, "session_start never runs Git");
|
|
68
|
+
assert.equal(f.widgets.has("gentle-shell-changes"), false, "the list fills only after a turn end or panel open");
|
|
28
69
|
await f.fire("session_shutdown");
|
|
29
70
|
});
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
71
|
+
|
|
72
|
+
test("the orchestrator repository's dirty file is listed, and leaves once committed", async (t) => {
|
|
73
|
+
const repo = tempRepo(t);
|
|
74
|
+
writeFileSync(join(repo.root, "file.ts"), "human or agent, Git decides\n");
|
|
75
|
+
writeFileSync(join(repo.root, "new.ts"), "one\ntwo\n");
|
|
76
|
+
const f = fixture([], { root: repo.root, gitRunner: real });
|
|
77
|
+
await f.fire("session_start");
|
|
78
|
+
assert.equal(f.gitCalls(), 0);
|
|
79
|
+
await f.fire("agent_end"); await settle();
|
|
80
|
+
assert.equal(f.widgets.has("gentle-shell-changes"), true, "uncaptured working-tree changes are listed");
|
|
81
|
+
const panel = await f.panel();
|
|
82
|
+
assert.deepEqual(panel!.trees.map(tree => [tree.root, tree.files, tree.added, tree.deleted]), [[repo.root, ["file.ts", "new.ts"], 3, 1]], "untracked files count their lines");
|
|
83
|
+
repo.git("add", "-A"); repo.git("commit", "-q", "-m", "work");
|
|
84
|
+
await f.fire("agent_end"); await settle();
|
|
85
|
+
assert.equal(f.widgets.has("gentle-shell-changes"), false, "committed work leaves the widget and the counter");
|
|
86
|
+
assert.equal(await f.panel(), undefined);
|
|
87
|
+
assert.match(f.notices.join("\n"), /No uncommitted changes/);
|
|
37
88
|
await f.fire("session_shutdown");
|
|
38
89
|
});
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
90
|
+
|
|
91
|
+
test("a subagent worktree is its own group and disappears once the worktree is removed", async (t) => {
|
|
92
|
+
const repo = tempRepo(t);
|
|
93
|
+
const linked = join(realpathSync(mkdtempSync(join(tmpdir(), "gentle-changes-wt-"))), "linked");
|
|
94
|
+
t.after(() => rmSync(join(linked, ".."), { recursive: true, force: true }));
|
|
95
|
+
repo.git("worktree", "add", "-q", "-b", "agent-branch", linked);
|
|
96
|
+
writeFileSync(join(repo.root, "file.ts"), "orchestrator\n");
|
|
97
|
+
writeFileSync(join(linked, "worker.ts"), "worker\n");
|
|
98
|
+
const f = fixture([sessionRoot(repo.root), sessionRoot(linked, "subagent:spawn")], { root: repo.root, gitRunner: real });
|
|
99
|
+
await f.fire("session_start");
|
|
100
|
+
const panel = await f.panel();
|
|
101
|
+
assert.deepEqual(panel!.trees.map(tree => [tree.root, tree.branch, tree.files]).sort(), [[linked, "agent-branch", ["worker.ts"]], [repo.root, "main", ["file.ts"]]].sort(), "one group per root, each with its own branch");
|
|
102
|
+
assert.match(panel!.text, /agent-branch · linked/);
|
|
103
|
+
repo.git("worktree", "remove", "--force", linked);
|
|
104
|
+
await f.fire("agent_end"); await settle();
|
|
105
|
+
const after = await f.panel();
|
|
106
|
+
assert.deepEqual(after!.trees.map(tree => tree.root), [repo.root], "a removed worktree leaves without error");
|
|
107
|
+
assert.doesNotMatch(f.notices.join("\n"), /Git status failed|Could not read/, "a removed worktree is not a failed scan");
|
|
108
|
+
await f.fire("session_shutdown");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("captured evidence only names a subagent's independent repository; Git names its files", async (t) => {
|
|
112
|
+
const repo = tempRepo(t);
|
|
113
|
+
const foreign = tempRepo(t, "gentle-changes-foreign-");
|
|
114
|
+
writeFileSync(join(foreign.root, "real.ts"), "dirty\n");
|
|
115
|
+
// The captured path is clean in Git, so it is not listed.
|
|
116
|
+
const f = fixture([sessionRoot(repo.root), captured(foreign.root, "child:1", "file.ts")], { root: repo.root, gitRunner: real });
|
|
117
|
+
await f.fire("session_start");
|
|
118
|
+
const panel = await f.panel();
|
|
119
|
+
assert.deepEqual(panel!.trees.map(tree => [tree.root, tree.files]), [[foreign.root, ["real.ts"]]]);
|
|
120
|
+
await f.fire("session_shutdown");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("reload and new sessions read the same state from Git, without stale entries", async (t) => {
|
|
124
|
+
const repo = tempRepo(t);
|
|
125
|
+
const linked = join(realpathSync(mkdtempSync(join(tmpdir(), "gentle-changes-wt-"))), "linked");
|
|
126
|
+
t.after(() => rmSync(join(linked, ".."), { recursive: true, force: true }));
|
|
127
|
+
repo.git("worktree", "add", "-q", linked);
|
|
128
|
+
writeFileSync(join(repo.root, "file.ts"), "agent\n");
|
|
129
|
+
writeFileSync(join(linked, "file.ts"), "worker\n");
|
|
130
|
+
const entries = [sessionRoot(repo.root), sessionRoot(linked, "subagent:spawn"), captured(repo.root, "a"), captured(repo.root, "b", "gone.ts")];
|
|
131
|
+
repo.git("commit", "-q", "-am", "orchestrator work");
|
|
132
|
+
const reloaded = fixture(entries, { root: repo.root, gitRunner: real });
|
|
133
|
+
await reloaded.fire("session_start");
|
|
134
|
+
assert.equal(reloaded.gitCalls(), 0);
|
|
135
|
+
const panel = await reloaded.panel();
|
|
136
|
+
assert.deepEqual(panel!.trees.map(tree => [tree.root, tree.files]), [[linked, ["file.ts"]]], "committed and captured-only files are not listed after reload");
|
|
137
|
+
await reloaded.fire("session_shutdown");
|
|
138
|
+
const next = fixture([...entries], { root: repo.root, sessionId: "new-session", gitRunner: real });
|
|
139
|
+
await next.fire("session_start");
|
|
140
|
+
assert.equal(await next.panel(), undefined, "another session's worktrees are not inherited");
|
|
141
|
+
assert.match(next.notices.join("\n"), /No uncommitted changes/);
|
|
142
|
+
await next.fire("session_shutdown");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("a failing Git root is omitted while the other roots stay listed", async (t) => {
|
|
146
|
+
const repo = tempRepo(t);
|
|
147
|
+
const broken = tempRepo(t, "gentle-changes-broken-");
|
|
148
|
+
writeFileSync(join(repo.root, "file.ts"), "agent\n");
|
|
149
|
+
writeFileSync(join(broken.root, "file.ts"), "agent\n");
|
|
150
|
+
const failing: Runner = (cwd) => cwd === broken.root ? async () => { throw new Error("git timed out"); } : shellGitRunner(cwd);
|
|
151
|
+
const f = fixture([sessionRoot(repo.root), sessionRoot(broken.root, "explicit")], { root: repo.root, gitRunner: failing });
|
|
152
|
+
await f.fire("session_start");
|
|
153
|
+
await f.fire("agent_end"); await settle();
|
|
154
|
+
const panel = await f.panel();
|
|
155
|
+
assert.deepEqual(panel!.trees.map(tree => [tree.root, tree.files]), [[repo.root, ["file.ts"]]]);
|
|
156
|
+
assert.match(f.notices.join("\n"), /Git status failed for 1 root\b/, "the panel reports the failed root");
|
|
157
|
+
await f.fire("session_shutdown");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// A root whose `git status` exits nonzero is a failed scan, never a clean
|
|
161
|
+
// root: when it is the only root the panel must not claim a clean tree.
|
|
162
|
+
const statusFailsAt = (broken: string): Runner => (cwd) => cwd === broken ? async (args) => args[0] === "status" ? { stdout: "", code: 128 } : shellGitRunner(cwd)(args) : shellGitRunner(cwd);
|
|
163
|
+
|
|
164
|
+
test("when every scanned root fails, Changes reports an error instead of a clean tree", async (t) => {
|
|
165
|
+
const repo = tempRepo(t);
|
|
166
|
+
writeFileSync(join(repo.root, "file.ts"), "agent\n");
|
|
167
|
+
const f = fixture([sessionRoot(repo.root)], { root: repo.root, gitRunner: statusFailsAt(repo.root) });
|
|
168
|
+
await f.fire("session_start");
|
|
169
|
+
assert.equal(await f.panel(), undefined);
|
|
170
|
+
assert.doesNotMatch(f.notices.join("\n"), /No uncommitted changes/);
|
|
171
|
+
assert.match(f.notices.join("\n"), /Could not read Git status for 1 root\b/);
|
|
172
|
+
assert.equal(f.levels.at(-1), "error");
|
|
173
|
+
await f.fire("session_shutdown");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("a clean root next to a failed one reports the failure count, not a plain clean tree", async (t) => {
|
|
177
|
+
const repo = tempRepo(t);
|
|
178
|
+
const broken = tempRepo(t, "gentle-changes-broken-");
|
|
179
|
+
const f = fixture([sessionRoot(repo.root), sessionRoot(broken.root, "explicit")], { root: repo.root, gitRunner: statusFailsAt(broken.root) });
|
|
180
|
+
await f.fire("session_start");
|
|
181
|
+
assert.equal(await f.panel(), undefined);
|
|
182
|
+
assert.match(f.notices.join("\n"), /Git status failed for 1 of 2 roots/);
|
|
183
|
+
assert.equal(f.levels.at(-1), "warning");
|
|
184
|
+
await f.fire("session_shutdown");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("a dirty root next to a failed one opens the panel and reports the failure count", async (t) => {
|
|
188
|
+
const repo = tempRepo(t);
|
|
189
|
+
const broken = tempRepo(t, "gentle-changes-broken-");
|
|
190
|
+
writeFileSync(join(repo.root, "file.ts"), "agent\n");
|
|
191
|
+
writeFileSync(join(broken.root, "file.ts"), "agent\n");
|
|
192
|
+
const f = fixture([sessionRoot(repo.root), sessionRoot(broken.root, "explicit")], { root: repo.root, gitRunner: statusFailsAt(broken.root) });
|
|
193
|
+
await f.fire("session_start");
|
|
194
|
+
const panel = await f.panel();
|
|
195
|
+
assert.deepEqual(panel!.trees.map(tree => [tree.root, tree.files]), [[repo.root, ["file.ts"]]]);
|
|
196
|
+
assert.match(f.notices.join("\n"), /Git status failed for 1 root\b/);
|
|
197
|
+
await f.fire("session_shutdown");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("every changed file is listed without a capture limit", async (t) => {
|
|
201
|
+
const repo = tempRepo(t);
|
|
202
|
+
mkdirSync(join(repo.root, "many"));
|
|
203
|
+
for (let i = 0; i < 300; i++) writeFileSync(join(repo.root, "many", `${i}.ts`), "x\n");
|
|
204
|
+
const f = fixture([], { root: repo.root, gitRunner: real });
|
|
205
|
+
await f.fire("session_start");
|
|
206
|
+
const panel = await f.panel();
|
|
207
|
+
assert.equal(panel!.files.length, 300);
|
|
208
|
+
assert.equal(panel!.trees[0].added, 300);
|
|
209
|
+
assert.equal(f.widgets.has("gentle-shell-changes"), true);
|
|
210
|
+
assert.doesNotMatch(panel!.text, /limit/);
|
|
211
|
+
assert.doesNotMatch(f.notices.join("\n"), /limit/);
|
|
212
|
+
await f.fire("session_shutdown");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("the open panel diffs the selected file against HEAD and reloads a same-count edit", async (t) => {
|
|
216
|
+
const repo = tempRepo(t);
|
|
217
|
+
writeFileSync(join(repo.root, "file.ts"), "first\n");
|
|
218
|
+
writeFileSync(join(repo.root, "new.ts"), "hello\n");
|
|
219
|
+
const f = fixture([], { root: repo.root, gitRunner: real, pollMs: 20 });
|
|
220
|
+
await f.fire("session_start");
|
|
221
|
+
const opened = f.commands.get("gentle:changes").handler("", f.ctx);
|
|
222
|
+
for (let i = 0; i < 200 && !f.overlay.view; i++) await new Promise(resolve => setTimeout(resolve, 5));
|
|
223
|
+
const view = f.overlay.view;
|
|
224
|
+
const shows = async (pattern: RegExp) => {
|
|
225
|
+
for (let i = 0; i < 100 && !pattern.test(view.render(200).join("\n")); i++) await new Promise(resolve => setTimeout(resolve, 10));
|
|
226
|
+
return view.render(200).join("\n");
|
|
227
|
+
};
|
|
228
|
+
try {
|
|
229
|
+
view.handleInput("\r");
|
|
230
|
+
view.handleInput("j");
|
|
231
|
+
assert.match(await shows(/\+first/), /-base/, "tracked files diff against HEAD");
|
|
232
|
+
view.handleInput("j");
|
|
233
|
+
assert.match(await shows(/\+hello/), /\+hello/, "untracked files show their full content");
|
|
234
|
+
view.handleInput("k");
|
|
235
|
+
await new Promise(resolve => setTimeout(resolve, 20));
|
|
236
|
+
writeFileSync(join(repo.root, "file.ts"), "secnd\n");
|
|
237
|
+
assert.match(await shows(/\+secnd/), /\+secnd/, "the poll reloads a same-count edit");
|
|
238
|
+
} finally { f.overlay.close!(); await opened; await f.fire("session_shutdown"); }
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// A repository rooted at HOME (or a broad user folder) makes `git status` scan
|
|
242
|
+
// the user's whole tree: Changes never scans such a root, at turn end or in
|
|
243
|
+
// the panel, matching the startup warning that tells the user to move.
|
|
244
|
+
test("a repository rooted at HOME is never scanned by Changes", async (t) => {
|
|
245
|
+
const repo = tempRepo(t, "gentle-changes-home-");
|
|
246
|
+
writeFileSync(join(repo.root, "file.ts"), "dirty at home\n");
|
|
247
|
+
const previousHome = process.env.HOME;
|
|
248
|
+
process.env.HOME = repo.root;
|
|
249
|
+
t.after(() => { if (previousHome === undefined) delete process.env.HOME; else process.env.HOME = previousHome; });
|
|
250
|
+
const f = fixture([sessionRoot(repo.root)], { root: repo.root, gitRunner: real });
|
|
251
|
+
await f.fire("session_start");
|
|
252
|
+
await f.fire("agent_end"); await settle();
|
|
253
|
+
assert.equal(f.gitCalls(), 0, "no Git runs against a home-rooted repository");
|
|
254
|
+
assert.equal(f.widgets.has("gentle-shell-changes"), false);
|
|
255
|
+
assert.equal(await f.panel(), undefined);
|
|
256
|
+
assert.equal(f.gitCalls(), 0);
|
|
257
|
+
assert.doesNotMatch(f.notices.join("\n"), /Git status failed|Could not read/, "a filtered home root is not a failed scan");
|
|
258
|
+
await f.fire("session_shutdown");
|
|
65
259
|
});
|
|
@@ -3,15 +3,16 @@ import test from "node:test";
|
|
|
3
3
|
import { mkdtemp, writeFile, rm } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import { SessionChanges, readChangeSnapshot, SESSION_CHANGE_ENTRY, MAX_CHANGE_BYTES } from "../lib/session-changes.ts";
|
|
6
|
+
import { SessionChanges, readChangeSnapshot, sessionChangeRoots, SESSION_CHANGE_ENTRY, MAX_CHANGE_BYTES } from "../lib/session-changes.ts";
|
|
7
|
+
import { SESSION_WORKTREE_ENTRY } from "../lib/session-worktree-registry.ts";
|
|
7
8
|
|
|
8
9
|
const text = (text: string) => ({ kind: "text" as const, text });
|
|
9
10
|
const absent = { kind: "absent" as const };
|
|
10
11
|
const evidence = (id = "a", root = "/repo", path = "file.ts", before = text("old\n"), after = text("new\n")) => ({ id, root, path, before, after });
|
|
11
12
|
|
|
12
|
-
test("session changes starts empty without reading the worktree or its untracked contents",
|
|
13
|
+
test("session changes starts empty without reading the worktree or its untracked contents", () => {
|
|
13
14
|
const changes = new SessionChanges("session");
|
|
14
|
-
assert.deepEqual(
|
|
15
|
+
assert.deepEqual(changes.model, { files: [], added: 0, deleted: 0 });
|
|
15
16
|
assert.deepEqual(changes.worktrees, []);
|
|
16
17
|
});
|
|
17
18
|
|
|
@@ -20,9 +21,7 @@ test("only attributed changes appear, against the pre-tool content rather than H
|
|
|
20
21
|
changes.record(evidence("a", "/repo", "file.ts", text("preexisting human edit\nold\n"), text("preexisting human edit\nnew\n")));
|
|
21
22
|
assert.equal(changes.model.files.length, 1);
|
|
22
23
|
assert.equal(changes.model.added, 1);
|
|
23
|
-
assert.equal(changes.model.deleted, 1);
|
|
24
|
-
assert.match(changes.loadDiff("/repo", changes.model.files[0]), /\+new/);
|
|
25
|
-
assert.doesNotMatch(changes.loadDiff("/repo", changes.model.files[0]), /\+preexisting/);
|
|
24
|
+
assert.equal(changes.model.deleted, 1, "the preexisting line is not counted");
|
|
26
25
|
});
|
|
27
26
|
|
|
28
27
|
test("diff counts include content lines that resemble patch headers", () => {
|
|
@@ -37,7 +36,7 @@ test("contiguous own edits combine and own reverts remove the file and worktree"
|
|
|
37
36
|
changes.record(evidence());
|
|
38
37
|
changes.record(evidence("b", "/repo", "file.ts", text("new\n"), text("final\n")));
|
|
39
38
|
assert.equal(changes.model.added, 1);
|
|
40
|
-
assert.
|
|
39
|
+
assert.equal(changes.model.deleted, 1);
|
|
41
40
|
changes.record(evidence("c", "/repo", "file.ts", text("final\n"), text("old\n")));
|
|
42
41
|
assert.deepEqual(changes.worktrees, []);
|
|
43
42
|
});
|
|
@@ -48,7 +47,7 @@ test("external edits between agent operations are not folded into an attributed
|
|
|
48
47
|
changes.record(evidence("b", "/repo", "file.ts", text("external\n"), text("agent again\n")));
|
|
49
48
|
const file = changes.model.files[0];
|
|
50
49
|
assert.match(file.countsUnavailable!, /external|continuity/i);
|
|
51
|
-
assert.
|
|
50
|
+
assert.equal(file.added, 0);
|
|
52
51
|
});
|
|
53
52
|
|
|
54
53
|
test("new files and child worktrees are admitted only by positive mutation evidence", () => {
|
|
@@ -85,30 +84,24 @@ test("bounded captures mark partial counts, including the narrow widget", async
|
|
|
85
84
|
assert.match(renderChangesWidget(changes.model,{fg:(_color,text)=>text},75).join("\n"),/partial counts/);
|
|
86
85
|
});
|
|
87
86
|
|
|
88
|
-
test("
|
|
87
|
+
test("malformed evidence is rejected", () => {
|
|
89
88
|
const changes = new SessionChanges("session");
|
|
90
|
-
changes.record(evidence());
|
|
91
|
-
const first = changes.model.files[0].diffRevision;
|
|
92
|
-
changes.record(evidence("b","/repo","file.ts",text("new\n"),text("other\n")));
|
|
93
|
-
assert.notEqual(changes.model.files[0].diffRevision,first);
|
|
94
89
|
assert.equal(changes.record({...evidence("bad"),path:"../escape"}),false);
|
|
95
90
|
assert.equal(changes.record({...evidence("bad"),path:".git/config"}),false);
|
|
96
91
|
});
|
|
97
92
|
|
|
98
|
-
test("capture capacity
|
|
93
|
+
test("capture capacity bounds recorded evidence", () => {
|
|
99
94
|
const changes = new SessionChanges("session");
|
|
100
95
|
for(let i=0;i<257;i++) changes.record(evidence(String(i),"/repo",String(i)+".ts"));
|
|
101
96
|
assert.equal(changes.model.files.length,256);
|
|
102
|
-
assert.
|
|
97
|
+
assert.equal(changes.record(evidence("late","/repo","late.ts")),false);
|
|
103
98
|
});
|
|
104
99
|
|
|
105
|
-
test("
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
assert.
|
|
111
|
-
assert.
|
|
112
|
-
assert.equal(new SessionChanges("s").takeNotice(),undefined);
|
|
113
|
-
assert.equal(new SessionChanges("s").model.notice,undefined);
|
|
100
|
+
test("session roots come from this session's worktree registrations and captured evidence, never files", () => {
|
|
101
|
+
const registered = (sessionId: string, root: string) => ({ type: "custom", customType: SESSION_WORKTREE_ENTRY, data: { sessionId, root, evidence: "subagent:spawn" } });
|
|
102
|
+
const captured = (sessionId: string, root: string) => ({ type: "custom", customType: SESSION_CHANGE_ENTRY, data: { sessionId, evidence: evidence(root, root) } });
|
|
103
|
+
const entries = [registered("session", "/repo"), registered("session", "/linked"), captured("session", "/foreign"), captured("session", "/repo"),
|
|
104
|
+
registered("other", "/elsewhere"), captured("other", "/theirs"), registered("session", "relative"), { type: "custom", customType: SESSION_CHANGE_ENTRY, data: { sessionId: "session", evidence: { root: "/bad" } } }];
|
|
105
|
+
assert.deepEqual(sessionChangeRoots("session", entries), ["/repo", "/linked", "/foreign"]);
|
|
106
|
+
assert.deepEqual(sessionChangeRoots("missing", entries), []);
|
|
114
107
|
});
|