gentle-pi 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -18
- package/assets/agents/gentle-ai-explore.md +11 -4
- package/assets/agents/gentle-ai-verify.md +23 -4
- package/assets/agents/gentle-ai-worker.md +6 -6
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +70 -163
- package/assets/orchestrator-memory.md +11 -4
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +29 -25
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +708 -22
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +399 -0
- package/docs/delegated-verification.md +3 -5
- package/docs/gentle-agents-activity.md +513 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +90 -33
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +1 -1
- package/docs/permission-modes.md +127 -0
- package/docs/prompt-history.md +1 -1
- package/docs/readme-reference.md +310 -47
- package/docs/render-cache-performance.md +28 -0
- package/docs/session-profile-format.md +39 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/yolo-mode.md +11 -10
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/child-context.ts +11 -2
- package/extensions/child-safety.ts +4 -2
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +718 -87
- package/extensions/gentle-ai.ts +991 -263
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +271 -80
- package/extensions/gentle-todo.ts +123 -18
- package/extensions/history/index.ts +11 -6
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +91 -21
- package/extensions/skill-registry.ts +175 -46
- package/extensions/startup-banner.ts +11 -2
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +40 -1
- package/lib/agents-runner.ts +90 -15
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +459 -0
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +484 -0
- package/lib/auto-permission-tool-context.ts +61 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +83 -10
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +14 -4
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +104 -28
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-shell-bundled.ts +535 -0
- package/lib/gentle-shell-launcher.ts +75 -24
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +6 -1
- package/lib/nan-provider.ts +33 -11
- package/lib/native-review-cli.ts +194 -73
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase.ts +1 -1
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/review-candidate-view-owner.ts +8 -6
- package/lib/review-candidate-view.ts +3 -2
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-risk-assessment.ts +77 -51
- package/lib/review-sidebar-state.ts +2 -1
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +22 -7
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +261 -0
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-worktree-registry.ts +74 -8
- package/lib/shell-bar.ts +41 -11
- package/lib/shell-changes-view.ts +40 -13
- package/lib/shell-changes.ts +88 -71
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-sidebar-layout.ts +5 -1
- package/lib/shell-todo.ts +81 -26
- package/lib/shell-usage.ts +4 -3
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/vim-editor-adapter.ts +27 -5
- package/lib/visual-customization-policy.ts +20 -9
- package/lib/visual-customize-view.ts +171 -20
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +198 -121
- package/package.json +5 -5
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +536 -0
- package/runtime/gentle-shell-launcher.mjs +74 -23
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +191 -70
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +77 -51
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +7 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +79 -32
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/pi-update.mjs +223 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +22 -8
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +23 -5
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +82 -18
- package/skills/gentle-ai/SKILL.md +12 -11
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +12 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-protocol.test.ts +57 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +89 -4
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +276 -0
- package/tests/auto-mode-sdk.test.ts +84 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +634 -0
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +454 -0
- package/tests/autonomous-guard.test.ts +86 -16
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +52 -1
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +34 -2
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +12 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +107 -2
- package/tests/command-palette.test.ts +5 -5
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -2
- package/tests/devbinary/native-review-parity.devtest.ts +1 -1
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +316 -0
- package/tests/generic-agent-tools.test.ts +16 -1
- package/tests/gentle-agents.test.ts +1895 -76
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +19 -2
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-installer.test.ts +548 -58
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +6 -6
- package/tests/gentle-ai.test.ts +999 -31
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +263 -19
- package/tests/gentle-shell-bundled.test.ts +1036 -0
- package/tests/gentle-shell-launcher.test.ts +251 -25
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +611 -100
- package/tests/gentle-todo.test.ts +317 -14
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/inprocess-reviewer.test.ts +20 -0
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2159 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +395 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +28 -18
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +236 -1
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +1 -1
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +14 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
- package/tests/odd-routing-contract.test.ts +80 -23
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +29 -6
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +6 -5
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/package-manifest.test.ts +105 -21
- package/tests/packed-runner-owned-path.test.ts +35 -1
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +4 -12
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/pi-update-launcher.test.ts +87 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/provider-defect-handoff.test.ts +4 -3
- package/tests/quiet-tool-rendering.test.ts +160 -3
- package/tests/rdd-aware-verification-contract.test.ts +29 -41
- package/tests/rdd-status-line.test.ts +52 -0
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/review-agent-end-preflight.test.ts +62 -10
- package/tests/review-candidate-view.test.ts +101 -12
- package/tests/review-contract-prompt.test.ts +33 -4
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +255 -1
- package/tests/review-controller-workspace-root.test.ts +43 -2
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +5 -3
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +21 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +5 -4
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-risk-assessment.test.ts +109 -33
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +83 -5
- package/tests/runtime-harness.mjs +42 -15
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +65 -5
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +394 -0
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +57 -2
- package/tests/shell-bar.test.ts +40 -0
- package/tests/shell-changes-view.test.ts +85 -34
- package/tests/shell-changes.test.ts +79 -62
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +27 -2
- package/tests/shell-todo.test.ts +132 -11
- package/tests/shell-usage-view.test.ts +1 -1
- package/tests/shell-usage.test.ts +35 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +311 -5
- package/tests/startup-banner.test.ts +117 -3
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +27 -1
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter.test.ts +47 -21
- package/tests/visual-customization-policy.test.ts +32 -3
- package/tests/visual-customize-view.test.ts +288 -10
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +18 -3
- package/tests/yolo-mode-runtime.test.ts +37 -10
- package/tests/yolo-mode.test.ts +38 -5
- package/tests/yolo-session-policy.test.ts +1 -1
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { existsSync, type FSWatcher, watch } from "node:fs";
|
|
3
4
|
import {
|
|
4
5
|
access,
|
|
6
|
+
lstat,
|
|
5
7
|
mkdir,
|
|
6
8
|
readFile,
|
|
7
9
|
readdir,
|
|
@@ -10,9 +12,12 @@ import {
|
|
|
10
12
|
writeFile,
|
|
11
13
|
} from "node:fs/promises";
|
|
12
14
|
import { homedir } from "node:os";
|
|
13
|
-
import { basename, join, normalize, relative, sep } from "node:path";
|
|
15
|
+
import { basename, delimiter, dirname, join, normalize, relative, resolve, sep } from "node:path";
|
|
14
16
|
import { fileURLToPath } from "node:url";
|
|
17
|
+
import { promisify } from "node:util";
|
|
15
18
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
19
|
+
import { isHomeOrBroadUserDirectory } from "../lib/bounded-writer-admission.ts";
|
|
20
|
+
import { resolveSessionWorktree } from "../lib/session-worktree-registry.ts";
|
|
16
21
|
|
|
17
22
|
const REGISTRY_REL_PATH = ".atl/skill-registry.md";
|
|
18
23
|
const CACHE_REL_PATH = ".atl/.skill-registry.cache.json";
|
|
@@ -311,11 +316,65 @@ interface RegenResult {
|
|
|
311
316
|
regenerated: boolean;
|
|
312
317
|
skillCount: number;
|
|
313
318
|
reason: string;
|
|
319
|
+
warning?: string;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const execFileAsync = promisify(execFile);
|
|
323
|
+
// A tracking check that cannot answer (for example a repository rooted at HOME)
|
|
324
|
+
// fails closed through the ordinary detection-failure path instead of hanging.
|
|
325
|
+
let gitTimeoutMs = 5000;
|
|
326
|
+
|
|
327
|
+
// A failed Git lookup is genuinely non-Git only when no ancestor has metadata.
|
|
328
|
+
// lstat also detects dangling redirects; permission failures remain fail-closed.
|
|
329
|
+
async function hasNoGitMetadata(cwd: string): Promise<boolean> {
|
|
330
|
+
if (process.env.GIT_DIR || process.env.GIT_WORK_TREE) return false;
|
|
331
|
+
const ceilings = (process.env.GIT_CEILING_DIRECTORIES ?? "").split(delimiter).filter(Boolean).map((path) => resolve(path));
|
|
332
|
+
let directory = resolve(cwd);
|
|
333
|
+
for (;;) {
|
|
334
|
+
// Git does not inspect ancestors at or above its discovery ceiling.
|
|
335
|
+
if (directory !== resolve(cwd) && ceilings.includes(directory)) return true;
|
|
336
|
+
try {
|
|
337
|
+
await lstat(join(directory, ".git"));
|
|
338
|
+
return false;
|
|
339
|
+
} catch (error) {
|
|
340
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") return false;
|
|
341
|
+
}
|
|
342
|
+
const parent = dirname(directory);
|
|
343
|
+
if (parent === directory) return true;
|
|
344
|
+
directory = parent;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Freshness (including forced self-heal) never grants automatic write permission.
|
|
349
|
+
async function automaticWriteWarning(
|
|
350
|
+
cwd: string,
|
|
351
|
+
targets: string[],
|
|
352
|
+
remedy = "Run /skill-registry:refresh to deliberately regenerate runtime files.",
|
|
353
|
+
): Promise<string | undefined> {
|
|
354
|
+
let tracked: string[];
|
|
355
|
+
try {
|
|
356
|
+
const { stdout } = await execFileAsync("git", ["ls-files", "--cached", "-z", "--", ...targets], {
|
|
357
|
+
cwd,
|
|
358
|
+
env: { ...process.env, LC_ALL: "C" },
|
|
359
|
+
timeout: gitTimeoutMs,
|
|
360
|
+
});
|
|
361
|
+
tracked = stdout.split("\0").filter(Boolean);
|
|
362
|
+
} catch (error) {
|
|
363
|
+
const failure = error as { code?: number; stderr?: string };
|
|
364
|
+
if (failure.code === 128 && failure.stderr?.startsWith("fatal: not a git repository") && await hasNoGitMetadata(cwd)) return undefined;
|
|
365
|
+
throw new Error(`Cannot verify Git tracking for ${targets.join(", ")}; automatic writes skipped. Resolve Git detection, or deliberately run /skill-registry:refresh.`);
|
|
366
|
+
}
|
|
367
|
+
if (tracked.length === 0) return undefined;
|
|
368
|
+
return `Skill registry automatic writes skipped for tracked targets: ${tracked.join(", ")}. ${remedy}`;
|
|
314
369
|
}
|
|
315
370
|
|
|
316
|
-
async function ensureAtlIgnored(cwd: string): Promise<
|
|
371
|
+
async function ensureAtlIgnored(cwd: string, intentional = false): Promise<string | undefined> {
|
|
317
372
|
const atlDir = join(cwd, ".atl");
|
|
318
373
|
const gitignorePath = join(atlDir, ".gitignore");
|
|
374
|
+
if (!intentional) {
|
|
375
|
+
const warning = await automaticWriteWarning(cwd, [".atl/.gitignore"]);
|
|
376
|
+
if (warning) return warning;
|
|
377
|
+
}
|
|
319
378
|
if (await pathExists(gitignorePath)) {
|
|
320
379
|
const existing = await readFile(gitignorePath, "utf8");
|
|
321
380
|
const ignoreRules = existing
|
|
@@ -355,7 +414,7 @@ async function nextLegacyDisabledPath(cwd: string): Promise<string> {
|
|
|
355
414
|
return `${base}.${Date.now()}`;
|
|
356
415
|
}
|
|
357
416
|
|
|
358
|
-
async function quarantineLegacyProjectRegistry(cwd: string): Promise<boolean> {
|
|
417
|
+
async function quarantineLegacyProjectRegistry(cwd: string, notifyWarning: (message: string) => void): Promise<boolean> {
|
|
359
418
|
const legacyPath = join(cwd, LEGACY_PROJECT_REGISTRY_REL_PATH);
|
|
360
419
|
if (!(await pathExists(legacyPath))) return false;
|
|
361
420
|
let source = "";
|
|
@@ -366,6 +425,15 @@ async function quarantineLegacyProjectRegistry(cwd: string): Promise<boolean> {
|
|
|
366
425
|
}
|
|
367
426
|
if (!isGeneratedLegacyProjectRegistry(source)) return false;
|
|
368
427
|
const disabledPath = await nextLegacyDisabledPath(cwd);
|
|
428
|
+
const warning = await automaticWriteWarning(
|
|
429
|
+
cwd,
|
|
430
|
+
[LEGACY_PROJECT_REGISTRY_REL_PATH, relative(cwd, disabledPath).split(sep).join("/")],
|
|
431
|
+
"Review and manually handle the tracked legacy extension (and any tracked quarantine destination) before retrying startup; refresh only regenerates registry runtime files.",
|
|
432
|
+
);
|
|
433
|
+
if (warning) {
|
|
434
|
+
notifyWarning(warning);
|
|
435
|
+
return false;
|
|
436
|
+
}
|
|
369
437
|
try {
|
|
370
438
|
await rename(legacyPath, disabledPath);
|
|
371
439
|
return true;
|
|
@@ -377,6 +445,7 @@ async function quarantineLegacyProjectRegistry(cwd: string): Promise<boolean> {
|
|
|
377
445
|
async function regenerateRegistry(
|
|
378
446
|
cwd: string,
|
|
379
447
|
force: boolean,
|
|
448
|
+
intentional = false,
|
|
380
449
|
): Promise<RegenResult> {
|
|
381
450
|
const existingDirs = await uniqueExistingDirs([
|
|
382
451
|
...projectSkillDirs(cwd),
|
|
@@ -401,6 +470,10 @@ async function regenerateRegistry(
|
|
|
401
470
|
if (!force && cached === fp && (await pathExists(registryPath))) {
|
|
402
471
|
return { regenerated: false, skillCount: 0, reason: "cache-hit" };
|
|
403
472
|
}
|
|
473
|
+
if (!intentional) {
|
|
474
|
+
const warning = await automaticWriteWarning(cwd, [REGISTRY_REL_PATH, CACHE_REL_PATH]);
|
|
475
|
+
if (warning) return { regenerated: false, skillCount: 0, reason: "tracked-target", warning };
|
|
476
|
+
}
|
|
404
477
|
const entries: SkillEntry[] = [];
|
|
405
478
|
for (const file of files) {
|
|
406
479
|
const entry = await loadSkill(file);
|
|
@@ -423,6 +496,17 @@ async function regenerateRegistry(
|
|
|
423
496
|
}
|
|
424
497
|
|
|
425
498
|
const watchedCwds = new Set<string>();
|
|
499
|
+
// Startup regeneration runs after session_start returns; watcher refreshes
|
|
500
|
+
// wait for it so an older startup scan never overwrites a newer refresh.
|
|
501
|
+
const pendingStartups = new Map<string, Promise<void>>();
|
|
502
|
+
let startupGeneration = 0;
|
|
503
|
+
|
|
504
|
+
// A repository rooted at HOME (or a broad user folder) makes every Git call
|
|
505
|
+
// scan the user's whole tree. gentle-ai owns the single user-facing warning.
|
|
506
|
+
function repositoryRootedAtHome(cwd: string): boolean {
|
|
507
|
+
const root = resolveSessionWorktree(cwd, cwd)?.root;
|
|
508
|
+
return root !== undefined && isHomeOrBroadUserDirectory(root);
|
|
509
|
+
}
|
|
426
510
|
|
|
427
511
|
function isTruthyEnv(value: string | undefined): boolean {
|
|
428
512
|
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
@@ -438,6 +522,9 @@ function shouldSkipSkillRegistryStartup(
|
|
|
438
522
|
env = process.env,
|
|
439
523
|
): boolean {
|
|
440
524
|
return (
|
|
525
|
+
// gentle-shell#1690: delegated children share the parent's cwd; the
|
|
526
|
+
// parent owns .atl/ writes, the legacy rename and the watcher.
|
|
527
|
+
env.GENTLE_PI_AGENTS_CHILD === "1" ||
|
|
441
528
|
pi.getFlag(NO_SKILL_REGISTRY_FLAG) === true ||
|
|
442
529
|
isTruthyEnv(env[NO_SKILL_REGISTRY_ENV]) ||
|
|
443
530
|
hasCliArg(argv, "--no-skills", "-ns")
|
|
@@ -461,7 +548,7 @@ function extensionSourcePath(source: string): string | undefined {
|
|
|
461
548
|
function shouldSkipDuplicateExtensionLoad(
|
|
462
549
|
source = import.meta.url,
|
|
463
550
|
cwd = process.cwd(),
|
|
464
|
-
state = globalThis as typeof globalThis & SkillRegistryExtensionGlobal,
|
|
551
|
+
state: SkillRegistryExtensionGlobal = globalThis as typeof globalThis & SkillRegistryExtensionGlobal,
|
|
465
552
|
): boolean {
|
|
466
553
|
const currentPath = extensionSourcePath(source);
|
|
467
554
|
const projectLocalPath = comparablePath(join(cwd, "extensions", "skill-registry.ts"));
|
|
@@ -492,7 +579,7 @@ function closeSkillRegistryWatchers(): void {
|
|
|
492
579
|
|
|
493
580
|
async function startSkillRegistryWatcher(
|
|
494
581
|
cwd: string,
|
|
495
|
-
notify: (message: string) => void,
|
|
582
|
+
notify: (message: string, level?: "info" | "warning") => void,
|
|
496
583
|
): Promise<void> {
|
|
497
584
|
if (watchedCwds.has(cwd)) return;
|
|
498
585
|
watchedCwds.add(cwd);
|
|
@@ -506,12 +593,14 @@ async function startSkillRegistryWatcher(
|
|
|
506
593
|
timer = setTimeout(() => {
|
|
507
594
|
void (async () => {
|
|
508
595
|
try {
|
|
596
|
+
await pendingStartups.get(cwd);
|
|
509
597
|
const result = await regenerateRegistry(cwd, false);
|
|
598
|
+
if (result.warning) notify(result.warning, "warning");
|
|
510
599
|
if (result.regenerated) {
|
|
511
|
-
notify(`Skill registry refreshed (${result.skillCount} skills)
|
|
600
|
+
notify(`Skill registry refreshed (${result.skillCount} skills)`, "info");
|
|
512
601
|
}
|
|
513
|
-
} catch {
|
|
514
|
-
|
|
602
|
+
} catch (error) {
|
|
603
|
+
notify(`Skill registry refresh failed: ${error instanceof Error ? error.message : String(error)}`, "warning");
|
|
515
604
|
}
|
|
516
605
|
})();
|
|
517
606
|
}, WATCH_DEBOUNCE_MS);
|
|
@@ -519,6 +608,13 @@ async function startSkillRegistryWatcher(
|
|
|
519
608
|
for (const dir of dirs) {
|
|
520
609
|
try {
|
|
521
610
|
const watcher = watch(dir, { recursive: true }, refresh);
|
|
611
|
+
watcher.on("error", (error) => {
|
|
612
|
+
// Recursive rescans can fail after watch() returns (e.g. a removed
|
|
613
|
+
// assets directory). Without this listener Node terminates Pi.
|
|
614
|
+
watcher.close();
|
|
615
|
+
activeWatchers.delete(watcher);
|
|
616
|
+
notify(`Skill registry watcher stopped: ${error.message}. Run /skill-registry:refresh to update manually.`, "warning");
|
|
617
|
+
});
|
|
522
618
|
activeWatchers.add(watcher);
|
|
523
619
|
} catch {
|
|
524
620
|
// Some filesystems do not support recursive watches; session_start/manual refresh still work.
|
|
@@ -545,12 +641,21 @@ export const __testing = {
|
|
|
545
641
|
activeWatcherCount() {
|
|
546
642
|
return activeWatchers.size;
|
|
547
643
|
},
|
|
644
|
+
async startupSettled(): Promise<void> {
|
|
645
|
+
await Promise.all(pendingStartups.values());
|
|
646
|
+
},
|
|
647
|
+
setGitTimeoutMs(ms: number): number {
|
|
648
|
+
const previous = gitTimeoutMs;
|
|
649
|
+
gitTimeoutMs = ms;
|
|
650
|
+
return previous;
|
|
651
|
+
},
|
|
548
652
|
};
|
|
549
653
|
|
|
550
654
|
export default function (pi: ExtensionAPI) {
|
|
551
655
|
if (shouldSkipDuplicateExtensionLoad()) return;
|
|
552
656
|
|
|
553
657
|
pi.on("session_shutdown", () => {
|
|
658
|
+
startupGeneration += 1;
|
|
554
659
|
closeSkillRegistryWatchers();
|
|
555
660
|
});
|
|
556
661
|
|
|
@@ -562,47 +667,71 @@ export default function (pi: ExtensionAPI) {
|
|
|
562
667
|
|
|
563
668
|
pi.on("session_start", async (_event, ctx) => {
|
|
564
669
|
if (shouldSkipSkillRegistryStartup(pi)) return;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
if (
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
)
|
|
670
|
+
if (repositoryRootedAtHome(ctx.cwd)) return;
|
|
671
|
+
const cwd = ctx.cwd;
|
|
672
|
+
const generation = startupGeneration;
|
|
673
|
+
const notifyWarning = (message: string) => {
|
|
674
|
+
if (ctx.hasUI) ctx.ui.notify(message, "warning");
|
|
675
|
+
};
|
|
676
|
+
const regenerate = async () => {
|
|
677
|
+
if (generation !== startupGeneration) return;
|
|
678
|
+
try {
|
|
679
|
+
const ignoreWarning = await ensureAtlIgnored(cwd);
|
|
680
|
+
if (ignoreWarning) notifyWarning(ignoreWarning);
|
|
681
|
+
const quarantinedLegacy = await quarantineLegacyProjectRegistry(cwd, notifyWarning);
|
|
682
|
+
const result = await regenerateRegistry(cwd, quarantinedLegacy);
|
|
683
|
+
if (result.warning) notifyWarning(result.warning);
|
|
684
|
+
if (result.regenerated && ctx.hasUI) {
|
|
685
|
+
ctx.ui.notify(
|
|
686
|
+
`Skill registry refreshed (${result.skillCount} skills)`,
|
|
687
|
+
"info",
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
if (quarantinedLegacy && ctx.hasUI) {
|
|
691
|
+
ctx.ui.notify(
|
|
692
|
+
"Disabled stale project-local skill registry extension; using package registry with project skills first.",
|
|
693
|
+
"warning",
|
|
694
|
+
);
|
|
695
|
+
}
|
|
696
|
+
if (quarantinedLegacy) {
|
|
697
|
+
setTimeout(() => {
|
|
698
|
+
if (generation !== startupGeneration) return;
|
|
699
|
+
void (async () => {
|
|
700
|
+
try {
|
|
701
|
+
const result = await regenerateRegistry(cwd, true);
|
|
702
|
+
if (result.warning) notifyWarning(result.warning);
|
|
703
|
+
} catch (error) {
|
|
704
|
+
notifyWarning(`Skill registry refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
705
|
+
}
|
|
706
|
+
})();
|
|
707
|
+
}, WATCH_DEBOUNCE_MS);
|
|
708
|
+
}
|
|
709
|
+
} catch (error) {
|
|
710
|
+
if (ctx.hasUI) {
|
|
711
|
+
const message =
|
|
712
|
+
error instanceof Error ? error.message : String(error);
|
|
713
|
+
ctx.ui.notify(
|
|
714
|
+
`Skill registry refresh failed: ${message}`,
|
|
715
|
+
"warning",
|
|
716
|
+
);
|
|
717
|
+
}
|
|
580
718
|
}
|
|
719
|
+
};
|
|
720
|
+
// Pi awaits session_start handlers one after another, so the Git tracking
|
|
721
|
+
// checks and the skill scan run after startup instead of inside it.
|
|
722
|
+
const startup = new Promise<void>((resolve) => setImmediate(resolve))
|
|
723
|
+
.then(regenerate)
|
|
724
|
+
.catch(() => { /* A replaced session's stale UI must not surface here. */ })
|
|
725
|
+
.finally(() => { if (pendingStartups.get(cwd) === startup) pendingStartups.delete(cwd); });
|
|
726
|
+
pendingStartups.set(cwd, startup);
|
|
727
|
+
try {
|
|
581
728
|
if (ctx.hasUI) {
|
|
582
|
-
await startSkillRegistryWatcher(
|
|
583
|
-
ctx.ui.notify(message,
|
|
729
|
+
await startSkillRegistryWatcher(cwd, (message, level = "info") => {
|
|
730
|
+
ctx.ui.notify(message, level);
|
|
584
731
|
});
|
|
585
732
|
}
|
|
586
|
-
if (quarantinedLegacy) {
|
|
587
|
-
setTimeout(() => {
|
|
588
|
-
void (async () => {
|
|
589
|
-
try {
|
|
590
|
-
await regenerateRegistry(ctx.cwd, true);
|
|
591
|
-
} catch {
|
|
592
|
-
// Best-effort same-session self-heal in case the stale extension already ran.
|
|
593
|
-
}
|
|
594
|
-
})();
|
|
595
|
-
}, WATCH_DEBOUNCE_MS);
|
|
596
|
-
}
|
|
597
733
|
} catch (error) {
|
|
598
|
-
|
|
599
|
-
const message =
|
|
600
|
-
error instanceof Error ? error.message : String(error);
|
|
601
|
-
ctx.ui.notify(
|
|
602
|
-
`Skill registry refresh failed: ${message}`,
|
|
603
|
-
"warning",
|
|
604
|
-
);
|
|
605
|
-
}
|
|
734
|
+
notifyWarning(`Skill registry refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
606
735
|
}
|
|
607
736
|
});
|
|
608
737
|
|
|
@@ -610,8 +739,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
610
739
|
description: "Regenerate .atl/skill-registry.md from local skill sources.",
|
|
611
740
|
handler: async (_args, ctx) => {
|
|
612
741
|
try {
|
|
613
|
-
await ensureAtlIgnored(ctx.cwd);
|
|
614
|
-
const result = await regenerateRegistry(ctx.cwd, true);
|
|
742
|
+
await ensureAtlIgnored(ctx.cwd, true);
|
|
743
|
+
const result = await regenerateRegistry(ctx.cwd, true, true);
|
|
615
744
|
ctx.ui.notify(
|
|
616
745
|
`Skill registry: ${result.skillCount} skill(s) written to ${REGISTRY_REL_PATH}`,
|
|
617
746
|
"info",
|
|
@@ -7,6 +7,7 @@ import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
|
7
7
|
import { join } from "node:path";
|
|
8
8
|
import { resolveAnimationPolicy } from "../lib/animation-policy.ts";
|
|
9
9
|
import { PI_SUBCOMMANDS } from "../lib/gentle-shell-launcher.ts";
|
|
10
|
+
import { bundledGentlePiConfigHome } from "../lib/gentle-shell-bundled.ts";
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
export type BannerColor = "pink" | "cyan" | "yellow" | "green";
|
|
@@ -67,7 +68,7 @@ function rgb(r: number, g: number, b: number, text: string): string {
|
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
function gentleAiConfigHome(): string {
|
|
70
|
-
return process.env.GENTLE_PI_CONFIG_HOME ?? join(os.homedir(), ".pi", "gentle-ai");
|
|
71
|
+
return bundledGentlePiConfigHome(process.env) ?? process.env.GENTLE_PI_CONFIG_HOME ?? join(os.homedir(), ".pi", "gentle-ai");
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
function bannerConfigPath(configHome = gentleAiConfigHome()): string {
|
|
@@ -548,12 +549,17 @@ async function countPackageExtensions(packages: unknown[]): Promise<number> {
|
|
|
548
549
|
return count;
|
|
549
550
|
}
|
|
550
551
|
|
|
551
|
-
|
|
552
|
+
// A repository rooted at HOME can make even this lookup crawl; a timeout reads
|
|
553
|
+
// as the ordinary failure instead of holding the banner stats.
|
|
554
|
+
const GIT_BRANCH_TIMEOUT_MS = 3000;
|
|
555
|
+
|
|
556
|
+
export function readGitBranch(cwd: string, run: typeof execFile = execFile, timeoutMs = GIT_BRANCH_TIMEOUT_MS): Promise<string> {
|
|
552
557
|
return new Promise((resolve) => {
|
|
553
558
|
run("git", ["-C", cwd, "branch", "--show-current"], {
|
|
554
559
|
encoding: "utf8",
|
|
555
560
|
shell: false,
|
|
556
561
|
windowsHide: true,
|
|
562
|
+
timeout: timeoutMs,
|
|
557
563
|
}, (error, stdout) => {
|
|
558
564
|
if (error) {
|
|
559
565
|
resolve("Not a git repo");
|
|
@@ -647,6 +653,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
647
653
|
pi.on("session_start", async (_event, ctx) => {
|
|
648
654
|
disposeHeader();
|
|
649
655
|
if (!ctx.hasUI) return;
|
|
656
|
+
// Delegated rpc children report hasUI=true but have no terminal to paint
|
|
657
|
+
// (gentle-shell#1690); do not rely on a piped stdout lacking rows/columns.
|
|
658
|
+
if (process.env.GENTLE_PI_AGENTS_CHILD === "1") return;
|
|
650
659
|
|
|
651
660
|
// CLI subcommands such as `pi update` or `pi install` skip the animated intro.
|
|
652
661
|
if (isPiCliSubcommandInvocation(process.argv)) return;
|
package/lib/agent-home.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { bundledGentlePiConfigHome, runningBundledPrefix } from "./gentle-shell-bundled.ts";
|
|
3
4
|
|
|
4
5
|
// Pi Subagents resolves its global directory as `PI_CODING_AGENT_DIR || ~/.pi/agent`,
|
|
5
6
|
// so an empty value must fall through here too or the two homes diverge again.
|
|
@@ -11,6 +12,14 @@ export function resolveGentlePiAgentHome(env: NodeJS.ProcessEnv = process.env):
|
|
|
11
12
|
// commands own lives here. It is defined once because the launch-time profile pin
|
|
12
13
|
// resolver reads the same profiles store the `/gentle:profiles` panel writes, and
|
|
13
14
|
// two spellings of the override would silently read two different stores.
|
|
15
|
+
// A bundled Gentle Shell keeps it inside its prefix (S9, bundledGentlePiConfigHome).
|
|
14
16
|
export function gentlePiConfigHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
15
|
-
return env.GENTLE_PI_CONFIG_HOME || join(homedir(), ".pi", "gentle-ai");
|
|
17
|
+
return bundledGentlePiConfigHome(env) ?? (env.GENTLE_PI_CONFIG_HOME || join(homedir(), ".pi", "gentle-ai"));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// The agent home Pi runs with, only in a bundled Gentle Shell (S9): there the
|
|
21
|
+
// extensions that otherwise write under ~/.pi/agent keep their files in it.
|
|
22
|
+
// Undefined elsewhere, where they keep their existing locations.
|
|
23
|
+
export function bundledGentlePiAgentHome(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
24
|
+
return runningBundledPrefix(env) === undefined ? undefined : resolveGentlePiAgentHome(env);
|
|
16
25
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { join, resolve } from "node:path";
|
|
2
|
+
import { resolveGentlePiAgentHome } from "./agent-home.ts";
|
|
3
|
+
import { resolveProfilePin } from "./agent-profile-pin.ts";
|
|
4
|
+
import {
|
|
5
|
+
discoverAgents,
|
|
6
|
+
loadAgentsConfig,
|
|
7
|
+
resolveAgentProfile,
|
|
8
|
+
withPinnedModelProfiles,
|
|
9
|
+
type AgentDefinition,
|
|
10
|
+
type DiscoveryRoots,
|
|
11
|
+
type ModelRef,
|
|
12
|
+
type ResolvedProfile,
|
|
13
|
+
} from "./agents-config.ts";
|
|
14
|
+
import type { WorktreeResolver } from "./session-worktree-registry.ts";
|
|
15
|
+
|
|
16
|
+
// gentle-shell#1731: subagent launches and the harness model-routing fact share
|
|
17
|
+
// one resolution of the effective subagent model, so the fact the orchestrator
|
|
18
|
+
// reads can never disagree with the model a launch would actually use.
|
|
19
|
+
|
|
20
|
+
export interface AgentHomeSelection {
|
|
21
|
+
env: NodeJS.ProcessEnv;
|
|
22
|
+
home: string;
|
|
23
|
+
/** An explicit agent home, kept literal. */
|
|
24
|
+
agentHome?: string;
|
|
25
|
+
/** An explicit home selects `<home>/.pi/agent` and ignores the environment. */
|
|
26
|
+
homeOverridden?: boolean;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The agent home Gentle Agents discovers definitions and `subagents.json` from. */
|
|
30
|
+
export function resolveAgentHomeDirectory(selection: AgentHomeSelection): string {
|
|
31
|
+
const selectedHome = selection.agentHome ?? (selection.homeOverridden ? join(selection.home, ".pi", "agent") : resolveGentlePiAgentHome(selection.env));
|
|
32
|
+
// Expand environment tildes like Pi, but leave explicit path APIs literal.
|
|
33
|
+
const environmentHome = selection.agentHome === undefined && !selection.homeOverridden;
|
|
34
|
+
const expandedHome = environmentHome && selectedHome === "~" ? selection.home
|
|
35
|
+
: environmentHome && (selectedHome.startsWith("~/") || (process.platform === "win32" && selectedHome.startsWith("~\\"))) ? join(selection.home, selectedHome.slice(2)) : selectedHome;
|
|
36
|
+
return resolve(expandedHome);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface AgentProfileScope {
|
|
40
|
+
roots: DiscoveryRoots;
|
|
41
|
+
/** The directory whose repository profile pin applies to the launch. */
|
|
42
|
+
pinCwd: string;
|
|
43
|
+
configHome: string;
|
|
44
|
+
resolveWorktree?: WorktreeResolver;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The launch profile for an agent: global and project `subagents.json`, with a
|
|
49
|
+
* repository profile pin replacing `modelProfiles` wholesale when one applies.
|
|
50
|
+
*/
|
|
51
|
+
export function resolvePinnedAgentProfile(agent: AgentDefinition, scope: AgentProfileScope): ResolvedProfile {
|
|
52
|
+
const config = withPinnedModelProfiles(
|
|
53
|
+
loadAgentsConfig(scope.roots),
|
|
54
|
+
resolveProfilePin({ cwd: scope.pinCwd, configHome: scope.configHome, resolveWorktree: scope.resolveWorktree })?.modelProfiles,
|
|
55
|
+
);
|
|
56
|
+
return resolveAgentProfile(agent, config);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface EffectiveAgentModel {
|
|
60
|
+
agent: string;
|
|
61
|
+
model: ModelRef | undefined;
|
|
62
|
+
/** No profile, definition, or default routes the agent: it inherits the session model. */
|
|
63
|
+
inherited: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The model a launch of `agentName` would use, falling back to the session model
|
|
68
|
+
* when nothing routes it; `undefined` when the agent is not discovered.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveEffectiveAgentModel(
|
|
71
|
+
agentName: string,
|
|
72
|
+
scope: AgentProfileScope & { fallback?: ModelRef },
|
|
73
|
+
): EffectiveAgentModel | undefined {
|
|
74
|
+
const agent = discoverAgents(scope.roots).agents.find((candidate) => candidate.name === agentName);
|
|
75
|
+
if (!agent) return undefined;
|
|
76
|
+
const profile = resolvePinnedAgentProfile(agent, scope);
|
|
77
|
+
return profile.model === undefined
|
|
78
|
+
? { agent: agentName, model: scope.fallback, inherited: true }
|
|
79
|
+
: { agent: agentName, model: profile.model, inherited: false };
|
|
80
|
+
}
|
package/lib/agent-profiles.ts
CHANGED
|
@@ -406,18 +406,28 @@ export interface ProfileListItem {
|
|
|
406
406
|
description: string;
|
|
407
407
|
}
|
|
408
408
|
|
|
409
|
+
/** One list item per saved profile, named with the markers that explain what a launch would use: the global `(active)` claim, plus `(pinned)` for the pin winner or `(session)` for this session's binding when either applies. */
|
|
409
410
|
export function buildProfileListItems(
|
|
410
411
|
file: AgentProfilesFile,
|
|
411
412
|
pinned?: string,
|
|
413
|
+
sessionBound?: string,
|
|
412
414
|
): ProfileListItem[] {
|
|
413
415
|
return Object.entries(file.profiles).map(([name, config]) => {
|
|
414
416
|
const roles = profileRoleEntries(config).length;
|
|
415
417
|
const active = name === file.active ? `${name} (active)` : name;
|
|
418
|
+
// A pinned profile is the one this repository launches with, which is not the
|
|
419
|
+
// same thing as the globally active profile, so both are named. A session
|
|
420
|
+
// binding outranks the pin in this list (slice 1 stores it; slice 2,
|
|
421
|
+
// gentle-shell#1558, makes launches resolve it), so its marker wins on the
|
|
422
|
+
// same profile.
|
|
423
|
+
const label = name === sessionBound
|
|
424
|
+
? `${active} (session)`
|
|
425
|
+
: name === pinned
|
|
426
|
+
? `${active} (pinned)`
|
|
427
|
+
: active;
|
|
416
428
|
return {
|
|
417
429
|
id: name,
|
|
418
|
-
|
|
419
|
-
// same thing as the globally active profile, so both are named.
|
|
420
|
-
label: name === pinned ? `${active} (pinned)` : active,
|
|
430
|
+
label,
|
|
421
431
|
description: `${roles} ${roles === 1 ? "role" : "roles"}`,
|
|
422
432
|
};
|
|
423
433
|
});
|
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
// behavior is unit-testable against a fake clock.
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* A completion older than this at flush time is stale:
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* A completion older than this at flush time is stale: replaying its full
|
|
15
|
+
* report into the model context could re-enter state the conversation may
|
|
16
|
+
* already have used, so the parent gets a compact notice instead. The parent
|
|
17
|
+
* is told never to poll, so a stale completion must still announce itself.
|
|
17
18
|
* Ninety seconds comfortably covers one slow orchestrator turn (model latency
|
|
18
19
|
* plus a few tool calls) while staying orders of magnitude below the 50-58
|
|
19
20
|
* minute followUp delays measured in issue #867.
|
|
@@ -36,6 +37,8 @@ export interface CompletionQueue<T extends { id: string }> {
|
|
|
36
37
|
consume(taskId: string): void;
|
|
37
38
|
/** Remove and return the still-pending completions in settle order, each with its staleness decision at `now`. Consumed entries are dropped and never returned. */
|
|
38
39
|
takeDeliverable(now: number): Array<DeliverableCompletion<T>>;
|
|
40
|
+
/** Return a taken completion whose forwarding failed to the pending queue, so a later flush retries it. A consumed id stays dropped. */
|
|
41
|
+
requeue(entry: DeliverableCompletion<T>): void;
|
|
39
42
|
/** Discard everything: pending completions and consumed/delivered bookkeeping. Used on session start and shutdown so nothing replays after a resume. */
|
|
40
43
|
dropAll(): void;
|
|
41
44
|
}
|
|
@@ -63,6 +66,11 @@ export function createCompletionQueue<T extends { id: string }>(): CompletionQue
|
|
|
63
66
|
}
|
|
64
67
|
return deliverable;
|
|
65
68
|
},
|
|
69
|
+
requeue(entry) {
|
|
70
|
+
delivered.delete(entry.task.id);
|
|
71
|
+
if (consumed.has(entry.task.id) || pending.some((pendingEntry) => pendingEntry.task.id === entry.task.id)) return;
|
|
72
|
+
pending.push({ task: entry.task, settledAt: entry.settledAt });
|
|
73
|
+
},
|
|
66
74
|
dropAll() {
|
|
67
75
|
pending = [];
|
|
68
76
|
consumed.clear();
|
package/lib/agents-messaging.ts
CHANGED
|
@@ -95,7 +95,7 @@ function parseAck(value: unknown): AckFrame | undefined {
|
|
|
95
95
|
if (!record(value) || !exact(value, ["id", "kind", "accepted", "error"]) || !validChildId(value.id) || value.kind !== "ack" || typeof value.accepted !== "boolean") return undefined;
|
|
96
96
|
if (value.accepted && value.error !== undefined) return undefined;
|
|
97
97
|
if (value.error !== undefined && !boundedText(value.error, CHILD_ACK_ERROR_MAX_BYTES)) return undefined;
|
|
98
|
-
return { id: value.id, accepted: value.accepted, ...(value.error ===
|
|
98
|
+
return { id: value.id, accepted: value.accepted, ...(typeof value.error === "string" ? { error: value.error } : {}) };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
function parseReply(value: unknown): ReplyFrame | undefined {
|
package/lib/agents-protocol.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { sanitizeTerminalText } from "./terminal-theme.ts";
|
|
3
3
|
|
|
4
4
|
// Gentle Agents protocol. A child pi process streams RPC events; the host
|
|
@@ -148,10 +148,23 @@ export interface TaskSummary {
|
|
|
148
148
|
|
|
149
149
|
export type TaskListener = (task: TaskRecord, thread: TaskThread) => void;
|
|
150
150
|
export type SummaryListener = (summary: TaskSummary) => void;
|
|
151
|
+
export interface TaskStatusChange {
|
|
152
|
+
readonly producerId: string;
|
|
153
|
+
readonly sessionId: string;
|
|
154
|
+
readonly parentSessionId: string;
|
|
155
|
+
readonly taskId: string;
|
|
156
|
+
readonly runId: string;
|
|
157
|
+
readonly status: TaskStatus;
|
|
158
|
+
readonly previousStatus?: TaskStatus;
|
|
159
|
+
readonly sequence: number;
|
|
160
|
+
}
|
|
161
|
+
export type StatusChangeListener = (change: TaskStatusChange) => void | Promise<void>;
|
|
151
162
|
|
|
152
163
|
const DEFAULT_LIMITS: ThreadLimits = { maxItems: 400, maxOutputChars: 16_000 };
|
|
153
164
|
/** Child UI requests that block on an answer; everything else (notify, setStatus, setWidget) is noise here. */
|
|
154
165
|
export const DIALOG_METHODS: ReadonlySet<string> = new Set(["select", "confirm", "input", "editor"]);
|
|
166
|
+
/** The one child notify that reaches the task thread: requested --tools the child does not have (#1690). */
|
|
167
|
+
export const MISSING_TOOLS_NOTE_PREFIX = "gentle-agents: requested tools missing in child:";
|
|
155
168
|
const LABEL_MAX = 72;
|
|
156
169
|
const TEXT_CAP = 20_000;
|
|
157
170
|
const ELLIPSIS = "…";
|
|
@@ -330,6 +343,7 @@ export function normalizeRpcEvent(raw: unknown, options: { observeResponses?: bo
|
|
|
330
343
|
case "agent_settled":
|
|
331
344
|
return [{ type: TASK_EVENT.AGENT_SETTLED }];
|
|
332
345
|
case "extension_ui_request":
|
|
346
|
+
if (event.method === "notify" && typeof event.message === "string" && event.message.startsWith(MISSING_TOOLS_NOTE_PREFIX)) return [{ type: TASK_EVENT.NOTE, text: clean(event.message) }];
|
|
333
347
|
return DIALOG_METHODS.has(String(event.method)) ? [{ type: TASK_EVENT.ASK, request: askRequest(event) }] : [];
|
|
334
348
|
case "auto_retry_start":
|
|
335
349
|
return [{ type: TASK_EVENT.NOTE, text: `retrying (${String(event.attempt ?? "?")}/${String(event.maxAttempts ?? "?")})` }];
|
|
@@ -460,14 +474,21 @@ export class TaskStore {
|
|
|
460
474
|
private readonly listeners = new Map<string, Set<TaskListener>>();
|
|
461
475
|
private readonly summaryListeners = new Set<SummaryListener>();
|
|
462
476
|
private readonly limits: Partial<ThreadLimits>;
|
|
477
|
+
private readonly producerId = randomUUID();
|
|
478
|
+
private sequence = 0;
|
|
479
|
+
private readonly runIds = new Map<string, string>();
|
|
480
|
+
private readonly statusListeners = new Set<StatusChangeListener>();
|
|
463
481
|
|
|
464
482
|
constructor(limits: Partial<ThreadLimits> = {}) {
|
|
465
483
|
this.limits = limits;
|
|
466
484
|
}
|
|
467
485
|
|
|
468
486
|
add(task: TaskRecord): void {
|
|
487
|
+
const previous = this.tasks.get(task.id)?.status;
|
|
488
|
+
if (!this.tasks.has(task.id)) this.runIds.set(task.id, randomUUID());
|
|
469
489
|
this.tasks.set(task.id, task);
|
|
470
490
|
this.threads.set(task.id, emptyThread(this.limits));
|
|
491
|
+
this.notifyStatus(task, previous);
|
|
471
492
|
this.notifySummary();
|
|
472
493
|
}
|
|
473
494
|
|
|
@@ -476,6 +497,7 @@ export class TaskStore {
|
|
|
476
497
|
restore(task: TaskRecord, thread: TaskThread): boolean {
|
|
477
498
|
if (this.tasks.has(task.id)) return false;
|
|
478
499
|
this.tasks.set(task.id, task);
|
|
500
|
+
this.runIds.set(task.id, randomUUID());
|
|
479
501
|
this.threads.set(task.id, { ...thread, limits: { ...DEFAULT_LIMITS, ...this.limits } });
|
|
480
502
|
this.notifySummary();
|
|
481
503
|
return true;
|
|
@@ -506,6 +528,7 @@ export class TaskStore {
|
|
|
506
528
|
if (!current) return undefined;
|
|
507
529
|
const next = { ...current, ...patch };
|
|
508
530
|
this.tasks.set(id, next);
|
|
531
|
+
this.notifyStatus(next, current.status);
|
|
509
532
|
this.notifyTask(next);
|
|
510
533
|
if (next.status !== current.status) this.notifySummary();
|
|
511
534
|
return next;
|
|
@@ -533,6 +556,22 @@ export class TaskStore {
|
|
|
533
556
|
return () => this.summaryListeners.delete(listener);
|
|
534
557
|
}
|
|
535
558
|
|
|
559
|
+
/** Optional, content-free observer. Restores and unchanged snapshots are silent. */
|
|
560
|
+
subscribeStatusChanges(listener: StatusChangeListener): () => void {
|
|
561
|
+
this.statusListeners.add(listener);
|
|
562
|
+
return () => { this.statusListeners.delete(listener); };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
private notifyStatus(task: TaskRecord, previousStatus?: TaskStatus): void {
|
|
566
|
+
if (task.status === previousStatus) return;
|
|
567
|
+
const change: TaskStatusChange = Object.freeze({ producerId: this.producerId,
|
|
568
|
+
sessionId: task.parentSessionId, parentSessionId: task.parentSessionId, taskId: task.id,
|
|
569
|
+
runId: this.runIds.get(task.id)!, status: task.status, previousStatus, sequence: ++this.sequence });
|
|
570
|
+
for (const listener of [...this.statusListeners]) {
|
|
571
|
+
try { void Promise.resolve(listener(change)).catch(() => {}); } catch { /* Observability cannot fail a mutation. */ }
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
536
575
|
private notifyTask(task: TaskRecord): void {
|
|
537
576
|
const set = this.listeners.get(task.id);
|
|
538
577
|
if (!set) return;
|