gentle-pi 3.7.0 → 4.1.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 +155 -10
- package/assets/agents/gentle-ai-explore.md +13 -6
- package/assets/agents/gentle-ai-verify.md +27 -6
- package/assets/agents/gentle-ai-worker.md +13 -15
- 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 +75 -169
- package/assets/orchestrator-memory.md +12 -26
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-skills.md +1 -1
- 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 +30 -38
- 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/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +956 -50
- 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/bundled-install.md +397 -0
- package/docs/delegated-verification.md +27 -4
- package/docs/gentle-agents-activity.md +537 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +165 -51
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +3 -3
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +364 -231
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +35 -0
- package/extensions/child-safety.ts +25 -0
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +1093 -326
- package/extensions/gentle-ai.ts +1636 -890
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +1619 -136
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +126 -20
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1424 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +254 -92
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +191 -58
- package/extensions/startup-banner.ts +71 -33
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +99 -0
- package/lib/agents-runner.ts +180 -107
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -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 +220 -0
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +195 -0
- package/lib/command-palette-catalog.ts +5 -10
- package/lib/command-palette.ts +25 -14
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +89 -27
- package/lib/gentle-shell-resume-hint.ts +176 -0
- 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/history-capture-policy.ts +95 -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 +5 -1
- package/lib/nan-provider.ts +249 -0
- package/lib/native-review-cli.ts +242 -180
- 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-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- 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/overlay-repaint.ts +26 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +73 -21
- package/lib/review-candidate-view.ts +115 -27
- 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-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +227 -56
- package/lib/review-sidebar-state.ts +224 -0
- 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 +527 -0
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +87 -9
- package/lib/shell-bar.ts +215 -30
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +42 -14
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +83 -15
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +84 -31
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +70 -13
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +383 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +119 -0
- package/lib/visual-customize-view.ts +481 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +317 -0
- package/package.json +20 -8
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +87 -25
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- 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 +239 -177
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +225 -54
- 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 +8 -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 +83 -35
- 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/mirror-odd-routing.mjs +2 -2
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +90 -0
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +54 -19
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +91 -38
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +16 -21
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +30 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +97 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +467 -93
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/autonomous-guard.test.ts +69 -1
- 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 +146 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- 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 +87 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-context-files.test.ts +255 -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 +94 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +596 -0
- package/tests/command-palette.test.ts +43 -7
- 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 -78
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +171 -3
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +69 -0
- package/tests/gentle-agents.test.ts +2804 -346
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +21 -4
- 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-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +566 -62
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +493 -14
- package/tests/gentle-ai.test.ts +1427 -236
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +896 -115
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +333 -65
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- 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 +4216 -119
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +333 -10
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +49 -19
- 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 +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -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-authority.test.ts +16 -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 +481 -0
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/native-review-cli.test.ts +239 -118
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +101 -4
- 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 +76 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
- package/tests/odd-routing-contract.test.ts +158 -50
- package/tests/orchestrator-budget.test.ts +43 -45
- 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 +8 -7
- 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/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +344 -123
- package/tests/packed-runner-owned-path.test.ts +80 -0
- 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 +10 -18
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +7 -14
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +569 -187
- package/tests/rdd-aware-verification-contract.test.ts +103 -40
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +237 -21
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +399 -12
- package/tests/review-contract-prompt.test.ts +138 -44
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +866 -6
- package/tests/review-controller-workspace-root.test.ts +206 -6
- 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 +342 -4
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +221 -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 +14 -37
- 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-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +594 -26
- 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-sidebar-state.test.ts +402 -0
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +202 -0
- package/tests/runtime-harness.mjs +186 -800
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +451 -0
- 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-messaging-grants.test.ts +255 -0
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +134 -2
- package/tests/shell-bar.test.ts +422 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +135 -32
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-prompt.test.ts +94 -2
- 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 +351 -22
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +207 -3
- package/tests/shell-usage-view.test.ts +28 -1
- package/tests/shell-usage.test.ts +108 -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 +357 -2
- package/tests/startup-banner.test.ts +246 -4
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -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 +107 -20
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/theme-customization.test.ts +72 -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-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +139 -0
- package/tests/visual-customize-view.test.ts +696 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +271 -0
- package/tests/yolo-mode-runtime.test.ts +188 -0
- package/tests/yolo-mode.test.ts +289 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
- /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
package/lib/session-changes.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
2
|
import { open } from "node:fs/promises";
|
|
4
3
|
import { isAbsolute } from "node:path";
|
|
5
4
|
import { generateUnifiedPatch } from "@earendil-works/pi-coding-agent";
|
|
6
5
|
import { changesModel, type ChangedFile, type WorktreeChanges } from "./shell-changes.ts";
|
|
6
|
+
import { SESSION_WORKTREE_ENTRY } from "./session-worktree-registry.ts";
|
|
7
7
|
|
|
8
|
+
// Captured write/edit evidence of a session and its subagents. It feeds
|
|
9
|
+
// session statistics and tells /gentle:changes which roots the session
|
|
10
|
+
// touched; the files Changes lists always come from Git.
|
|
8
11
|
export const SESSION_CHANGE_ENTRY = "gentle-pi.session-change/v1";
|
|
9
|
-
export const SESSION_CHANGE_EVENT = "gentle-pi:session-change";
|
|
10
12
|
export const SESSION_CHANGE_RELAY = "gentle-pi:child-session-change";
|
|
11
13
|
export const MAX_CHANGE_BYTES = 64 * 1024;
|
|
12
14
|
const MAX_RECORDS = 256;
|
|
@@ -65,13 +67,29 @@ export function isSessionChangeEvidence(value: unknown): value is SessionChangeE
|
|
|
65
67
|
export const sameSnapshot = (a: ChangeSnapshot, b: ChangeSnapshot): boolean =>
|
|
66
68
|
a.kind === b.kind && (a.kind === "absent" || (a.kind === "text" && b.kind === "text" && a.text === b.text));
|
|
67
69
|
const snapshotText = (value: ChangeSnapshot) => value.kind === "text" ? value.text : "";
|
|
70
|
+
const shown = (state: FileState) => Boolean(state.unavailable) || !sameSnapshot(state.before, state.after);
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Roots this session works in: its registered worktrees (own root, explicit and
|
|
74
|
+
* subagent registrations) and the roots of captured evidence (subagents in an
|
|
75
|
+
* independent repository). Only roots, never files.
|
|
76
|
+
*/
|
|
77
|
+
export function sessionChangeRoots(sessionId: string, entries: readonly Entry[]): string[] {
|
|
78
|
+
const roots = new Set<string>();
|
|
79
|
+
for (const entry of entries) {
|
|
80
|
+
if (entry.type !== "custom" || !entry.data || typeof entry.data !== "object") continue;
|
|
81
|
+
const data = entry.data as { sessionId?: unknown; root?: unknown; evidence?: unknown };
|
|
82
|
+
if (data.sessionId !== sessionId) continue;
|
|
83
|
+
if (entry.customType === SESSION_WORKTREE_ENTRY && typeof data.root === "string" && isAbsolute(data.root)) roots.add(data.root);
|
|
84
|
+
if (entry.customType === SESSION_CHANGE_ENTRY && isSessionChangeEvidence(data.evidence)) roots.add(data.evidence.root);
|
|
85
|
+
}
|
|
86
|
+
return [...roots];
|
|
87
|
+
}
|
|
68
88
|
|
|
69
89
|
export class SessionChanges {
|
|
70
90
|
private readonly seen = new Set<string>();
|
|
71
91
|
private readonly files = new Map<string, Map<string, FileState>>();
|
|
72
92
|
private bytes = 0;
|
|
73
|
-
private announced = false;
|
|
74
|
-
notice: string | undefined;
|
|
75
93
|
readonly sessionId: string;
|
|
76
94
|
private cached: WorktreeChanges[] | undefined;
|
|
77
95
|
constructor(sessionId: string, entries: readonly Entry[] = []) { this.sessionId = sessionId; this.restore(entries); }
|
|
@@ -86,10 +104,8 @@ export class SessionChanges {
|
|
|
86
104
|
record(evidence: SessionChangeEvidence): boolean {
|
|
87
105
|
if (!isSessionChangeEvidence(evidence) || this.seen.has(evidence.id)) return false;
|
|
88
106
|
const bytes = Buffer.byteLength(JSON.stringify(evidence));
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return false;
|
|
92
|
-
}
|
|
107
|
+
// Bounds session-file growth; Changes itself lists from Git without a cap.
|
|
108
|
+
if (this.seen.size >= MAX_RECORDS || this.bytes + bytes > MAX_SESSION_BYTES) return false;
|
|
93
109
|
this.seen.add(evidence.id);
|
|
94
110
|
this.bytes += bytes;
|
|
95
111
|
if (sameSnapshot(evidence.before, evidence.after)) return false;
|
|
@@ -109,13 +125,13 @@ export class SessionChanges {
|
|
|
109
125
|
}
|
|
110
126
|
private changed(root: string): ChangedFile[] {
|
|
111
127
|
return [...(this.files.get(root) ?? [])].flatMap(([path, state]) => {
|
|
112
|
-
if (!
|
|
128
|
+
if (!shown(state)) return [];
|
|
113
129
|
const patch = this.patch(path, state);
|
|
114
130
|
const patchLines = patch.split("\n");
|
|
115
131
|
const firstHunk = patchLines.findIndex(line => line.startsWith("@@"));
|
|
116
132
|
const lines = firstHunk < 0 ? [] : patchLines.slice(firstHunk + 1);
|
|
117
133
|
return [{
|
|
118
|
-
path,
|
|
134
|
+
path, status: state.before.kind === "absent" ? "added" as const : state.after.kind === "absent" ? "deleted" as const : "modified" as const,
|
|
119
135
|
added: state.unavailable ? 0 : lines.filter(line => line.startsWith("+")).length,
|
|
120
136
|
deleted: state.unavailable ? 0 : lines.filter(line => line.startsWith("-")).length,
|
|
121
137
|
...(state.unavailable ? { countsUnavailable: state.unavailable } : {}),
|
|
@@ -131,17 +147,6 @@ export class SessionChanges {
|
|
|
131
147
|
}
|
|
132
148
|
get model() {
|
|
133
149
|
const trees = this.worktrees;
|
|
134
|
-
return
|
|
135
|
-
}
|
|
136
|
-
/** Return the capture-limit notice once, for a single user notification. The `notice` field stays set. */
|
|
137
|
-
takeNotice(): string | undefined {
|
|
138
|
-
if (!this.notice || this.announced) return undefined;
|
|
139
|
-
this.announced = true;
|
|
140
|
-
return this.notice;
|
|
141
|
-
}
|
|
142
|
-
async refresh() { return this.model; }
|
|
143
|
-
loadDiff(root: string, file: ChangedFile): string {
|
|
144
|
-
const state = this.files.get(root)?.get(file.path);
|
|
145
|
-
return state ? this.patch(file.path, state) : "No captured agent diff for this file.";
|
|
150
|
+
return changesModel(trees.flatMap(tree => tree.model.files.map(file => ({ ...file, path: trees.length === 1 ? file.path : tree.root + "/" + file.path }))));
|
|
146
151
|
}
|
|
147
152
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export const MESSAGING_REASON_MIN_CHARACTERS = 8;
|
|
4
|
+
export const MESSAGING_REASON_MAX_UTF8_BYTES = 512;
|
|
5
|
+
|
|
6
|
+
const PREVIEW_CONTROL_CHARACTER = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u;
|
|
7
|
+
|
|
8
|
+
function escapePreviewText(value: string): string {
|
|
9
|
+
return Array.from(value, (character) => {
|
|
10
|
+
if (!PREVIEW_CONTROL_CHARACTER.test(character)) return character;
|
|
11
|
+
if (character === "\r") return "\\r";
|
|
12
|
+
if (character === "\n") return "\\n";
|
|
13
|
+
if (character === "\t") return "\\t";
|
|
14
|
+
return `\\u${character.codePointAt(0)!.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
15
|
+
}).join("");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeMessagingReason(value: unknown): string {
|
|
19
|
+
if (typeof value !== "string") throw new Error("Cross-orchestrator communication requires a concrete reason of at least 8 trimmed characters and at most 512 UTF-8 bytes.");
|
|
20
|
+
const reason = value.trim();
|
|
21
|
+
if (Array.from(reason).length < MESSAGING_REASON_MIN_CHARACTERS || Buffer.byteLength(value, "utf8") > MESSAGING_REASON_MAX_UTF8_BYTES) {
|
|
22
|
+
throw new Error("Cross-orchestrator communication requires a concrete reason of at least 8 trimmed characters and at most 512 UTF-8 bytes.");
|
|
23
|
+
}
|
|
24
|
+
return reason;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Supported decision tokens for cross-orchestrator communication consent.
|
|
29
|
+
*/
|
|
30
|
+
export const MESSAGING_CONSENT_DECISIONS = {
|
|
31
|
+
ALLOW_ONCE: "Allow once",
|
|
32
|
+
ALLOW_SESSION: "Allow for this session",
|
|
33
|
+
DENY: "Deny",
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export type MessagingConsentDecision =
|
|
37
|
+
(typeof MESSAGING_CONSENT_DECISIONS)[keyof typeof MESSAGING_CONSENT_DECISIONS];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Options for requesting human authorization before outbound cross-session communication.
|
|
41
|
+
*/
|
|
42
|
+
export interface MessagingConsentOptions {
|
|
43
|
+
/** Outbound message payload. */
|
|
44
|
+
message: string;
|
|
45
|
+
/** Concrete caller-supplied reason explaining why this communication is needed. */
|
|
46
|
+
reason: string;
|
|
47
|
+
/** Optional abort signal for request cancellation. */
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Ephemeral in-memory grant manager for cross-orchestrator communication.
|
|
53
|
+
* Permissions are scoped strictly to the live session identity and session manager instance.
|
|
54
|
+
*/
|
|
55
|
+
export class SessionMessagingGrants {
|
|
56
|
+
private readonly grants = new Map<string, Set<string>>();
|
|
57
|
+
private manager?: ExtensionContext["sessionManager"];
|
|
58
|
+
private sessionId?: string;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Authorizes an outbound cross-orchestrator message to the given recipient.
|
|
62
|
+
* Prompts the user interactively when no session-level grant exists, and fails closed otherwise.
|
|
63
|
+
*/
|
|
64
|
+
async authorize(
|
|
65
|
+
ctx: Pick<ExtensionContext, "sessionManager" | "hasUI" | "ui">,
|
|
66
|
+
recipient: string,
|
|
67
|
+
options: MessagingConsentOptions
|
|
68
|
+
): Promise<void> {
|
|
69
|
+
const message = options.message;
|
|
70
|
+
if (typeof message !== "string" || Buffer.byteLength(message, "utf8") > 8192) throw new Error("Cross-orchestrator message is invalid.");
|
|
71
|
+
const reason = normalizeMessagingReason(options.reason);
|
|
72
|
+
const signal = options.signal;
|
|
73
|
+
if (signal?.aborted) throw new Error("Cross-orchestrator message authorization aborted.");
|
|
74
|
+
const id = ctx.sessionManager.getSessionId();
|
|
75
|
+
if (!id) throw new Error("Cross-orchestrator communication requires an active session identity.");
|
|
76
|
+
if (this.manager !== ctx.sessionManager || this.sessionId !== id) {
|
|
77
|
+
this.grants.clear();
|
|
78
|
+
this.manager = ctx.sessionManager;
|
|
79
|
+
this.sessionId = id;
|
|
80
|
+
}
|
|
81
|
+
if (!ctx.hasUI || typeof ctx.ui?.select !== "function") {
|
|
82
|
+
throw new Error("Cross-orchestrator communication requires interactive human consent before sending.");
|
|
83
|
+
}
|
|
84
|
+
if (this.grants.get(id)?.has(recipient)) return;
|
|
85
|
+
|
|
86
|
+
const previewMessage = escapePreviewText(message);
|
|
87
|
+
const isTruncated = message.length > 200;
|
|
88
|
+
const messageLabel = isTruncated
|
|
89
|
+
? `Message (${message.length} chars, preview): ${escapePreviewText(message.slice(0, 197))}...`
|
|
90
|
+
: `Message: ${previewMessage}`;
|
|
91
|
+
const title = `Authorize cross-orchestrator message to ${escapePreviewText(recipient)}?\nReason: ${escapePreviewText(reason)}\n${messageLabel}`;
|
|
92
|
+
|
|
93
|
+
const selected = await ctx.ui.select(
|
|
94
|
+
title,
|
|
95
|
+
[
|
|
96
|
+
MESSAGING_CONSENT_DECISIONS.ALLOW_ONCE,
|
|
97
|
+
MESSAGING_CONSENT_DECISIONS.ALLOW_SESSION,
|
|
98
|
+
MESSAGING_CONSENT_DECISIONS.DENY,
|
|
99
|
+
],
|
|
100
|
+
{ signal }
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
if (signal?.aborted) throw new Error("Cross-orchestrator message authorization aborted.");
|
|
104
|
+
if (ctx.sessionManager !== this.manager || ctx.sessionManager.getSessionId() !== id) {
|
|
105
|
+
throw new Error("Session identity changed during authorization.");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (selected === MESSAGING_CONSENT_DECISIONS.ALLOW_SESSION) {
|
|
109
|
+
const sessionGrants = this.grants.get(id) ?? new Set<string>();
|
|
110
|
+
sessionGrants.add(recipient);
|
|
111
|
+
this.grants.set(id, sessionGrants);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (selected === MESSAGING_CONSENT_DECISIONS.ALLOW_ONCE) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
throw new Error("Cross-orchestrator communication denied by user.");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Checks whether a live session-level grant exists for the target recipient in the current session.
|
|
124
|
+
*/
|
|
125
|
+
assertCurrent(ctx: Pick<ExtensionContext, "sessionManager">, recipient: string): boolean {
|
|
126
|
+
if (
|
|
127
|
+
ctx.sessionManager !== this.manager ||
|
|
128
|
+
!this.sessionId ||
|
|
129
|
+
ctx.sessionManager.getSessionId() !== this.sessionId
|
|
130
|
+
) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
return this.grants.get(this.sessionId)?.has(recipient) ?? false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Session-bound active profiles (gentle-shell#1064, slice 1).
|
|
2
|
+
//
|
|
3
|
+
// One parent Pi session may bind one saved profile for its own launches without
|
|
4
|
+
// touching any shared state: no clone pin (`p`), no repository declaration (`P`),
|
|
5
|
+
// no global routing, no materialized stores, no agent frontmatter, no Pi default
|
|
6
|
+
// settings. The binding is an in-process store keyed by the parent session id, so
|
|
7
|
+
// concurrent Pi processes are isolated by construction and a resumed session
|
|
8
|
+
// starts from the shared layers (`p → P → global`) until it binds again.
|
|
9
|
+
// Persistence across resume and reload semantics are deliberate follow-up
|
|
10
|
+
// slices; this module never writes a file.
|
|
11
|
+
//
|
|
12
|
+
// Pi loads every extension entrypoint with its own Jiti instance and
|
|
13
|
+
// `moduleCache: false` (loader.js `loadExtensionModule`), so a module-local Map
|
|
14
|
+
// would be re-evaluated per entrypoint: the panel writes one copy while the
|
|
15
|
+
// launch, usage, and status readers hold their own empty copies
|
|
16
|
+
// (gentle-shell#1558). The store therefore lives on `globalThis` behind a
|
|
17
|
+
// `Symbol.for` key, which every evaluation of this module in one process
|
|
18
|
+
// resolves to the same symbol and therefore the same shared Map.
|
|
19
|
+
import type { AgentModelConfig } from "./model-routing-authority.ts";
|
|
20
|
+
|
|
21
|
+
/** A session's bound profile: its saved name plus the routing snapshot taken at bind time. */
|
|
22
|
+
export interface SessionProfileBinding {
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly modelProfiles: AgentModelConfig;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const bindingsRegistryKey = Symbol.for("gentle-pi:session-profile-bindings");
|
|
28
|
+
|
|
29
|
+
/** The one process-wide binding store, shared across every extension entrypoint that imports this module. */
|
|
30
|
+
function bindingsBySession(): Map<string, SessionProfileBinding> {
|
|
31
|
+
const holder = globalThis as { [bindingsRegistryKey]?: Map<string, SessionProfileBinding> };
|
|
32
|
+
(holder[bindingsRegistryKey] ??= new Map());
|
|
33
|
+
return holder[bindingsRegistryKey]!;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Copy one routing snapshot level deep — the layer a defensive copy needs, so a binding never aliases its source or its readers. */
|
|
37
|
+
function cloneProfiles(profiles: AgentModelConfig): AgentModelConfig {
|
|
38
|
+
return Object.fromEntries(Object.entries(profiles).map(([agent, entry]) => [agent, { ...entry }]));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Bind (or rebind) one parent session to a profile. The routing snapshot is
|
|
43
|
+
* copied, so later edits to the caller's object or to the saved profile
|
|
44
|
+
* definition never mutate a session that already bound it; refreshing a bound
|
|
45
|
+
* session requires an explicit re-selection.
|
|
46
|
+
*/
|
|
47
|
+
export function bindSessionProfile(sessionId: string, name: string, modelProfiles: AgentModelConfig): void {
|
|
48
|
+
bindingsBySession().set(sessionId, { name, modelProfiles: cloneProfiles(modelProfiles) });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Read a session's binding as a private copy. Returns `undefined` for unknown,
|
|
53
|
+
* cleared, or undefined session ids: an unbound session keeps resolving the
|
|
54
|
+
* shared layers exactly as before.
|
|
55
|
+
*/
|
|
56
|
+
export function readSessionProfileBinding(sessionId: string | undefined): SessionProfileBinding | undefined {
|
|
57
|
+
if (sessionId === undefined) return undefined;
|
|
58
|
+
const binding = bindingsBySession().get(sessionId);
|
|
59
|
+
return binding === undefined ? undefined : { name: binding.name, modelProfiles: cloneProfiles(binding.modelProfiles) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Remove one session's binding without touching any other session's. */
|
|
63
|
+
export function clearSessionProfileBinding(sessionId: string): void {
|
|
64
|
+
bindingsBySession().delete(sessionId);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Testing-only reset of every binding. */
|
|
68
|
+
export function resetSessionProfileBindingsForTesting(): void {
|
|
69
|
+
bindingsBySession().clear();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The launch-time precedence for wholesale routing replacement: a session
|
|
74
|
+
* binding outranks a winning pin, a pin alone still replaces, and neither
|
|
75
|
+
* leaves today's routing untouched. Named so the precedence rule a launch and
|
|
76
|
+
* the status display agree on lives in exactly one place.
|
|
77
|
+
*/
|
|
78
|
+
export function sessionOrPinModelProfiles(
|
|
79
|
+
session: AgentModelConfig | undefined,
|
|
80
|
+
pin: AgentModelConfig | undefined,
|
|
81
|
+
): AgentModelConfig | undefined {
|
|
82
|
+
return session ?? pin;
|
|
83
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
// Inherited profile freeze (gentle-shell#1064 slice 3b-ii).
|
|
2
|
+
//
|
|
3
|
+
// A parent session that never pressed Enter in `/gentle:profiles` resolves the
|
|
4
|
+
// shared defaults `p → P → G` (local pin, repository declaration, global
|
|
5
|
+
// `active` in profiles.json) ONCE, at startup, and keeps that snapshot with its
|
|
6
|
+
// origin. Changing a default later only affects new sessions, exactly like Pi's
|
|
7
|
+
// "set as default" for the orchestrator model; an open session changes profile
|
|
8
|
+
// only through its own Enter. Each launch re-resolves the defaults to compare
|
|
9
|
+
// and shows one drift notice per distinct change.
|
|
10
|
+
//
|
|
11
|
+
// Precedence, the single rule every consumer reads through
|
|
12
|
+
// `resolveSessionProfile`:
|
|
13
|
+
//
|
|
14
|
+
// explicit Enter binding → frozen inherited snapshot → live p → P → G
|
|
15
|
+
//
|
|
16
|
+
// The live resolution applies only in `follow` mode (GENTLE_PI_PROFILE_FOLLOW=1,
|
|
17
|
+
// for CI and headless runs), in subagent children (their routing comes from the
|
|
18
|
+
// parent through --model), and when no session id is available. In `follow`
|
|
19
|
+
// mode the orchestrator still comes from the startup profile while children
|
|
20
|
+
// follow later defaults: an accepted, documented limitation.
|
|
21
|
+
//
|
|
22
|
+
// The frozen state is in-process only, like the explicit binding of slice 2: it
|
|
23
|
+
// lives on `globalThis` behind a `Symbol.for` key because Pi evaluates this
|
|
24
|
+
// module once per extension entrypoint. A resumed session (`--resume`,
|
|
25
|
+
// `--continue`, `--session`) therefore freezes again from the current defaults,
|
|
26
|
+
// and its drift memory starts empty. Persisting the freeze as a
|
|
27
|
+
// `gentle-pi.session-profile/v1` entry with origin `local | repo | global` is
|
|
28
|
+
// the integration point with slice 3b-i.
|
|
29
|
+
import { createHash } from "node:crypto";
|
|
30
|
+
import { profileRoleEntries, profilesFilePath, readProfilesFileResult } from "./agent-profiles.ts";
|
|
31
|
+
import { readProfilePinStatus, resolveProfilePin, resolveUnversionedProjectProfile } from "./agent-profile-pin.ts";
|
|
32
|
+
import type { AgentModelConfig } from "./model-routing-authority.ts";
|
|
33
|
+
import { readSessionProfileBinding } from "./session-profile-binding.ts";
|
|
34
|
+
import type { WorktreeIdentity, WorktreeResolver } from "./session-worktree-registry.ts";
|
|
35
|
+
|
|
36
|
+
/** Opt-in that keeps the live `p → P → G` resolution instead of freezing it at startup. */
|
|
37
|
+
export const PROFILE_FOLLOW_ENV = "GENTLE_PI_PROFILE_FOLLOW";
|
|
38
|
+
|
|
39
|
+
/** Which shared default supplied an inherited profile. */
|
|
40
|
+
export type InheritedProfileOrigin = "local" | "repo" | "global";
|
|
41
|
+
|
|
42
|
+
/** A profile resolved from the shared defaults: its name, its origin, and its role routing. */
|
|
43
|
+
export interface InheritedProfile {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly origin: InheritedProfileOrigin;
|
|
46
|
+
readonly modelProfiles: AgentModelConfig;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A frozen session: `profile` is `undefined` when no layer had a profile at startup. */
|
|
50
|
+
export interface FrozenInheritedProfile {
|
|
51
|
+
readonly profile: InheritedProfile | undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The profile a session uses. `inherited` is the startup snapshot (with or
|
|
56
|
+
* without a profile); `follow` and `live` carry the current defaults.
|
|
57
|
+
*/
|
|
58
|
+
export type SessionProfileResolution =
|
|
59
|
+
| { kind: "explicit"; name: string; modelProfiles: AgentModelConfig }
|
|
60
|
+
| { kind: "inherited"; profile: InheritedProfile | undefined }
|
|
61
|
+
| { kind: "follow"; profile: InheritedProfile | undefined }
|
|
62
|
+
| { kind: "live"; profile: InheritedProfile | undefined };
|
|
63
|
+
|
|
64
|
+
export interface InheritedProfileOptions {
|
|
65
|
+
cwd: string;
|
|
66
|
+
configHome: string;
|
|
67
|
+
resolveWorktree?: WorktreeResolver;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface SessionProfileOptions extends InheritedProfileOptions {
|
|
71
|
+
sessionId: string | undefined;
|
|
72
|
+
env?: NodeJS.ProcessEnv;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface FreezeRegistry {
|
|
76
|
+
frozen: Map<string, FrozenInheritedProfile>;
|
|
77
|
+
// Drift fingerprints already reported, per session.
|
|
78
|
+
notified: Map<string, Set<string>>;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const freezeRegistryKey = Symbol.for("gentle-pi:session-profile-freeze");
|
|
82
|
+
|
|
83
|
+
function registry(): FreezeRegistry {
|
|
84
|
+
const holder = globalThis as { [freezeRegistryKey]?: FreezeRegistry };
|
|
85
|
+
(holder[freezeRegistryKey] ??= { frozen: new Map(), notified: new Map() });
|
|
86
|
+
return holder[freezeRegistryKey]!;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function cloneProfiles(profiles: AgentModelConfig): AgentModelConfig {
|
|
90
|
+
return Object.fromEntries(Object.entries(profiles).map(([agent, entry]) => [agent, { ...entry }]));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cloneProfile(profile: InheritedProfile | undefined): InheritedProfile | undefined {
|
|
94
|
+
return profile === undefined ? undefined : { name: profile.name, origin: profile.origin, modelProfiles: cloneProfiles(profile.modelProfiles) };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function roleRouting(config: AgentModelConfig): AgentModelConfig {
|
|
98
|
+
return Object.fromEntries(profileRoleEntries(config).map(([agent, entry]) => [agent, { ...entry }]));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function profileFollowEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
102
|
+
return env[PROFILE_FOLLOW_ENV] === "1";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isChildSession(env: NodeJS.ProcessEnv): boolean {
|
|
106
|
+
return env.GENTLE_PI_AGENTS_CHILD === "1";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The shared defaults `p → P → G` for one directory, with the layer that won.
|
|
111
|
+
* Pin layers keep `resolveProfilePin` semantics; outside a Git worktree the
|
|
112
|
+
* repository declaration is read as admission does for non-Git projects; the
|
|
113
|
+
* global layer is the `active` profile of profiles.json. The orchestrator key
|
|
114
|
+
* is never part of the routing. `undefined` means no layer has a profile.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveInheritedProfile(options: InheritedProfileOptions): InheritedProfile | undefined {
|
|
117
|
+
// One worktree lookup serves both the pin layers and the non-Git check.
|
|
118
|
+
let resolved = false;
|
|
119
|
+
let identity: WorktreeIdentity | undefined;
|
|
120
|
+
const resolveWorktree: WorktreeResolver | undefined = options.resolveWorktree === undefined ? undefined : (cwd, base) => {
|
|
121
|
+
if (!resolved) {
|
|
122
|
+
resolved = true;
|
|
123
|
+
identity = options.resolveWorktree!(cwd, base);
|
|
124
|
+
}
|
|
125
|
+
return identity;
|
|
126
|
+
};
|
|
127
|
+
const pin = resolveProfilePin({ cwd: options.cwd, configHome: options.configHome, resolveWorktree });
|
|
128
|
+
if (pin !== undefined) return { name: pin.profile, origin: pin.source, modelProfiles: pin.modelProfiles };
|
|
129
|
+
if (readProfilePinStatus(options.cwd, resolveWorktree) === undefined) {
|
|
130
|
+
const declaration = resolveUnversionedProjectProfile(options.cwd, options.configHome);
|
|
131
|
+
if (declaration !== undefined) return { name: declaration.profile, origin: "repo", modelProfiles: declaration.modelProfiles };
|
|
132
|
+
}
|
|
133
|
+
const store = readProfilesFileResult(profilesFilePath(options.configHome));
|
|
134
|
+
if (store.status !== "valid" || store.file.active === undefined) return undefined;
|
|
135
|
+
const active = store.file.active;
|
|
136
|
+
if (!Object.prototype.hasOwnProperty.call(store.file.profiles, active)) return undefined;
|
|
137
|
+
return { name: active, origin: "global", modelProfiles: roleRouting(store.file.profiles[active]!) };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** A copy of one session's frozen snapshot, or `undefined` when it never froze. */
|
|
141
|
+
export function readFrozenInheritedProfile(sessionId: string | undefined): FrozenInheritedProfile | undefined {
|
|
142
|
+
if (sessionId === undefined) return undefined;
|
|
143
|
+
const frozen = registry().frozen.get(sessionId);
|
|
144
|
+
return frozen === undefined ? undefined : { profile: cloneProfile(frozen.profile) };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function freezes(options: SessionProfileOptions): boolean {
|
|
148
|
+
const env = options.env ?? process.env;
|
|
149
|
+
return options.sessionId !== undefined && options.sessionId.length > 0 && !isChildSession(env) && !profileFollowEnabled(env);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Only called after `freezes(options)`, which guarantees a session id.
|
|
153
|
+
function frozenOrFreeze(options: SessionProfileOptions): FrozenInheritedProfile {
|
|
154
|
+
const store = registry().frozen;
|
|
155
|
+
const sessionId = options.sessionId!;
|
|
156
|
+
let frozen = store.get(sessionId);
|
|
157
|
+
if (frozen === undefined) {
|
|
158
|
+
frozen = { profile: cloneProfile(resolveInheritedProfile(options)) };
|
|
159
|
+
store.set(sessionId, frozen);
|
|
160
|
+
}
|
|
161
|
+
return { profile: cloneProfile(frozen.profile) };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Freeze a parent session's inherited profile at startup. Idempotent: the
|
|
166
|
+
* first snapshot wins, so a session keeps it across `/reload` in the same
|
|
167
|
+
* process. Children and `follow` sessions never freeze.
|
|
168
|
+
*/
|
|
169
|
+
export function freezeSessionProfileAtStartup(options: SessionProfileOptions): void {
|
|
170
|
+
if (freezes(options)) frozenOrFreeze(options);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The single precedence rule for launch, status and Usage. A parent session
|
|
175
|
+
* that has not frozen yet freezes here, so the snapshot never depends on which
|
|
176
|
+
* extension's `session_start` ran first.
|
|
177
|
+
*/
|
|
178
|
+
export function resolveSessionProfile(options: SessionProfileOptions): SessionProfileResolution {
|
|
179
|
+
const explicit = readSessionProfileBinding(options.sessionId);
|
|
180
|
+
if (explicit !== undefined) return { kind: "explicit", name: explicit.name, modelProfiles: explicit.modelProfiles };
|
|
181
|
+
if (freezes(options)) return { kind: "inherited", profile: frozenOrFreeze(options).profile };
|
|
182
|
+
const env = options.env ?? process.env;
|
|
183
|
+
const profile = resolveInheritedProfile(options);
|
|
184
|
+
return isChildSession(env) || !profileFollowEnabled(env) ? { kind: "live", profile } : { kind: "follow", profile };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** The routing that replaces subagent model profiles, or `undefined` for today's routing. */
|
|
188
|
+
export function sessionProfileModelProfiles(resolution: SessionProfileResolution): AgentModelConfig | undefined {
|
|
189
|
+
return resolution.kind === "explicit" ? resolution.modelProfiles : resolution.profile?.modelProfiles;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The routing one launch target uses under a resolution read once per request.
|
|
194
|
+
* Explicit and frozen profiles belong to the session, so they apply wherever
|
|
195
|
+
* the launch goes, a foreign repository included; a `follow` or `live`
|
|
196
|
+
* resolution reads the defaults of the target itself. A session frozen without
|
|
197
|
+
* a profile has nothing to carry into a foreign repository, so that
|
|
198
|
+
* repository keeps its own pin layers (local pin, then declaration), as
|
|
199
|
+
* before the freeze; the global active profile is not the repository's own
|
|
200
|
+
* default and does not apply there either.
|
|
201
|
+
*/
|
|
202
|
+
export function sessionProfileRoutingAt(
|
|
203
|
+
resolution: SessionProfileResolution,
|
|
204
|
+
target: InheritedProfileOptions & { foreignRepository?: boolean },
|
|
205
|
+
): AgentModelConfig | undefined {
|
|
206
|
+
if (resolution.kind === "explicit") return resolution.modelProfiles;
|
|
207
|
+
if (resolution.kind === "inherited") {
|
|
208
|
+
if (resolution.profile !== undefined || target.foreignRepository !== true) return resolution.profile?.modelProfiles;
|
|
209
|
+
return resolveProfilePin(target)?.modelProfiles;
|
|
210
|
+
}
|
|
211
|
+
return resolveInheritedProfile(target)?.modelProfiles;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** The status label, spelled as before: `name (session)`, `name (local)`, `name (repo)`, or the bare global name. */
|
|
215
|
+
export function sessionProfileLabel(resolution: SessionProfileResolution): string | undefined {
|
|
216
|
+
if (resolution.kind === "explicit") return `${resolution.name} (session)`;
|
|
217
|
+
const profile = resolution.profile;
|
|
218
|
+
if (profile === undefined) return undefined;
|
|
219
|
+
return profile.origin === "global" ? profile.name : `${profile.name} (${profile.origin})`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function stableJson(value: unknown): string {
|
|
223
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
224
|
+
if (value !== null && typeof value === "object") {
|
|
225
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson((value as Record<string, unknown>)[key])}`).join(",")}}`;
|
|
226
|
+
}
|
|
227
|
+
return JSON.stringify(value) ?? "null";
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// The origin does not route anything: the same profile reached through another
|
|
231
|
+
// layer is not drift.
|
|
232
|
+
function fingerprint(profile: InheritedProfile | undefined): string {
|
|
233
|
+
if (profile === undefined) return "none";
|
|
234
|
+
return createHash("sha256").update(stableJson({ name: profile.name, modelProfiles: profile.modelProfiles })).digest("hex");
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function describe(profile: InheritedProfile | undefined): string {
|
|
238
|
+
return profile === undefined ? "no profile" : `"${profile.name}" (${profile.origin})`;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Compare a frozen inherited session with the current defaults. Returns the
|
|
243
|
+
* one-line notice the first time a given current default differs from the
|
|
244
|
+
* snapshot, and `undefined` otherwise: the same change is never reported
|
|
245
|
+
* twice in one session. Explicit bindings, `follow` sessions and children are
|
|
246
|
+
* never compared. The comparison never adopts the new default.
|
|
247
|
+
*/
|
|
248
|
+
export function inheritedProfileDriftNotice(options: SessionProfileOptions): string | undefined {
|
|
249
|
+
const resolution = resolveSessionProfile(options);
|
|
250
|
+
if (resolution.kind !== "inherited") return undefined;
|
|
251
|
+
const current = resolveInheritedProfile(options);
|
|
252
|
+
const key = fingerprint(current);
|
|
253
|
+
if (key === fingerprint(resolution.profile)) return undefined;
|
|
254
|
+
const notified = registry().notified;
|
|
255
|
+
const seen = notified.get(options.sessionId!) ?? new Set<string>();
|
|
256
|
+
if (seen.has(key)) return undefined;
|
|
257
|
+
seen.add(key);
|
|
258
|
+
notified.set(options.sessionId!, seen);
|
|
259
|
+
return `el Gentleman: the default profile changed to ${describe(current)}, this session keeps ${describe(resolution.profile)}. Press Enter on a profile in /gentle:profiles to adopt it.`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Testing-only reset of every frozen snapshot and drift memory. */
|
|
263
|
+
export function resetFrozenInheritedProfilesForTesting(): void {
|
|
264
|
+
registry().frozen.clear();
|
|
265
|
+
registry().notified.clear();
|
|
266
|
+
}
|