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,162 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { resolveSessionWorktree, type WorktreeResolver } from "./session-worktree-registry.ts";
|
|
4
|
+
|
|
5
|
+
// gentle-shell#1731: concurrent bounded writers are admitted only when their
|
|
6
|
+
// `## Allowed edit surfaces` are disjoint. Overlap is deliberately conservative:
|
|
7
|
+
// an entry covers every path it matches and everything under such a path, and
|
|
8
|
+
// any entry or segment this module cannot reason about counts as overlapping.
|
|
9
|
+
// Like minimatch, `**` is a globstar only as a whole segment; inside a segment
|
|
10
|
+
// (`src/**.ts`) it matches within that one segment, like `*`.
|
|
11
|
+
|
|
12
|
+
const SPECIAL = /[*?{}]/;
|
|
13
|
+
const MAX_LISTED_PAIRS = 5;
|
|
14
|
+
|
|
15
|
+
// Repository-relative, slash-separated and lowercased (case-insensitive
|
|
16
|
+
// filesystems make `A.ts` and `a.ts` one file). Undefined means unscopable.
|
|
17
|
+
export function normalizeSurfaceEntry(entry: string): string | undefined {
|
|
18
|
+
let value = entry.trim().replace(/^(?:[-*+]|\d+[.)]) +/, "");
|
|
19
|
+
const quoted = value.match(/^`([^`]*)`$/);
|
|
20
|
+
if (quoted) value = quoted[1];
|
|
21
|
+
// Backslashes are kept: an escape and a separator are indistinguishable, so
|
|
22
|
+
// the overlap check treats such an entry as uninterpretable.
|
|
23
|
+
value = value.normalize("NFC");
|
|
24
|
+
if (!value || /^(?:\/|~|[A-Za-z]:)/.test(value)) return undefined;
|
|
25
|
+
const segments = value.split("/").filter(segment => segment !== "" && segment !== ".");
|
|
26
|
+
if (!segments.length || segments.includes("..")) return undefined;
|
|
27
|
+
return segments.join("/").toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Syntax this module never interprets, so the entry overlaps everything:
|
|
31
|
+
// bracket classes (negation and POSIX classes do not survive lowercasing),
|
|
32
|
+
// extglob groups, and braces that are nested, unbalanced, or span a `/` (a
|
|
33
|
+
// spanning group shows up as an unbalanced segment after the split).
|
|
34
|
+
function uninterpretable(entry: string): boolean {
|
|
35
|
+
if (/[[\]()\\]/.test(entry)) return true;
|
|
36
|
+
return entry.split("/").some(segment => {
|
|
37
|
+
let depth = 0;
|
|
38
|
+
for (const char of segment) {
|
|
39
|
+
if (char === "{" && ++depth > 1) return true;
|
|
40
|
+
if (char === "}" && --depth < 0) return true;
|
|
41
|
+
}
|
|
42
|
+
return depth !== 0;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Exact glob semantics for one segment of an interpretable entry: `*`, `?` and
|
|
47
|
+
// one level of balanced braces.
|
|
48
|
+
function segmentRegExp(pattern: string): RegExp {
|
|
49
|
+
let source = "";
|
|
50
|
+
let braces = false;
|
|
51
|
+
for (const char of pattern) {
|
|
52
|
+
if (char === "*") source += "[^/]*";
|
|
53
|
+
else if (char === "?") source += "[^/]";
|
|
54
|
+
else if (char === "{") { braces = true; source += "(?:"; }
|
|
55
|
+
else if (char === "}") { braces = false; source += ")"; }
|
|
56
|
+
else if (char === "," && braces) source += "|";
|
|
57
|
+
else source += char.replace(/[.+^$|\\/()[\]{}]/g, "\\$&");
|
|
58
|
+
}
|
|
59
|
+
return new RegExp(`^${source}$`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Every match of a glob starts with the literal text before its first special
|
|
63
|
+
// character and ends with the literal text after its last one.
|
|
64
|
+
function affixesCompatible(a: string, b: string): boolean {
|
|
65
|
+
const prefix = (value: string) => { const index = value.search(SPECIAL); return index < 0 ? value : value.slice(0, index); };
|
|
66
|
+
const suffix = (value: string) => { let index = value.length - 1; while (index >= 0 && !SPECIAL.test(value[index])) index -= 1; return value.slice(index + 1); };
|
|
67
|
+
const [pa, pb, sa, sb] = [prefix(a), prefix(b), suffix(a), suffix(b)];
|
|
68
|
+
return (pa.startsWith(pb) || pb.startsWith(pa)) && (sa.endsWith(sb) || sb.endsWith(sa));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function segmentsCompatible(a: string, b: string): boolean {
|
|
72
|
+
const aGlob = SPECIAL.test(a);
|
|
73
|
+
const bGlob = SPECIAL.test(b);
|
|
74
|
+
if (!aGlob && !bGlob) return a === b;
|
|
75
|
+
// A non-ASCII character may span two UTF-16 units that one `?` cannot match.
|
|
76
|
+
if (/[^\x00-\x7f]/.test(a + b)) return true;
|
|
77
|
+
if (!aGlob || !bGlob) {
|
|
78
|
+
const [literal, pattern] = aGlob ? [b, a] : [a, b];
|
|
79
|
+
return segmentRegExp(pattern).test(literal);
|
|
80
|
+
}
|
|
81
|
+
return affixesCompatible(a, b);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// True when some path could be covered by both entries. Walking segment by
|
|
85
|
+
// segment: a `**` can absorb the rest of the other entry, and an entry that
|
|
86
|
+
// runs out first names a directory covering whatever the other continues with.
|
|
87
|
+
export function surfacesOverlap(a: string, b: string): boolean {
|
|
88
|
+
const left = normalizeSurfaceEntry(a);
|
|
89
|
+
const right = normalizeSurfaceEntry(b);
|
|
90
|
+
if (left === undefined || right === undefined || uninterpretable(left) || uninterpretable(right)) return true;
|
|
91
|
+
const as = left.split("/");
|
|
92
|
+
const bs = right.split("/");
|
|
93
|
+
for (let index = 0; index < Math.min(as.length, bs.length); index++) {
|
|
94
|
+
if (as[index] === "**" || bs[index] === "**") return true;
|
|
95
|
+
if (!segmentsCompatible(as[index], bs[index])) return false;
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// An empty list scopes nothing, so it is treated as claiming the whole tree.
|
|
101
|
+
export function overlappingSurfaces(a: readonly string[], b: readonly string[]): Array<[string, string]> {
|
|
102
|
+
const pairs: Array<[string, string]> = [];
|
|
103
|
+
for (const left of a.length ? a : ["."]) {
|
|
104
|
+
for (const right of b.length ? b : ["."]) if (surfacesOverlap(left, right)) pairs.push([left, right]);
|
|
105
|
+
}
|
|
106
|
+
return pairs;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface WriterSurfaceConflict {
|
|
110
|
+
taskId: string;
|
|
111
|
+
root: string;
|
|
112
|
+
pairs: Array<[string, string]>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// The registry key for a writer's cwd: the realpath Git worktree root, so a
|
|
116
|
+
// subdirectory, the root itself and a symlinked spelling share one key; without
|
|
117
|
+
// Git, the realpath of the cwd, and its resolved spelling when even that fails.
|
|
118
|
+
export function canonicalWriterRoot(cwd: string, resolver: WorktreeResolver = resolveSessionWorktree): string {
|
|
119
|
+
const root = resolver(cwd, cwd)?.root;
|
|
120
|
+
if (root) return root;
|
|
121
|
+
try { return realpathSync(cwd); }
|
|
122
|
+
catch { return resolve(cwd); }
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Live writer claims keyed by task id. Writers in different worktree roots
|
|
126
|
+
// never conflict: their surfaces name different files.
|
|
127
|
+
export class WriterSurfaceRegistry {
|
|
128
|
+
private readonly live = new Map<string, { root: string; surfaces: readonly string[] }>();
|
|
129
|
+
|
|
130
|
+
get size(): number {
|
|
131
|
+
return this.live.size;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
conflicts(root: string, surfaces: readonly string[]): WriterSurfaceConflict[] {
|
|
135
|
+
const target = resolve(root);
|
|
136
|
+
const conflicts: WriterSurfaceConflict[] = [];
|
|
137
|
+
for (const [taskId, writer] of this.live) {
|
|
138
|
+
if (writer.root !== target) continue;
|
|
139
|
+
const pairs = overlappingSurfaces(surfaces, writer.surfaces);
|
|
140
|
+
if (pairs.length) conflicts.push({ taskId, root: target, pairs });
|
|
141
|
+
}
|
|
142
|
+
return conflicts;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
claim(taskId: string, root: string, surfaces: readonly string[]): void {
|
|
146
|
+
this.live.set(taskId, { root: resolve(root), surfaces: [...surfaces] });
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
release(taskId: string): boolean {
|
|
150
|
+
return this.live.delete(taskId);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function writerSurfaceConflictMessage(conflicts: readonly WriterSurfaceConflict[]): string {
|
|
155
|
+
const details = conflicts.map(({ taskId, root, pairs }) => {
|
|
156
|
+
const listed = pairs.slice(0, MAX_LISTED_PAIRS).map(([mine, theirs]) => `\`${mine}\` overlaps \`${theirs}\``).join(", ");
|
|
157
|
+
const more = pairs.length > MAX_LISTED_PAIRS ? ` and ${pairs.length - MAX_LISTED_PAIRS} more` : "";
|
|
158
|
+
return `task ${taskId} in ${root} (${listed}${more})`;
|
|
159
|
+
}).join("; ");
|
|
160
|
+
const ids = conflicts.map(conflict => conflict.taskId).join(", ");
|
|
161
|
+
return `Parallel writer rejected: its Allowed edit surfaces overlap a live writer: ${details}. Wait for ${ids} to finish, narrow \`## Allowed edit surfaces\` to paths disjoint from the live writer, or launch this writer in an isolated worktree (workspace_root).`;
|
|
162
|
+
}
|
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { appendSystemPromptOnce, type AppendableSystemPromptOptions } from "./append-system-prompt.ts";
|
|
3
|
-
import {
|
|
3
|
+
import { sameReviewSessionIdentity, resolveCanonicalGitRepositoryIdentity, type ReviewSessionIdentity } from "./review-session-standing-permission.ts";
|
|
4
|
+
import { realpath } from "node:fs/promises";
|
|
5
|
+
import { gentlePiConfigHome } from "./agent-home.ts";
|
|
6
|
+
import { isPermissionMode, resolvePermissionModePolicy, writePermissionModePolicy, type PermissionMode } from "./permission-mode-policy.ts";
|
|
7
|
+
export type { PermissionMode } from "./permission-mode-policy.ts";
|
|
8
|
+
export const MODE_STATUS_KEY = "gentle:mode";
|
|
9
|
+
export const AUTO_MODE_NOTICE = "Auto uses additional model calls with separate usage accounting; hard safety and independent consent remain.";
|
|
4
10
|
|
|
5
11
|
export const YOLO_STATUS_KEY = "gentle:yolo";
|
|
6
12
|
export const YOLO_STATUS_TEXT = "🚀 YOLO ON 🔥 — destructive confirmations remain";
|
|
7
13
|
export const YOLO_DIRECTIVE = `<gentle-yolo-session>
|
|
8
|
-
YOLO session standing permission is ON,
|
|
14
|
+
YOLO session standing permission is ON, authorized by the human for this live primary session and canonical workspace (directly or through a trusted global default).
|
|
9
15
|
Qualify the default commit, push and PR confirmation clauses: for ordinary already-scoped implementation, checks, commits, non-force pushes and PR creation, this activation supplies standing permission instead of repeated permission questions. Make ordinary reversible implementation choices without needless interviews.
|
|
10
16
|
This does NOT override explicit human restrictions, repository policy, project trust, configured confirmations or blocks, or the authorized task scope. Ask before scope expansion, privacy-sensitive disclosure, genuinely unresolved consequential product choices, or ambiguous destinations or credentials. Never invent destinations or credentials.
|
|
11
17
|
Destructive/data-loss confirmations remain mandatory and independent. Never auto-answer ask_user tools, provider consent, maintenance/recovery authorization or opaque-token decisions. Review standing permission and RDD are unchanged. Children remain bounded by their delegated task; they receive no independent delivery grant or YOLO inheritance.
|
|
12
18
|
</gentle-yolo-session>`;
|
|
13
19
|
|
|
14
|
-
/**
|
|
20
|
+
/** Legacy standalone API; separate from review grants and their registry. */
|
|
15
21
|
export class YoloSessionPolicy {
|
|
16
22
|
private grant: ReviewSessionIdentity | undefined;
|
|
17
23
|
private generation = 0;
|
|
18
|
-
|
|
19
24
|
get epoch(): number { return this.generation; }
|
|
20
25
|
get enabled(): boolean { return this.grant !== undefined; }
|
|
21
|
-
|
|
22
|
-
reset(): void {
|
|
23
|
-
this.generation += 1;
|
|
24
|
-
this.grant = undefined;
|
|
25
|
-
}
|
|
26
|
-
|
|
26
|
+
reset(): void { this.generation += 1; this.grant = undefined; }
|
|
27
27
|
set(enabled: boolean, identity: ReviewSessionIdentity | undefined, expectedEpoch = this.epoch): boolean {
|
|
28
28
|
if (expectedEpoch !== this.epoch) return false;
|
|
29
29
|
this.reset();
|
|
@@ -32,7 +32,6 @@ export class YoloSessionPolicy {
|
|
|
32
32
|
this.grant = identity;
|
|
33
33
|
return true;
|
|
34
34
|
}
|
|
35
|
-
|
|
36
35
|
active(identity: ReviewSessionIdentity | undefined): boolean {
|
|
37
36
|
if (!this.grant) return false;
|
|
38
37
|
if (identity !== undefined && sameReviewSessionIdentity(this.grant, identity)) return true;
|
|
@@ -48,31 +47,29 @@ export function updateYoloPrompt(options: AppendableSystemPromptOptions | null |
|
|
|
48
47
|
.replace(`\n\n${YOLO_DIRECTIVE}`, "").replace(YOLO_DIRECTIVE, "");
|
|
49
48
|
if (active) appendSystemPromptOnce(options, YOLO_DIRECTIVE);
|
|
50
49
|
}
|
|
51
|
-
|
|
52
50
|
const HUMAN_ACTION = { on: "on", off: "off", toggle: "" } as const;
|
|
53
51
|
type HumanAction = (typeof HUMAN_ACTION)[keyof typeof HUMAN_ACTION];
|
|
54
52
|
export const YOLO_DISPLAY = { on: "ON", off: "OFF", unavailable: "UNAVAILABLE" } as const;
|
|
55
53
|
export type YoloDisplay = (typeof YOLO_DISPLAY)[keyof typeof YOLO_DISPLAY];
|
|
56
|
-
|
|
57
|
-
/** Private trusted-extension discovery, never a model/tool or serialized authority event. */
|
|
58
54
|
const HOST_UI_CHANNEL = "gentle:yolo:host-ui";
|
|
59
55
|
const DISCOVERY_TIMEOUT_MS = 100;
|
|
60
56
|
export interface YoloUiAdapter {
|
|
61
57
|
read(): Promise<YoloDisplay>;
|
|
58
|
+
readMode?(): Promise<PermissionMode>;
|
|
59
|
+
readSnapshot?(): Promise<{ yolo: YoloDisplay; mode: PermissionMode; defaultMode: PermissionMode }>;
|
|
60
|
+
setMode?(mode: PermissionMode): Promise<boolean>;
|
|
61
|
+
readDefault?(): PermissionMode;
|
|
62
|
+
saveDefault?(mode: PermissionMode): Promise<boolean>;
|
|
62
63
|
toggle(): Promise<void>;
|
|
63
64
|
observe(refresh: () => void): () => void;
|
|
64
65
|
dispose(): void;
|
|
65
66
|
}
|
|
66
|
-
interface HostUiRequest {
|
|
67
|
-
context: ExtensionContext;
|
|
68
|
-
respond(adapter: YoloUiAdapter): void;
|
|
69
|
-
}
|
|
67
|
+
interface HostUiRequest { context: ExtensionContext; respond(adapter: YoloUiAdapter): void }
|
|
70
68
|
function isUiAdapter(value: unknown): value is YoloUiAdapter {
|
|
71
69
|
if (!value || typeof value !== "object") return false;
|
|
72
70
|
const adapter = value as Partial<YoloUiAdapter>;
|
|
73
71
|
return [adapter.read, adapter.toggle, adapter.observe, adapter.dispose].every(fn => typeof fn === "function");
|
|
74
72
|
}
|
|
75
|
-
|
|
76
73
|
/** emit is void (and listeners may be wrapped async); explicitly bound the callback wait. */
|
|
77
74
|
export function discoverYoloUiAdapter(pi: ExtensionAPI, context: ExtensionContext): Promise<YoloUiAdapter | undefined> {
|
|
78
75
|
return new Promise(resolve => {
|
|
@@ -81,160 +78,240 @@ export function discoverYoloUiAdapter(pi: ExtensionAPI, context: ExtensionContex
|
|
|
81
78
|
const respond = (value: unknown) => {
|
|
82
79
|
if (!isUiAdapter(value)) return;
|
|
83
80
|
if (settled) { value.dispose(); return; }
|
|
84
|
-
settled = true;
|
|
85
|
-
clearTimeout(timer);
|
|
86
|
-
resolve(value);
|
|
81
|
+
settled = true; clearTimeout(timer); resolve(value);
|
|
87
82
|
};
|
|
88
83
|
try { pi.events.emit(HOST_UI_CHANNEL, { context, respond } satisfies HostUiRequest); }
|
|
89
84
|
catch { settled = true; clearTimeout(timer); resolve(undefined); }
|
|
90
85
|
});
|
|
91
86
|
}
|
|
92
|
-
|
|
93
87
|
export interface YoloSessionController {
|
|
94
88
|
active(context: ExtensionContext): Promise<boolean>;
|
|
89
|
+
mode(context: ExtensionContext): Promise<PermissionMode>;
|
|
90
|
+
setMode(mode: PermissionMode, context: ExtensionContext): Promise<boolean>;
|
|
91
|
+
defaultMode(): PermissionMode;
|
|
92
|
+
saveDefault(mode: PermissionMode, context: ExtensionContext): Promise<boolean>;
|
|
93
|
+
epoch(): number;
|
|
95
94
|
humanAction(action: HumanAction, context: ExtensionContext, current?: () => boolean): Promise<boolean>;
|
|
96
95
|
reset(context: ExtensionContext): void;
|
|
97
96
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
97
|
+
interface ModeIdentity {
|
|
98
|
+
manager: ExtensionContext["sessionManager"];
|
|
99
|
+
id: string;
|
|
100
|
+
workspace: string;
|
|
101
|
+
}
|
|
102
|
+
/** UI/slash authoring only. Defaults are read from the dedicated trusted user store. */
|
|
103
|
+
export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessEnv,
|
|
104
|
+
options: { onChange?: () => void } = {}): YoloSessionController {
|
|
105
|
+
const store = { gentlePiConfigHome: gentlePiConfigHome(env) };
|
|
106
|
+
let generation = 0;
|
|
102
107
|
let ownerGeneration = 0;
|
|
103
|
-
let
|
|
108
|
+
let liveMode: PermissionMode = "normal";
|
|
109
|
+
let binding: ModeIdentity | undefined;
|
|
110
|
+
let interactiveGrant = false;
|
|
111
|
+
// A same-session revocation must not replay the global elevated preference.
|
|
112
|
+
const seen = new WeakMap<object, Set<string>>();
|
|
104
113
|
const observers = new Set<() => void>();
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
publishedEpoch = policy.epoch;
|
|
113
|
-
for (const refresh of [...observers]) { try { refresh(); } catch { /* display only */ } }
|
|
114
|
+
const pendingCaptures = new Set<{ manager: ModeIdentity["manager"]; id: string }>();
|
|
115
|
+
const primary = () => env.GENTLE_PI_AGENTS_CHILD !== "1";
|
|
116
|
+
const interactive = (ctx: ExtensionContext): boolean => {
|
|
117
|
+
if (!primary() || !ctx.hasUI) return false;
|
|
118
|
+
const mode = (ctx as ExtensionContext & { mode?: string }).mode;
|
|
119
|
+
if (mode !== undefined) return mode === "tui";
|
|
120
|
+
try { return (ctx.ui.getAllThemes?.().length ?? 0) > 0; } catch { return false; }
|
|
114
121
|
};
|
|
115
|
-
const
|
|
122
|
+
const session = (ctx: ExtensionContext): { manager: ModeIdentity["manager"]; id: string } | undefined => {
|
|
116
123
|
try {
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
// Git lookup awaits: a replacement manager ID must not activate stale scope.
|
|
120
|
-
return cwd === context.cwd && identity?.sessionManager === context.sessionManager &&
|
|
121
|
-
identity?.sessionId === context.sessionManager.getSessionId() ? identity : undefined;
|
|
124
|
+
const manager = ctx.sessionManager, id = manager.getSessionId();
|
|
125
|
+
return manager && typeof id === "string" && id.length > 0 ? { manager, id } : undefined;
|
|
122
126
|
} catch { return undefined; }
|
|
123
127
|
};
|
|
124
|
-
const
|
|
125
|
-
const
|
|
126
|
-
const identity = policy.enabled ? await capture(context) : undefined;
|
|
127
|
-
if (epoch !== policy.epoch) return false;
|
|
128
|
-
const enabled = policy.active(identity);
|
|
129
|
-
publish(context, enabled);
|
|
130
|
-
return enabled;
|
|
128
|
+
const remember = (scope: { manager: object; id: string }): void => {
|
|
129
|
+
const ids = seen.get(scope.manager) ?? new Set<string>(); ids.add(scope.id); seen.set(scope.manager, ids);
|
|
131
130
|
};
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
const publish = (ctx: ExtensionContext): void => {
|
|
132
|
+
const yolo = liveMode === "yolo";
|
|
133
|
+
try { ctx.ui.setStatus(YOLO_STATUS_KEY, yolo ? YOLO_STATUS_TEXT : undefined); } catch { /* display only */ }
|
|
134
|
+
try { ctx.ui.setWidget(YOLO_STATUS_KEY, yolo ? [YOLO_STATUS_TEXT] : undefined); } catch { /* display only */ }
|
|
135
|
+
try { ctx.ui.setStatus(MODE_STATUS_KEY, `Permissions: ${liveMode.toUpperCase()}`); } catch { /* display only */ }
|
|
136
|
+
};
|
|
137
|
+
const changed = (ctx: ExtensionContext): void => {
|
|
138
|
+
generation += 1; publish(ctx);
|
|
139
|
+
try { options.onChange?.(); } catch { /* observer is not authority */ }
|
|
140
|
+
for (const refresh of [...observers]) { try { refresh(); } catch { /* display only */ } }
|
|
141
|
+
};
|
|
142
|
+
const notify = (ctx: ExtensionContext, message: string, warning = false): void => {
|
|
143
|
+
try { ctx.ui.notify(message, warning ? "warning" : "info"); } catch { /* headless/stale UI */ }
|
|
139
144
|
};
|
|
140
|
-
//
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const generation = ownerGeneration;
|
|
144
|
-
const manager = context.sessionManager, sessionId = manager.getSessionId(), cwd = context.cwd;
|
|
145
|
+
// Capture plain identity before async filesystem work; recheck after every await.
|
|
146
|
+
const bindCurrent = (ctx: ExtensionContext, disposed = () => false, requireUI = true): (() => boolean) => {
|
|
147
|
+
const owner = ownerGeneration, scope = session(ctx), cwd = ctx.cwd;
|
|
145
148
|
return () => {
|
|
146
|
-
try {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
+
try {
|
|
150
|
+
const now = session(ctx);
|
|
151
|
+
return !disposed() && owner === ownerGeneration && !!scope && !!now &&
|
|
152
|
+
scope.manager === now.manager && scope.id === now.id && cwd === ctx.cwd &&
|
|
153
|
+
(!requireUI || interactive(ctx));
|
|
154
|
+
} catch { return false; }
|
|
149
155
|
};
|
|
150
156
|
};
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
return
|
|
157
|
+
const capture = async (ctx: ExtensionContext): Promise<ModeIdentity | undefined> => {
|
|
158
|
+
if (!primary()) return undefined;
|
|
159
|
+
const scope = session(ctx), cwd = ctx.cwd, current = bindCurrent(ctx, () => false, false);
|
|
160
|
+
if (!scope) return undefined;
|
|
161
|
+
pendingCaptures.add(scope);
|
|
162
|
+
try {
|
|
163
|
+
const canonical = await realpath(cwd);
|
|
164
|
+
if (!current()) return undefined;
|
|
165
|
+
const clone = await resolveCanonicalGitRepositoryIdentity(canonical);
|
|
166
|
+
if (!current()) return undefined;
|
|
167
|
+
return { ...scope, workspace: clone ?? `cwd:${canonical}` };
|
|
168
|
+
} catch { return undefined; }
|
|
169
|
+
finally { pendingCaptures.delete(scope); }
|
|
170
|
+
};
|
|
171
|
+
const defaultMode = (): PermissionMode => resolvePermissionModePolicy(store).mode;
|
|
172
|
+
const mode = async (ctx: ExtensionContext): Promise<PermissionMode> => {
|
|
173
|
+
const epoch = generation;
|
|
174
|
+
const identity = await capture(ctx);
|
|
175
|
+
if (epoch !== generation) return "normal";
|
|
176
|
+
if (binding && (!identity || binding.manager !== identity.manager || binding.id !== identity.id ||
|
|
177
|
+
binding.workspace !== identity.workspace || (interactiveGrant && !interactive(ctx)))) {
|
|
178
|
+
binding = undefined; liveMode = "normal"; interactiveGrant = false; changed(ctx);
|
|
179
|
+
}
|
|
180
|
+
if (!binding && identity) {
|
|
181
|
+
const fresh = !seen.get(identity.manager)?.has(identity.id);
|
|
182
|
+
remember(identity); binding = identity;
|
|
183
|
+
if (fresh) { liveMode = defaultMode(); interactiveGrant = false; changed(ctx); }
|
|
159
184
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
185
|
+
publish(ctx);
|
|
186
|
+
return identity && binding ? liveMode : "normal";
|
|
187
|
+
};
|
|
188
|
+
const active = async (ctx: ExtensionContext): Promise<boolean> => await mode(ctx) === "yolo";
|
|
189
|
+
const reset = (ctx: ExtensionContext): void => {
|
|
190
|
+
ownerGeneration += 1;
|
|
191
|
+
// Initial session_start remains fresh; shutdown/reload of an observed session revokes.
|
|
192
|
+
if (binding) remember(binding);
|
|
193
|
+
const scope = session(ctx);
|
|
194
|
+
if (scope && [...pendingCaptures].some(pending => pending.manager === scope.manager && pending.id === scope.id)) remember(scope);
|
|
195
|
+
binding = undefined; liveMode = "normal"; interactiveGrant = false;
|
|
196
|
+
changed(ctx); observers.clear();
|
|
197
|
+
};
|
|
198
|
+
const setMode = async (requested: PermissionMode, ctx: ExtensionContext, lifetime = () => true): Promise<boolean> => {
|
|
199
|
+
if (!isPermissionMode(requested) || !interactive(ctx) || !lifetime()) return false;
|
|
200
|
+
const current = bindCurrent(ctx, () => !lifetime());
|
|
201
|
+
// Revoke immediately, including off versus a pending activation race.
|
|
202
|
+
const scope = session(ctx); if (scope) remember(scope);
|
|
203
|
+
binding = undefined; liveMode = "normal"; interactiveGrant = false; changed(ctx);
|
|
204
|
+
const epoch = generation;
|
|
205
|
+
const identity = await capture(ctx);
|
|
206
|
+
if (!current() || epoch !== generation || !identity) return false;
|
|
207
|
+
binding = identity; remember(identity); liveMode = requested; interactiveGrant = true; changed(ctx);
|
|
208
|
+
if (requested === "auto") notify(ctx, AUTO_MODE_NOTICE);
|
|
209
|
+
return true;
|
|
210
|
+
};
|
|
211
|
+
const saveDefault = async (requested: PermissionMode, ctx: ExtensionContext, lifetime = () => true): Promise<boolean> => {
|
|
212
|
+
if (!isPermissionMode(requested) || !interactive(ctx) || !lifetime()) return false;
|
|
213
|
+
const current = bindCurrent(ctx, () => !lifetime()), epoch = generation;
|
|
214
|
+
const identity = await capture(ctx);
|
|
215
|
+
if (!identity || !current() || epoch !== generation) return false;
|
|
216
|
+
// Initialize before saving, without another await that could let a superseding
|
|
217
|
+
// mode action authorize this stale save. Observer reentrancy is checked too.
|
|
218
|
+
if (!binding || binding.manager !== identity.manager || binding.id !== identity.id || binding.workspace !== identity.workspace) {
|
|
219
|
+
liveMode = seen.get(identity.manager)?.has(identity.id) ? "normal" : defaultMode();
|
|
220
|
+
binding = identity; remember(identity); interactiveGrant = false; changed(ctx);
|
|
221
|
+
if (!current() || generation !== epoch + 1) return false;
|
|
222
|
+
}
|
|
223
|
+
try { writePermissionModePolicy(requested, store); }
|
|
224
|
+
catch { notify(ctx, "Default unchanged — permission mode preference is malformed, unreadable or could not be saved.", true); return false; }
|
|
225
|
+
notify(ctx, `Default ${requested.toUpperCase()} saved for next primary sessions.${requested === "auto" ? ` ${AUTO_MODE_NOTICE}` : ""}`);
|
|
226
|
+
for (const refresh of [...observers]) { try { refresh(); } catch { /* display only */ } }
|
|
227
|
+
return true;
|
|
228
|
+
};
|
|
229
|
+
const humanAction = async (action: HumanAction, ctx: ExtensionContext, current = () => true): Promise<boolean> => {
|
|
166
230
|
if (!current()) return false;
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
231
|
+
const enable = action === HUMAN_ACTION.on || (action === HUMAN_ACTION.toggle && liveMode !== "yolo");
|
|
232
|
+
const pending = setMode(enable ? "yolo" : "normal", ctx, current);
|
|
233
|
+
const epoch = generation; // setMode revokes synchronously, then publishes once on success.
|
|
234
|
+
const result = await pending;
|
|
235
|
+
if (!current()) return false;
|
|
236
|
+
// Supersession is not activation failure: the winning action owns notification.
|
|
237
|
+
if (generation !== epoch + (result ? 1 : 0)) return true;
|
|
238
|
+
notify(ctx, result ? enable ? YOLO_STATUS_TEXT : "YOLO OFF" :
|
|
239
|
+
"YOLO OFF — activation requires an interactive primary TUI session and an identifiable workspace.", !result);
|
|
174
240
|
return true;
|
|
175
241
|
};
|
|
176
|
-
// Legacy minimal hosts can run the command without an inter-extension bus;
|
|
177
|
-
// they simply cannot expose the menu adapter.
|
|
178
242
|
pi.events?.on?.(HOST_UI_CHANNEL, (payload: unknown) => {
|
|
179
243
|
if (!payload || typeof payload !== "object") return;
|
|
180
244
|
const request = payload as Partial<HostUiRequest>;
|
|
181
245
|
if (typeof request.respond !== "function" || !request.context) return;
|
|
182
|
-
const
|
|
246
|
+
const ctx = request.context;
|
|
183
247
|
try {
|
|
184
248
|
let disposed = false;
|
|
185
249
|
const subscriptions = new Set<() => void>();
|
|
186
|
-
const current = bindCurrent(
|
|
250
|
+
const current = bindCurrent(ctx, () => disposed);
|
|
187
251
|
const read = async (): Promise<YoloDisplay> => {
|
|
188
252
|
if (!current()) return YOLO_DISPLAY.unavailable;
|
|
189
|
-
const epoch =
|
|
190
|
-
const identity = await capture(context);
|
|
253
|
+
const epoch = generation, value = await mode(ctx);
|
|
191
254
|
if (!current()) return YOLO_DISPLAY.unavailable;
|
|
192
|
-
if (epoch !==
|
|
193
|
-
|
|
194
|
-
publish(context, enabled);
|
|
195
|
-
return identity ? enabled ? YOLO_DISPLAY.on : YOLO_DISPLAY.off : YOLO_DISPLAY.unavailable;
|
|
255
|
+
if (epoch !== generation) return read();
|
|
256
|
+
return binding ? value === "yolo" ? YOLO_DISPLAY.on : YOLO_DISPLAY.off : YOLO_DISPLAY.unavailable;
|
|
196
257
|
};
|
|
197
258
|
request.respond({
|
|
198
259
|
read,
|
|
199
|
-
|
|
200
|
-
|
|
260
|
+
readSnapshot: async () => {
|
|
261
|
+
const yolo = await read();
|
|
262
|
+
return { yolo, mode: current() ? liveMode : "normal", defaultMode: defaultMode() };
|
|
263
|
+
},
|
|
264
|
+
readMode: async () => current() ? mode(ctx) : "normal",
|
|
265
|
+
setMode: async value => setMode(value, ctx, current),
|
|
266
|
+
readDefault: defaultMode,
|
|
267
|
+
saveDefault: async value => saveDefault(value, ctx, current),
|
|
268
|
+
toggle: async () => { await humanAction(HUMAN_ACTION.toggle, ctx, current); },
|
|
269
|
+
observe: refresh => {
|
|
201
270
|
if (!current()) return () => {};
|
|
202
271
|
subscriptions.add(refresh); observers.add(refresh);
|
|
203
272
|
return () => { subscriptions.delete(refresh); observers.delete(refresh); };
|
|
204
273
|
},
|
|
205
|
-
dispose: () => {
|
|
206
|
-
disposed = true;
|
|
207
|
-
for (const refresh of subscriptions) observers.delete(refresh);
|
|
208
|
-
subscriptions.clear();
|
|
209
|
-
},
|
|
274
|
+
dispose: () => { disposed = true; for (const refresh of subscriptions) observers.delete(refresh); subscriptions.clear(); },
|
|
210
275
|
});
|
|
211
|
-
} catch { /*
|
|
276
|
+
} catch { /* obsolete trusted context: discovery expires closed */ }
|
|
277
|
+
});
|
|
278
|
+
pi.registerCommand("gentle:mode", {
|
|
279
|
+
description: "Permissions: normal|auto|yolo|status; default normal|auto|yolo saves for new primary sessions.",
|
|
280
|
+
handler: async (args, ctx) => {
|
|
281
|
+
const words = args.trim().split(/\s+/);
|
|
282
|
+
if (!args.trim() || args.trim() === "status") {
|
|
283
|
+
notify(ctx, `Permissions: ${(await mode(ctx)).toUpperCase()} — default: ${defaultMode().toUpperCase()}`); return;
|
|
284
|
+
}
|
|
285
|
+
if (words.length === 2 && words[0] === "default" && isPermissionMode(words[1])) {
|
|
286
|
+
await saveDefault(words[1], ctx); return;
|
|
287
|
+
}
|
|
288
|
+
if (words.length === 1 && isPermissionMode(words[0])) {
|
|
289
|
+
if (await setMode(words[0], ctx)) notify(ctx, `Permissions: ${words[0].toUpperCase()}`);
|
|
290
|
+
else notify(ctx, "Mode unchanged — interactive primary session required or the session changed.", true);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
notify(ctx, "Use /gentle:mode normal|auto|yolo|status or /gentle:mode default normal|auto|yolo. State unchanged.", true);
|
|
294
|
+
},
|
|
212
295
|
});
|
|
213
296
|
pi.registerCommand("gentle:yolo", {
|
|
214
|
-
description: "
|
|
215
|
-
handler: async (args,
|
|
297
|
+
description: "Ordinary development/delivery permission (enable|disable|status); no argument opens a menu. Destructive confirmations remain.",
|
|
298
|
+
handler: async (args, ctx) => {
|
|
216
299
|
let action = args.trim() || "status";
|
|
217
300
|
let current = () => true;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const
|
|
222
|
-
const selected = await context.ui.select(`🚀 Gentle YOLO 🔥 — full speed, destructive actions still ask (current: ${state})`, ["enable", "disable", "status"]);
|
|
301
|
+
if (!args.trim() && ctx.hasUI && typeof ctx.ui.select === "function") {
|
|
302
|
+
current = bindCurrent(ctx);
|
|
303
|
+
const state = await active(ctx) ? YOLO_DISPLAY.on : YOLO_DISPLAY.off;
|
|
304
|
+
const selected = await ctx.ui.select(`🚀 Gentle YOLO 🔥 — full speed, destructive actions still ask (current: ${state})`, ["enable", "disable", "status"]);
|
|
223
305
|
if (selected === undefined) return;
|
|
224
306
|
action = selected;
|
|
225
307
|
}
|
|
226
308
|
if (action !== "enable" && action !== "disable" && action !== "status") {
|
|
227
|
-
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
if (action === "status") {
|
|
231
|
-
context.ui.notify(await active(context) ? YOLO_STATUS_TEXT : "YOLO OFF", "info");
|
|
232
|
-
return;
|
|
309
|
+
notify(ctx, "Use /gentle:yolo enable|disable|status. State unchanged.", true); return;
|
|
233
310
|
}
|
|
234
|
-
if (
|
|
235
|
-
|
|
236
|
-
|
|
311
|
+
if (action === "status") { notify(ctx, await active(ctx) ? YOLO_STATUS_TEXT : "YOLO OFF"); return; }
|
|
312
|
+
if (await humanAction(action === "enable" ? HUMAN_ACTION.on : HUMAN_ACTION.off, ctx, current)) return;
|
|
313
|
+
notify(ctx, "YOLO unchanged — the session changed while the menu was open.", true);
|
|
237
314
|
},
|
|
238
315
|
});
|
|
239
|
-
return { active, humanAction, reset };
|
|
316
|
+
return { active, mode, setMode, defaultMode, saveDefault, epoch: () => generation, humanAction, reset };
|
|
240
317
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gentle-pi",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "Turn Pi into el Gentleman: an ODD development harness with focused subagents, configured TDD evidence, native review, and skill discovery.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"@heyhuynhgiabuu/pi-pretty": "0.6.27"
|
|
76
76
|
},
|
|
77
77
|
"peerDependencies": {
|
|
78
|
-
"@earendil-works/pi-coding-agent": ">=
|
|
78
|
+
"@earendil-works/pi-coding-agent": ">=1.2.0",
|
|
79
79
|
"@earendil-works/pi-ai": "*",
|
|
80
80
|
"@earendil-works/pi-tui": "*",
|
|
81
81
|
"typebox": "*"
|
|
@@ -92,9 +92,9 @@
|
|
|
92
92
|
}
|
|
93
93
|
},
|
|
94
94
|
"devDependencies": {
|
|
95
|
-
"@earendil-works/pi-coding-agent": ">=1.
|
|
96
|
-
"@earendil-works/pi-ai": ">=1.
|
|
97
|
-
"@earendil-works/pi-tui": ">=1.
|
|
95
|
+
"@earendil-works/pi-coding-agent": ">=1.2.0",
|
|
96
|
+
"@earendil-works/pi-ai": ">=1.2.0",
|
|
97
|
+
"@earendil-works/pi-tui": ">=1.2.0",
|
|
98
98
|
"@types/node": "^24.13.3",
|
|
99
99
|
"typescript": "^5.9.3"
|
|
100
100
|
},
|