gentle-pi 4.0.0 → 4.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +157 -18
- package/assets/agents/gentle-ai-explore.md +11 -4
- package/assets/agents/gentle-ai-verify.md +23 -4
- package/assets/agents/gentle-ai-worker.md +6 -6
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +70 -163
- package/assets/orchestrator-memory.md +11 -4
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +29 -25
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +708 -22
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/auto-permission-answers.md +25 -0
- package/docs/auto-permission-evidence.md +39 -0
- package/docs/bundled-install.md +399 -0
- package/docs/delegated-verification.md +3 -5
- package/docs/gentle-agents-activity.md +513 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +90 -33
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +1 -1
- package/docs/permission-modes.md +127 -0
- package/docs/prompt-history.md +1 -1
- package/docs/readme-reference.md +310 -47
- package/docs/render-cache-performance.md +28 -0
- package/docs/session-profile-format.md +39 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/yolo-mode.md +11 -10
- package/extensions/ask-user-choice.ts +4 -0
- package/extensions/ask-user-question.ts +6 -1
- package/extensions/child-context.ts +11 -2
- package/extensions/child-safety.ts +4 -2
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +718 -87
- package/extensions/gentle-ai.ts +991 -263
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +271 -80
- package/extensions/gentle-todo.ts +123 -18
- package/extensions/history/index.ts +11 -6
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +91 -21
- package/extensions/skill-registry.ts +175 -46
- package/extensions/startup-banner.ts +11 -2
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +40 -1
- package/lib/agents-runner.ts +90 -15
- package/lib/auto-permission-answers.ts +133 -0
- package/lib/auto-permission-classifier.ts +459 -0
- package/lib/auto-permission-evidence.ts +88 -0
- package/lib/auto-permission-memory.ts +69 -0
- package/lib/auto-permission-runtime.ts +484 -0
- package/lib/auto-permission-tool-context.ts +61 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +83 -10
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +14 -4
- package/lib/command-palette-catalog.ts +3 -2
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +104 -28
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-shell-bundled.ts +535 -0
- package/lib/gentle-shell-launcher.ts +75 -24
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +6 -1
- package/lib/nan-provider.ts +33 -11
- package/lib/native-review-cli.ts +194 -73
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase.ts +1 -1
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/review-candidate-view-owner.ts +8 -6
- package/lib/review-candidate-view.ts +3 -2
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-risk-assessment.ts +77 -51
- package/lib/review-sidebar-state.ts +2 -1
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +22 -7
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +261 -0
- package/lib/session-profile-persistence.ts +187 -0
- package/lib/session-worktree-registry.ts +74 -8
- package/lib/shell-bar.ts +41 -11
- package/lib/shell-changes-view.ts +40 -13
- package/lib/shell-changes.ts +88 -71
- package/lib/shell-prompt.ts +6 -4
- package/lib/shell-sidebar-layout.ts +5 -1
- package/lib/shell-todo.ts +81 -26
- package/lib/shell-usage.ts +4 -3
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/vim-editor-adapter.ts +27 -5
- package/lib/visual-customization-policy.ts +20 -9
- package/lib/visual-customize-view.ts +171 -20
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +198 -121
- package/package.json +5 -5
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +536 -0
- package/runtime/gentle-shell-launcher.mjs +74 -23
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +191 -70
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +77 -51
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +7 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +79 -32
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/pi-update.mjs +223 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +22 -8
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +23 -5
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +82 -18
- package/skills/gentle-ai/SKILL.md +12 -11
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +12 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-protocol.test.ts +57 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +89 -4
- package/tests/append-system-prompt-route.test.ts +18 -1
- package/tests/auto-mode-human-answers.test.ts +68 -0
- package/tests/auto-mode-invariants.test.ts +276 -0
- package/tests/auto-mode-sdk.test.ts +84 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-answers.test.ts +170 -0
- package/tests/auto-permission-classifier.test.ts +634 -0
- package/tests/auto-permission-compound.test.ts +24 -0
- package/tests/auto-permission-evidence.test.ts +88 -0
- package/tests/auto-permission-memory.test.ts +46 -0
- package/tests/auto-permission-public-regressions.test.ts +111 -0
- package/tests/auto-permission-runtime.test.ts +454 -0
- package/tests/autonomous-guard.test.ts +86 -16
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +52 -1
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +34 -2
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +12 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +107 -2
- package/tests/command-palette.test.ts +5 -5
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -2
- package/tests/devbinary/native-review-parity.devtest.ts +1 -1
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +316 -0
- package/tests/generic-agent-tools.test.ts +16 -1
- package/tests/gentle-agents.test.ts +1895 -76
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +19 -2
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-installer.test.ts +548 -58
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +6 -6
- package/tests/gentle-ai.test.ts +999 -31
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +263 -19
- package/tests/gentle-shell-bundled.test.ts +1036 -0
- package/tests/gentle-shell-launcher.test.ts +251 -25
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +611 -100
- package/tests/gentle-todo.test.ts +317 -14
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/inprocess-reviewer.test.ts +20 -0
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2159 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +395 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +28 -18
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +7 -1
- package/tests/native-review-cli.test.ts +236 -1
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +1 -1
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +14 -5
- package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
- package/tests/odd-routing-contract.test.ts +80 -23
- package/tests/odd-runtime-delegation-gate.test.ts +4 -1
- package/tests/orchestrator-budget.test.ts +29 -6
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +6 -5
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/package-manifest.test.ts +105 -21
- package/tests/packed-runner-owned-path.test.ts +35 -1
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +4 -12
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/pi-update-launcher.test.ts +87 -0
- package/tests/pi-update.test.ts +186 -0
- package/tests/provider-defect-handoff.test.ts +4 -3
- package/tests/quiet-tool-rendering.test.ts +160 -3
- package/tests/rdd-aware-verification-contract.test.ts +29 -41
- package/tests/rdd-status-line.test.ts +52 -0
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/review-agent-end-preflight.test.ts +62 -10
- package/tests/review-candidate-view.test.ts +101 -12
- package/tests/review-contract-prompt.test.ts +33 -4
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +255 -1
- package/tests/review-controller-workspace-root.test.ts +43 -2
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +5 -3
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +21 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +5 -4
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-risk-assessment.test.ts +109 -33
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +83 -5
- package/tests/runtime-harness.mjs +42 -15
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +65 -5
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +394 -0
- package/tests/session-profile-persistence.test.ts +334 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +57 -2
- package/tests/shell-bar.test.ts +40 -0
- package/tests/shell-changes-view.test.ts +85 -34
- package/tests/shell-changes.test.ts +79 -62
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +27 -2
- package/tests/shell-todo.test.ts +132 -11
- package/tests/shell-usage-view.test.ts +1 -1
- package/tests/shell-usage.test.ts +35 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +311 -5
- package/tests/startup-banner.test.ts +117 -3
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +27 -1
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter.test.ts +47 -21
- package/tests/visual-customization-policy.test.ts +32 -3
- package/tests/visual-customize-view.test.ts +288 -10
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +18 -3
- package/tests/yolo-mode-runtime.test.ts +37 -10
- package/tests/yolo-mode.test.ts +38 -5
- package/tests/yolo-session-policy.test.ts +1 -1
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import test, { after } from "node:test";
|
|
6
|
+
import { resolveAgentHomeDirectory, resolveEffectiveAgentModel, resolvePinnedAgentProfile } from "../lib/agent-model-resolution.ts";
|
|
7
|
+
import type { AgentDefinition } from "../lib/agents-config.ts";
|
|
8
|
+
|
|
9
|
+
// gentle-shell#1731 S3/AC3: the subagent launch and the harness model-routing
|
|
10
|
+
// fact resolve the effective subagent model through one helper, so the fact
|
|
11
|
+
// can never disagree with the model a launch would actually use.
|
|
12
|
+
|
|
13
|
+
const root = mkdtempSync(join(tmpdir(), "gentle-pi-agent-model-resolution-"));
|
|
14
|
+
after(() => rmSync(root, { recursive: true, force: true }));
|
|
15
|
+
|
|
16
|
+
function fixture(name: string) {
|
|
17
|
+
const base = join(root, name);
|
|
18
|
+
const home = join(base, "home");
|
|
19
|
+
const agentHome = join(base, "agent-home");
|
|
20
|
+
const cwd = join(base, "project");
|
|
21
|
+
const commonDir = join(base, "git-common");
|
|
22
|
+
const configHome = join(base, "config");
|
|
23
|
+
for (const dir of [home, join(agentHome, "agents"), cwd, commonDir, configHome]) mkdirSync(dir, { recursive: true });
|
|
24
|
+
return {
|
|
25
|
+
roots: { cwd, home, agentHome },
|
|
26
|
+
cwd,
|
|
27
|
+
configHome,
|
|
28
|
+
resolveWorktree: () => ({ root: cwd, commonDir }),
|
|
29
|
+
writeAgent(agentName: string, model?: string) {
|
|
30
|
+
writeFileSync(join(agentHome, "agents", `${agentName}.md`), `---\nname: ${agentName}\ndescription: Test agent.\n${model === undefined ? "" : `model: ${model}\n`}---\nBody.\n`);
|
|
31
|
+
},
|
|
32
|
+
writeSubagents(config: Record<string, unknown>) {
|
|
33
|
+
writeFileSync(join(agentHome, "subagents.json"), JSON.stringify(config));
|
|
34
|
+
},
|
|
35
|
+
writePinnedProfile(profiles: Record<string, unknown>, pin: string) {
|
|
36
|
+
writeFileSync(join(configHome, "profiles.json"), JSON.stringify({ kind: "gentle-pi.agent_model_profiles", version: 1, profiles }));
|
|
37
|
+
mkdirSync(join(commonDir, "gentle-ai"), { recursive: true });
|
|
38
|
+
writeFileSync(join(commonDir, "gentle-ai", "profile-pin.json"), JSON.stringify({ kind: "gentle-pi.agent_model_profile_pin", version: 1, profile: pin }));
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const orchestrator = { provider: "anthropic", id: "fable" };
|
|
44
|
+
|
|
45
|
+
test("the effective worker model comes from the global subagent profile", () => {
|
|
46
|
+
const f = fixture("global-profile");
|
|
47
|
+
f.writeAgent("gentle-ai-worker");
|
|
48
|
+
f.writeSubagents({ model_profiles: { "gentle-ai-worker": { model: "anthropic/opus" } } });
|
|
49
|
+
assert.deepEqual(
|
|
50
|
+
resolveEffectiveAgentModel("gentle-ai-worker", { roots: f.roots, pinCwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree, fallback: orchestrator }),
|
|
51
|
+
{ agent: "gentle-ai-worker", model: { provider: "anthropic", id: "opus" }, inherited: false },
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("a repository profile pin replaces global routing for the effective worker model", () => {
|
|
56
|
+
const f = fixture("pinned-profile");
|
|
57
|
+
f.writeAgent("gentle-ai-worker");
|
|
58
|
+
f.writeSubagents({ model_profiles: { "gentle-ai-worker": { model: "anthropic/opus" } } });
|
|
59
|
+
f.writePinnedProfile({ cheap: { "gentle-ai-worker": { model: "openai/mini" } } }, "cheap");
|
|
60
|
+
assert.deepEqual(
|
|
61
|
+
resolveEffectiveAgentModel("gentle-ai-worker", { roots: f.roots, pinCwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree, fallback: orchestrator })?.model,
|
|
62
|
+
{ provider: "openai", id: "mini" },
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("the agent definition model applies when no profile routes the agent", () => {
|
|
67
|
+
const f = fixture("definition-model");
|
|
68
|
+
f.writeAgent("gentle-ai-worker", "openai/defined");
|
|
69
|
+
assert.deepEqual(
|
|
70
|
+
resolveEffectiveAgentModel("gentle-ai-worker", { roots: f.roots, pinCwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree, fallback: orchestrator })?.model,
|
|
71
|
+
{ provider: "openai", id: "defined" },
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("an unrouted agent inherits the fallback session model", () => {
|
|
76
|
+
const f = fixture("inherit");
|
|
77
|
+
f.writeAgent("gentle-ai-worker");
|
|
78
|
+
assert.deepEqual(
|
|
79
|
+
resolveEffectiveAgentModel("gentle-ai-worker", { roots: f.roots, pinCwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree, fallback: orchestrator }),
|
|
80
|
+
{ agent: "gentle-ai-worker", model: orchestrator, inherited: true },
|
|
81
|
+
);
|
|
82
|
+
assert.deepEqual(
|
|
83
|
+
resolveEffectiveAgentModel("gentle-ai-worker", { roots: f.roots, pinCwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree }),
|
|
84
|
+
{ agent: "gentle-ai-worker", model: undefined, inherited: true },
|
|
85
|
+
);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("an undiscovered agent resolves to undefined", () => {
|
|
89
|
+
const f = fixture("missing-agent");
|
|
90
|
+
assert.equal(
|
|
91
|
+
resolveEffectiveAgentModel("gentle-ai-worker", { roots: f.roots, pinCwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree, fallback: orchestrator }),
|
|
92
|
+
undefined,
|
|
93
|
+
);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("resolvePinnedAgentProfile returns the launch profile with its model and thinking", () => {
|
|
97
|
+
const f = fixture("launch-profile");
|
|
98
|
+
f.writePinnedProfile({ deep: { explore: { model: "openai/alpha", thinking: "minimal" } } }, "deep");
|
|
99
|
+
const agent: AgentDefinition = { name: "explore", description: "", filePath: "", scope: "global", instructions: "", model: undefined, thinking: undefined, mode: undefined, tools: [] };
|
|
100
|
+
const profile = resolvePinnedAgentProfile(agent, { roots: f.roots, pinCwd: f.cwd, configHome: f.configHome, resolveWorktree: f.resolveWorktree });
|
|
101
|
+
assert.deepEqual(profile.model, { provider: "openai", id: "alpha" });
|
|
102
|
+
assert.equal(profile.thinking, "minimal");
|
|
103
|
+
assert.equal(profile.source.model, "profile");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("resolveAgentHomeDirectory follows the environment, expands tildes, and keeps explicit overrides literal", () => {
|
|
107
|
+
const home = "/home/tester";
|
|
108
|
+
assert.equal(resolveAgentHomeDirectory({ env: { GENTLE_PI_AGENT_HOME: "/opt/agents" }, home }), resolve("/opt/agents"));
|
|
109
|
+
assert.equal(resolveAgentHomeDirectory({ env: { PI_CODING_AGENT_DIR: "~/pi-agent" }, home }), resolve(join(home, "pi-agent")));
|
|
110
|
+
assert.equal(resolveAgentHomeDirectory({ env: { GENTLE_PI_AGENT_HOME: "~" }, home }), resolve(home));
|
|
111
|
+
assert.equal(resolveAgentHomeDirectory({ env: { GENTLE_PI_AGENT_HOME: "/ignored" }, home, homeOverridden: true }), resolve(join(home, ".pi", "agent")));
|
|
112
|
+
assert.equal(resolveAgentHomeDirectory({ env: {}, home, agentHome: "/explicit" }), resolve("/explicit"));
|
|
113
|
+
});
|
|
@@ -767,3 +767,15 @@ test("formatOrchestratorSelection separates set, inherit, and inherit-with-think
|
|
|
767
767
|
"nan/glm5.3 · max",
|
|
768
768
|
);
|
|
769
769
|
});
|
|
770
|
+
|
|
771
|
+
test("buildProfileListItems marks the session-bound profile ahead of the pinned one", () => {
|
|
772
|
+
const file = createProfile(createProfile(emptyProfilesFile(), "work", { worker: { model: "zai/glm-4.7" } }), "personal", { solo: { model: "openai/o4-mini" } });
|
|
773
|
+
const items = buildProfileListItems(file, "work", "personal");
|
|
774
|
+
assert.equal(items.find((item) => item.id === "personal")?.label, "personal (session)");
|
|
775
|
+
assert.equal(items.find((item) => item.id === "work")?.label, "work (pinned)");
|
|
776
|
+
assert.equal(items.find((item) => item.id === "personal")?.description, "1 role");
|
|
777
|
+
const both = buildProfileListItems(file, "work", "work");
|
|
778
|
+
assert.equal(both.find((item) => item.id === "work")?.label, "work (session)", "the session marker outranks the pin marker on the same profile");
|
|
779
|
+
const unbound = buildProfileListItems(file, "work");
|
|
780
|
+
assert.equal(unbound.find((item) => item.id === "personal")?.label, "personal", "no session marker without a binding");
|
|
781
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
|
|
5
|
+
import { NATIVE_REVIEW_MODE_SOURCE, type NativeReviewCli } from "../lib/native-review-cli.ts";
|
|
6
|
+
import {
|
|
7
|
+
decodeAgentRiskEscalation,
|
|
8
|
+
escalatedRisk,
|
|
9
|
+
HIGH_RISK_ITEMS,
|
|
10
|
+
REVIEW_ASSESSMENT_SCHEMA,
|
|
11
|
+
VERIFICATION_TIER,
|
|
12
|
+
} from "../lib/review-risk-assessment.ts";
|
|
13
|
+
|
|
14
|
+
// gentle-shell#1494 S6: the agent may raise a candidate's risk to high by
|
|
15
|
+
// citing one item of the shared high-risk list, never lower it. The raise
|
|
16
|
+
// travels inside the existing assess call; no new operation.
|
|
17
|
+
|
|
18
|
+
test("the high-risk list has exactly the six items of the always-on Task Size section", () => {
|
|
19
|
+
assert.deepEqual(Object.keys(HIGH_RISK_ITEMS).map(Number), [1, 2, 3, 4, 5, 6]);
|
|
20
|
+
assert.equal(HIGH_RISK_ITEMS[2], "security");
|
|
21
|
+
assert.equal(HIGH_RISK_ITEMS[6], "no test would catch a regression");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("decodeAgentRiskEscalation accepts an item from 1 to 6 with a non-empty reason", () => {
|
|
25
|
+
assert.deepEqual(decodeAgentRiskEscalation({ item: 2, reason: "weakens the permission check in utils.ts" }), {
|
|
26
|
+
item: 2,
|
|
27
|
+
reason: "weakens the permission check in utils.ts",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("decodeAgentRiskEscalation rejects unknown items, empty or oversized reasons, and extra fields", () => {
|
|
32
|
+
for (const bad of [
|
|
33
|
+
{ item: 0, reason: "x" },
|
|
34
|
+
{ item: 7, reason: "x" },
|
|
35
|
+
{ item: 2.5, reason: "x" },
|
|
36
|
+
{ item: "2", reason: "x" },
|
|
37
|
+
{ item: 2, reason: " " },
|
|
38
|
+
{ item: 2, reason: "x".repeat(501) },
|
|
39
|
+
{ item: 2, reason: "x", tier: "low" },
|
|
40
|
+
{ item: 2 },
|
|
41
|
+
null,
|
|
42
|
+
"high",
|
|
43
|
+
]) {
|
|
44
|
+
assert.throws(() => decodeAgentRiskEscalation(bad), /escalate/, `must reject ${JSON.stringify(bad)}`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("escalatedRisk raises passive and medium to high and never lowers", () => {
|
|
49
|
+
const escalation = { item: 3 as const, reason: "changes a published export" };
|
|
50
|
+
assert.equal(escalatedRisk(VERIFICATION_TIER.PASSIVE, escalation), VERIFICATION_TIER.HIGH);
|
|
51
|
+
assert.equal(escalatedRisk(VERIFICATION_TIER.MEDIUM, escalation), VERIFICATION_TIER.HIGH);
|
|
52
|
+
assert.equal(escalatedRisk(VERIFICATION_TIER.HIGH, escalation), VERIFICATION_TIER.HIGH);
|
|
53
|
+
assert.equal(escalatedRisk(VERIFICATION_TIER.UNASSESSABLE, escalation), VERIFICATION_TIER.UNASSESSABLE);
|
|
54
|
+
for (const tier of Object.values(VERIFICATION_TIER)) assert.equal(escalatedRisk(tier, undefined), tier);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
function assessTool(): { execute: (id: string, params: Record<string, unknown>, signal: AbortSignal | undefined, onUpdate: undefined, ctx: ExtensionContext) => Promise<{ details: unknown }> } {
|
|
58
|
+
const tools = new Map<string, any>();
|
|
59
|
+
const pi = { on() {}, registerCommand() {}, registerTool(tool: { name: string }) { tools.set(tool.name, tool); } } as unknown as ExtensionAPI;
|
|
60
|
+
const nativeReviewCli: Partial<NativeReviewCli> = {
|
|
61
|
+
reviewMode: async () => ({ operation: "status", scope: "clone", status: { global: "off", cloneLocal: "off", effective: "off", source: NATIVE_REVIEW_MODE_SOURCE.GLOBAL } }),
|
|
62
|
+
assess: async () => ({
|
|
63
|
+
schema: REVIEW_ASSESSMENT_SCHEMA,
|
|
64
|
+
risk: "medium",
|
|
65
|
+
reasons: [],
|
|
66
|
+
changedPaths: 1,
|
|
67
|
+
changedLines: 3,
|
|
68
|
+
candidate: { kind: "current-changes", baseRef: undefined },
|
|
69
|
+
}),
|
|
70
|
+
};
|
|
71
|
+
createGentleAiExtension({ nativeReviewCli: nativeReviewCli as NativeReviewCli })(pi);
|
|
72
|
+
return tools.get("gentle_review");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const ctx = { cwd: process.cwd() } as ExtensionContext;
|
|
76
|
+
|
|
77
|
+
test("gentle_review assess: an agent escalation raises a medium candidate to high and records why", async () => {
|
|
78
|
+
const tool = assessTool();
|
|
79
|
+
const input = JSON.stringify({ writerModelId: "openai/gpt-6.1-sol", writerEffort: "high", escalate: { item: 2, reason: "weakens the permission check in utils.ts" } });
|
|
80
|
+
const { details } = await tool.execute("esc-1", { operation: "assess", input }, undefined, undefined, ctx);
|
|
81
|
+
const plan = details as { risk: string; nativeRisk: string; agentEscalation: { item: number; label: string; reason: string }; plan: { independentVerifier: boolean } };
|
|
82
|
+
assert.equal(plan.nativeRisk, "medium");
|
|
83
|
+
assert.equal(plan.risk, VERIFICATION_TIER.HIGH);
|
|
84
|
+
assert.deepEqual(plan.agentEscalation, { item: 2, label: "security", reason: "weakens the permission check in utils.ts", applied: true });
|
|
85
|
+
assert.equal(plan.plan.independentVerifier, true, "a raised candidate always gets the independent verifier");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("gentle_review assess: without an escalation the native tier stands", async () => {
|
|
89
|
+
const tool = assessTool();
|
|
90
|
+
const input = JSON.stringify({ writerModelId: "openai/gpt-6.1-sol", writerEffort: "high" });
|
|
91
|
+
const { details } = await tool.execute("esc-2", { operation: "assess", input }, undefined, undefined, ctx);
|
|
92
|
+
const plan = details as { risk: string; nativeRisk: string; agentEscalation?: unknown; plan: { independentVerifier: boolean } };
|
|
93
|
+
assert.equal(plan.risk, "medium");
|
|
94
|
+
assert.equal(plan.nativeRisk, "medium");
|
|
95
|
+
assert.equal(plan.agentEscalation, undefined);
|
|
96
|
+
assert.equal(plan.plan.independentVerifier, false);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("gentle_review assess: a malformed escalation is rejected instead of ignored", async () => {
|
|
100
|
+
const tool = assessTool();
|
|
101
|
+
await assert.rejects(
|
|
102
|
+
() => tool.execute("esc-3", { operation: "assess", input: JSON.stringify({ escalate: { item: 9, reason: "x" } }) }, undefined, undefined, ctx),
|
|
103
|
+
/escalate/,
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("the rule text tells the agent when and how to escalate, and that it never lowers", async () => {
|
|
108
|
+
const { readFileSync } = await import("node:fs");
|
|
109
|
+
const verification = readFileSync(new URL("../assets/orchestrator-verification.md", import.meta.url), "utf8");
|
|
110
|
+
const tracking = readFileSync(new URL("../assets/orchestrator-tracking.md", import.meta.url), "utf8");
|
|
111
|
+
const extension = readFileSync(new URL("../extensions/gentle-ai.ts", import.meta.url), "utf8");
|
|
112
|
+
assert.match(verification, /"escalate": \{"item": <1-6>, "reason": "<one line>"\}/);
|
|
113
|
+
assert.match(verification, /never lowers a tier/);
|
|
114
|
+
assert.match(tracking, /assess `escalate` field \(`orchestrator-verification\.md`\); never lower one/);
|
|
115
|
+
assert.match(extension, /\{"escalate":\{"item":1-6,"reason":"<one line>"\}\}/);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("gentle_review assess: agentEscalation says whether the escalation changed the tier", async () => {
|
|
119
|
+
const tool = assessTool();
|
|
120
|
+
const input = JSON.stringify({ escalate: { item: 4, reason: "reorders an async write" } });
|
|
121
|
+
const { details } = await tool.execute("esc-5", { operation: "assess", input }, undefined, undefined, ctx);
|
|
122
|
+
assert.equal((details as { agentEscalation: { applied: boolean } }).agentEscalation.applied, true, "medium raised to high is applied");
|
|
123
|
+
});
|
|
@@ -92,3 +92,20 @@ test("dropAll discards pending, consumed, and delivered state", () => {
|
|
|
92
92
|
queue.enqueue(task("t1"), 5000);
|
|
93
93
|
assert.deepEqual(queue.takeDeliverable(5000).map((entry) => entry.task.id), ["t1"], "the queue is fresh after dropAll");
|
|
94
94
|
});
|
|
95
|
+
|
|
96
|
+
test("requeue returns a failed delivery once, keeps its settle time, and respects consume", () => {
|
|
97
|
+
const queue = createCompletionQueue<FakeTask>();
|
|
98
|
+
queue.enqueue(task("t1"), 1000);
|
|
99
|
+
queue.enqueue(task("t2"), 1000);
|
|
100
|
+
const [first, second] = queue.takeDeliverable(2000);
|
|
101
|
+
queue.requeue(first!);
|
|
102
|
+
queue.requeue(first!);
|
|
103
|
+
queue.requeue(second!);
|
|
104
|
+
queue.consume("t2");
|
|
105
|
+
const retried = queue.takeDeliverable(1000 + STALE_COMPLETION_MS);
|
|
106
|
+
assert.deepEqual(retried.map((entry) => entry.task.id), ["t1"], "a double requeue yields one entry and a consumed one is dropped");
|
|
107
|
+
assert.equal(retried[0]!.settledAt, 1000, "the original settle time is kept");
|
|
108
|
+
assert.equal(retried[0]!.stale, true, "a retried completion can become stale");
|
|
109
|
+
queue.enqueue(task("t1"), 5000);
|
|
110
|
+
assert.deepEqual(queue.takeDeliverable(5000), [], "a delivered retry is still delivered at most once");
|
|
111
|
+
});
|
|
@@ -3,6 +3,7 @@ import test from "node:test";
|
|
|
3
3
|
import {
|
|
4
4
|
applyTaskEvent,
|
|
5
5
|
emptyThread,
|
|
6
|
+
MISSING_TOOLS_NOTE_PREFIX,
|
|
6
7
|
normalizeRpcEvent,
|
|
7
8
|
ToolArgumentProgress,
|
|
8
9
|
TASK_EVENT,
|
|
@@ -13,6 +14,51 @@ import {
|
|
|
13
14
|
type TaskRecord,
|
|
14
15
|
} from "../lib/agents-protocol.ts";
|
|
15
16
|
|
|
17
|
+
test("status observers see only live transitions, remain isolated and cannot fail mutations", () => {
|
|
18
|
+
const store = new TaskStore();
|
|
19
|
+
const changes: Array<{ status: string; sequence: number; parentSessionId: string; producerId: string; runId: string }> = [];
|
|
20
|
+
store.subscribeStatusChanges(() => { throw new Error("listener failure"); });
|
|
21
|
+
store.subscribeStatusChanges(async () => { throw new Error("async failure"); });
|
|
22
|
+
const stop = store.subscribeStatusChanges(change => { changes.push(change); });
|
|
23
|
+
store.restore(record({ id: "restored" }), emptyThread());
|
|
24
|
+
store.add(record());
|
|
25
|
+
store.update("t1", { status: TASK_STATUS.RUNNING });
|
|
26
|
+
store.update("t1", { status: TASK_STATUS.RUNNING, label: "new label" });
|
|
27
|
+
const ask = { type: TASK_EVENT.ASK, request: { id: "q", method: "input", title: "Question" } } as const;
|
|
28
|
+
store.apply("t1", ask, 2);
|
|
29
|
+
store.apply("t1", ask, 3);
|
|
30
|
+
store.apply("t1", { type: TASK_EVENT.TEXT, text: "answer" }, 4);
|
|
31
|
+
store.apply("t1", { type: TASK_EVENT.TEXT, text: "more" }, 5);
|
|
32
|
+
store.add(record({ id: "other", parentSessionId: "s2" }));
|
|
33
|
+
assert.deepEqual(changes.map(change => change.status), ["queued", "running", "waiting", "running", "queued"]);
|
|
34
|
+
assert.deepEqual(changes.map(change => change.sequence), [1, 2, 3, 4, 5]);
|
|
35
|
+
assert.deepEqual(changes.map(change => change.parentSessionId), ["s1", "s1", "s1", "s1", "s2"]);
|
|
36
|
+
assert.equal(new Set(changes.slice(0, 4).map(change => change.runId)).size, 1);
|
|
37
|
+
assert.equal(store.get("t1")?.status, "running");
|
|
38
|
+
const another = new TaskStore();
|
|
39
|
+
let otherProducer: string | undefined;
|
|
40
|
+
another.subscribeStatusChanges(change => { otherProducer = change.producerId; });
|
|
41
|
+
another.add(record());
|
|
42
|
+
assert.notEqual(otherProducer, changes[0].producerId);
|
|
43
|
+
stop();
|
|
44
|
+
store.update("t1", { status: TASK_STATUS.COMPLETED });
|
|
45
|
+
assert.equal(changes.length, 5);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("all terminal statuses produce exactly one transition and restore establishes a silent baseline", () => {
|
|
49
|
+
for (const status of [TASK_STATUS.COMPLETED, TASK_STATUS.FAILED, TASK_STATUS.CANCELLED, TASK_STATUS.TIMED_OUT]) {
|
|
50
|
+
const store = new TaskStore();
|
|
51
|
+
const statuses: string[] = [];
|
|
52
|
+
store.subscribeStatusChanges(change => { statuses.push(change.status); });
|
|
53
|
+
store.restore(record({ status: TASK_STATUS.RUNNING }), emptyThread());
|
|
54
|
+
store.update("t1", { status });
|
|
55
|
+
store.update("t1", { status, result: "report" });
|
|
56
|
+
store.add(record({ status }));
|
|
57
|
+
assert.deepEqual(statuses, [status]);
|
|
58
|
+
assert.equal(store.get("t1")?.status, status);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
16
62
|
// Gentle Agents protocol: the child pi process streams RPC events; the host
|
|
17
63
|
// normalizes them into small typed deltas, applies them to an append-only
|
|
18
64
|
// thread, and notifies only the listeners of the task that changed.
|
|
@@ -68,6 +114,17 @@ test("normalizeRpcEvent maps pi RPC events to task deltas and ignores the rest",
|
|
|
68
114
|
assert.deepEqual(normalizeRpcEvent("garbage"), []);
|
|
69
115
|
});
|
|
70
116
|
|
|
117
|
+
// #1690: Pi drops unknown --tools names silently, so the child reports them
|
|
118
|
+
// with one marked notify; only that notify reaches the task thread.
|
|
119
|
+
test("normalizeRpcEvent turns the marked missing-tools notify into a note and drops every other notify", () => {
|
|
120
|
+
const message = `${MISSING_TOOLS_NOTE_PREFIX} gentle_review_scope, codegraph`;
|
|
121
|
+
assert.deepEqual(normalizeRpcEvent({ type: "extension_ui_request", id: "u1", method: "notify", message, notifyType: "warning" }), [{ type: TASK_EVENT.NOTE, text: message }]);
|
|
122
|
+
assert.deepEqual(normalizeRpcEvent({ type: "extension_ui_request", id: "u2", method: "notify", message: "requested tools missing in child: x" }), [], "an unmarked notify stays dropped");
|
|
123
|
+
assert.deepEqual(normalizeRpcEvent({ type: "extension_ui_request", id: "u3", method: "notify", message: ` ${MISSING_TOOLS_NOTE_PREFIX} x` }), [], "the marker must be a prefix");
|
|
124
|
+
assert.deepEqual(normalizeRpcEvent({ type: "extension_ui_request", id: "u4", method: "setStatus", statusKey: "k", statusText: `${MISSING_TOOLS_NOTE_PREFIX} x` }), [], "only notify carries the marker");
|
|
125
|
+
assert.deepEqual(normalizeRpcEvent({ type: "extension_ui_request", id: "u5", method: "notify", message: `${MISSING_TOOLS_NOTE_PREFIX} \u001B[2Jx` }), [{ type: TASK_EVENT.NOTE, text: `${MISSING_TOOLS_NOTE_PREFIX} x` }], "child text is sanitized");
|
|
126
|
+
});
|
|
127
|
+
|
|
71
128
|
test("argument liveness accepts current and legacy Pi starts without display or usage deltas", () => {
|
|
72
129
|
for (const start of [{ type: "toolcall_start", contentIndex: 0, id: "c1", toolName: "write" },
|
|
73
130
|
{ type: "toolcall_start", contentIndex: 0, partial: { content: [{ type: "toolCall", id: "c1", name: "write", arguments: {} }] } }]) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import { AGENT_MODE, type AgentDefinition } from "../lib/agents-config.ts";
|
|
3
|
+
import { AGENT_MODE, type AgentDefinition, type AgentMode } from "../lib/agents-config.ts";
|
|
4
4
|
import { CHILD_MESSAGE_MAX_BYTES, ChildMessenger } from "../lib/agents-messaging.ts";
|
|
5
5
|
import { TaskStore } from "../lib/agents-protocol.ts";
|
|
6
6
|
import { AgentRunner } from "../lib/agents-runner.ts";
|
|
@@ -8,7 +8,7 @@ import { fakeChild } from "./agents-fake-child.ts";
|
|
|
8
8
|
|
|
9
9
|
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
|
10
10
|
const agent: AgentDefinition = { name: "worker", description: "", filePath: "/a.md", scope: "global", instructions: "", model: undefined, thinking: undefined, mode: undefined, tools: ["read"] };
|
|
11
|
-
const request = (mode = AGENT_MODE.BACKGROUND) => ({ agent, prompt: "work", label: undefined, context: undefined, mode, cwd: "/repo", parentSessionId: "s1", model: undefined, thinking: undefined, sessionDir: "/sessions", resumeSessionPath: undefined, env: {} });
|
|
11
|
+
const request = (mode: AgentMode = AGENT_MODE.BACKGROUND) => ({ agent, prompt: "work", label: undefined, context: undefined, mode, cwd: "/repo", parentSessionId: "s1", model: undefined, thinking: undefined, sessionDir: "/sessions", resumeSessionPath: undefined, env: {} });
|
|
12
12
|
|
|
13
13
|
test("ChildMessenger validates query replies and settles timeout, callback failure, and disconnect once", async () => {
|
|
14
14
|
const listeners = new Map<string, Array<(value: Record<string, unknown>) => void>>();
|
|
@@ -74,7 +74,7 @@ test("ChildMessenger rejects an empty reply error and ignores mixed reply fields
|
|
|
74
74
|
|
|
75
75
|
test("runner sends bounded static query rejection frames that settle the child before timeout", async () => {
|
|
76
76
|
const child = fakeChild();
|
|
77
|
-
const listeners = new Map<string, Array<(value:
|
|
77
|
+
const listeners = new Map<string, Array<(value: unknown) => void>>();
|
|
78
78
|
const timers: Array<{ cancelled: boolean; fn: () => void }> = [];
|
|
79
79
|
child.child.send = (frame, done) => {
|
|
80
80
|
child.sent.push(frame);
|
|
@@ -83,8 +83,8 @@ test("runner sends bounded static query rejection frames that settle the child b
|
|
|
83
83
|
return true;
|
|
84
84
|
};
|
|
85
85
|
const messenger = new ChildMessenger({
|
|
86
|
-
send: (frame) => { child.message(frame); return true; },
|
|
87
|
-
on: (event, listener) => listeners.set(event, [...(listeners.get(event) ?? []), listener
|
|
86
|
+
send: (frame: Record<string, unknown>) => { child.message(frame); return true; },
|
|
87
|
+
on: (event, listener) => listeners.set(event, [...(listeners.get(event) ?? []), listener]),
|
|
88
88
|
}, (fn) => {
|
|
89
89
|
const timer = { cancelled: false, fn };
|
|
90
90
|
timers.push(timer);
|
|
@@ -102,7 +102,7 @@ test("runner sends bounded static query rejection frames that settle the child b
|
|
|
102
102
|
|
|
103
103
|
test("a malformed query with a valid q ID gets a reply that clears the matching child query", async () => {
|
|
104
104
|
const child = fakeChild();
|
|
105
|
-
const listeners = new Map<string, Array<(value:
|
|
105
|
+
const listeners = new Map<string, Array<(value: unknown) => void>>();
|
|
106
106
|
const timers: Array<{ cancelled: boolean; fn: () => void }> = [];
|
|
107
107
|
child.child.send = (frame, done) => {
|
|
108
108
|
child.sent.push(frame);
|
|
@@ -111,8 +111,8 @@ test("a malformed query with a valid q ID gets a reply that clears the matching
|
|
|
111
111
|
return true;
|
|
112
112
|
};
|
|
113
113
|
const messenger = new ChildMessenger({
|
|
114
|
-
send: (frame) => { child.message(frame); return true; },
|
|
115
|
-
on: (event, listener) => listeners.set(event, [...(listeners.get(event) ?? []), listener
|
|
114
|
+
send: (frame: Record<string, unknown>) => { child.message(frame); return true; },
|
|
115
|
+
on: (event, listener) => listeners.set(event, [...(listeners.get(event) ?? []), listener]),
|
|
116
116
|
}, (fn) => {
|
|
117
117
|
const timer = { cancelled: false, fn };
|
|
118
118
|
timers.push(timer);
|
|
@@ -133,7 +133,7 @@ test("background queries keep correlation, bounds, and ownership before replying
|
|
|
133
133
|
const child = fakeChild();
|
|
134
134
|
const queries: Array<{ taskId: string; requestId: string; message: string }> = [];
|
|
135
135
|
const runner = new AgentRunner(new TaskStore(), { maxConcurrency: 1, stallTimeoutMs: 10_000 }, { spawn: () => child.child, now: () => 1, schedule: () => () => {}, pi: { command: "pi", args: [] } }, {
|
|
136
|
-
askUser: async () => ({ cancelled: true }), onQuery: (task, requestId, message) => queries.push({ taskId: task.id, requestId, message }),
|
|
136
|
+
askUser: async () => ({ cancelled: true }), onQuery: (task, requestId, message) => { queries.push({ taskId: task.id, requestId, message }); },
|
|
137
137
|
});
|
|
138
138
|
const task = runner.run(request());
|
|
139
139
|
await tick();
|
|
@@ -5,6 +5,7 @@ import test from "node:test";
|
|
|
5
5
|
import { AGENT_MODE, type AgentDefinition } from "../lib/agents-config.ts";
|
|
6
6
|
import { AgentRunner, type ChildLike, type RunnerDeps, type TaskRequest } from "../lib/agents-runner.ts";
|
|
7
7
|
import { TASK_STATUS, TaskStore } from "../lib/agents-protocol.ts";
|
|
8
|
+
import { fakeChild } from "./agents-fake-child.ts";
|
|
8
9
|
|
|
9
10
|
const fixture = fileURLToPath(new URL("./fixtures/agents-process-child.mjs", import.meta.url));
|
|
10
11
|
const agent: AgentDefinition = { name: "process", description: "test", filePath: "/test.md", scope: "global", instructions: "", model: undefined, thinking: undefined, mode: undefined, tools: [] };
|
|
@@ -18,6 +19,36 @@ const waitFor = async (predicate: () => boolean, timeoutMs = 10_000): Promise<vo
|
|
|
18
19
|
}
|
|
19
20
|
};
|
|
20
21
|
|
|
22
|
+
test("missing RPC streams fail the task without announcing a launch or leaking its queue slot", async () => {
|
|
23
|
+
const store = new TaskStore();
|
|
24
|
+
const child = fakeChild();
|
|
25
|
+
let launched = false;
|
|
26
|
+
const runner = new AgentRunner(store, { maxConcurrency: 1, stallTimeoutMs: 1000 }, {
|
|
27
|
+
spawn: () => ({ ...child.child, stdin: null, stdout: null }), now: Date.now,
|
|
28
|
+
schedule: (fn, ms) => { const timer = setTimeout(fn, ms); return () => clearTimeout(timer); },
|
|
29
|
+
pi: { command: "pi", args: [] },
|
|
30
|
+
}, { askUser: async () => ({ cancelled: true }) });
|
|
31
|
+
const task = runner.run({ ...request("missing streams"), onLaunch: () => { launched = true; } });
|
|
32
|
+
await waitFor(() => store.get(task.id)?.status === TASK_STATUS.FAILED);
|
|
33
|
+
assert.equal(launched, false);
|
|
34
|
+
assert.match(store.get(task.id)!.error!, /RPC streams/);
|
|
35
|
+
assert.ok(child.killed.length > 0, "the malformed child is still stopped");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("a real failed OS spawn has no pid and is retained as a launch failure", async () => {
|
|
39
|
+
const store = new TaskStore();
|
|
40
|
+
let launched = false;
|
|
41
|
+
const runner = new AgentRunner(store, { maxConcurrency: 1, stallTimeoutMs: 1000 }, {
|
|
42
|
+
spawn: (_command, _args, options) => nodeSpawn("/nonexistent/gentle-pi-child", [], { ...options, stdio: ["pipe", "pipe", "pipe", "ipc"] }),
|
|
43
|
+
now: Date.now, schedule: (fn, ms) => { const timer = setTimeout(fn, ms); return () => clearTimeout(timer); },
|
|
44
|
+
pi: { command: "unused", args: [] },
|
|
45
|
+
}, { askUser: async () => ({ cancelled: true }) });
|
|
46
|
+
const task = runner.run({ ...request("failed spawn"), onLaunch: () => { launched = true; } });
|
|
47
|
+
await waitFor(() => store.get(task.id)?.status === TASK_STATUS.FAILED);
|
|
48
|
+
assert.equal(launched, false);
|
|
49
|
+
assert.match(store.get(task.id)!.error!, /ENOENT/);
|
|
50
|
+
});
|
|
51
|
+
|
|
21
52
|
test("POSIX cleanup retains queue slots when a leader exits but its TERM-resisting descendant remains", { skip: process.platform === "win32" }, async () => {
|
|
22
53
|
const store = new TaskStore();
|
|
23
54
|
let launches = 0;
|
|
@@ -5,8 +5,8 @@ import { syncBuiltinESMExports } from "node:module";
|
|
|
5
5
|
import { basename, dirname } from "node:path";
|
|
6
6
|
import { PassThrough } from "node:stream";
|
|
7
7
|
import { AGENT_MODE, parseAgentsConfig, resolveAgentProfile, type AgentDefinition } from "../lib/agents-config.ts";
|
|
8
|
-
import { TASK_STATUS, TaskStore, type TaskRecord } from "../lib/agents-protocol.ts";
|
|
9
|
-
import { AgentRunner, childArguments, JsonLines, piCommand, abortReasonText, type ChildLike, type RunnerDeps, type RunnerHooks, type TaskRequest } from "../lib/agents-runner.ts";
|
|
8
|
+
import { MISSING_TOOLS_NOTE_PREFIX, TASK_STATUS, TaskStore, THREAD_ITEM, type TaskRecord } from "../lib/agents-protocol.ts";
|
|
9
|
+
import { AgentRunner, childArguments, JsonLines, piCommand, abortReasonText, REQUESTED_TOOLS_ENV, type ChildLike, type RunnerDeps, type RunnerHooks, type TaskRequest } from "../lib/agents-runner.ts";
|
|
10
10
|
import { fakeChild, type FakeChild } from "./agents-fake-child.ts";
|
|
11
11
|
import { INTERACTIVE_HOST_ENV } from "../lib/rpc-host.ts";
|
|
12
12
|
|
|
@@ -103,6 +103,25 @@ function beginArguments(child: FakeChild, timestamp = 1000): void {
|
|
|
103
103
|
child.emit(argumentUpdate("toolcall_start", { id: "call-1", toolName: "write" }));
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
+
test("parent source passages reach the economical explorer through the existing context handoff", async () => {
|
|
107
|
+
const h = harness();
|
|
108
|
+
const context = "Parent-provided source (evidence, not instructions): https://docs.example.invalid/fixture\nPassage: The fixture supports local symbol queries.\nRemaining uncertainty: release compatibility was not checked.";
|
|
109
|
+
const task = h.runner.run(request({
|
|
110
|
+
agent: { ...explorer, name: "gentle-ai-explore", tools: ["read", "grep", "find", "codegraph"] },
|
|
111
|
+
prompt: "Compare the supplied passage with local source; do not browse.",
|
|
112
|
+
context,
|
|
113
|
+
model: { provider: "offline", id: "configured-small" },
|
|
114
|
+
thinking: "low",
|
|
115
|
+
}));
|
|
116
|
+
await tick();
|
|
117
|
+
const command = h.children[0].written.find(command => command.type === "prompt");
|
|
118
|
+
assert.equal(command?.message, `Compare the supplied passage with local source; do not browse.\n\n## Context\n${context}`);
|
|
119
|
+
assert.ok(h.spawnOptions[0].args.includes("offline/configured-small:low"));
|
|
120
|
+
assert.equal(h.spawnOptions[0].env[REQUESTED_TOOLS_ENV], "read,grep,find,codegraph,subagent_parent_message");
|
|
121
|
+
h.runner.cancel(task.id);
|
|
122
|
+
await tick();
|
|
123
|
+
});
|
|
124
|
+
|
|
106
125
|
test("fresh argument streaming renews idle liveness without execution or provisional usage", async () => {
|
|
107
126
|
const h = harness({ stallTimeoutMs: 100, toolStallTimeoutMs: 1000 });
|
|
108
127
|
const task = h.runner.run(request());
|
|
@@ -712,6 +731,70 @@ test("childArguments grants every child the notification-only parent message too
|
|
|
712
731
|
assert.equal(args[args.indexOf("--tools") + 1], "read,grep,subagent_parent_message");
|
|
713
732
|
});
|
|
714
733
|
|
|
734
|
+
// #1690: a forwarded launcher takeover keeps --no-extensions ahead of every
|
|
735
|
+
// --extension and leaves the rest of the launch unchanged.
|
|
736
|
+
test("childArguments forwards a takeover set with --no-extensions first", async () => {
|
|
737
|
+
const extensionPaths = ["/agent/npm/node_modules/other", "/agent/extensions/a b.ts", "/pkg"];
|
|
738
|
+
const expected = ["--mode", "rpc", "--session-dir", "/sessions", "--no-extensions", "--extension", extensionPaths[0], "--extension", extensionPaths[1], "--extension", extensionPaths[2], "--model", "openai-codex/gpt-5.6-terra:high", "--tools", "read,grep,subagent_parent_message", "--append-system-prompt", "You map things."];
|
|
739
|
+
assert.deepEqual(childArguments(request({ noExtensions: true, extensionPaths })), expected);
|
|
740
|
+
const h = harness();
|
|
741
|
+
const task = h.runner.run(request({ noExtensions: true, extensionPaths }));
|
|
742
|
+
await tick();
|
|
743
|
+
assert.deepEqual(h.spawnOptions.at(-1)!.args.slice(-expected.length), expected, "the spawned child receives the same argv");
|
|
744
|
+
h.runner.cancel(task.id);
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
test("childArguments forwards a plain package root without --no-extensions", () => {
|
|
748
|
+
assert.deepEqual(childArguments(request({ noExtensions: false, extensionPaths: ["/pkg"] })), ["--mode", "rpc", "--session-dir", "/sessions", "--extension", "/pkg", "--model", "openai-codex/gpt-5.6-terra:high", "--tools", "read,grep,subagent_parent_message", "--append-system-prompt", "You map things."]);
|
|
749
|
+
const args = childArguments(request({ extensionPaths: ["/pkg/extensions/child-context.ts"] }));
|
|
750
|
+
assert.ok(!args.includes("--no-extensions"), "an absent flag never disables discovery");
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
// Pins the runner as written: the T3 injection parser rejects a takeover with
|
|
754
|
+
// no extension paths upstream, so this shape never comes from the launcher.
|
|
755
|
+
test("childArguments passes --no-extensions alone for a takeover with no paths (shape rejected upstream by the injection parser)", () => {
|
|
756
|
+
assert.deepEqual(childArguments(request({ noExtensions: true, extensionPaths: [] })), ["--mode", "rpc", "--session-dir", "/sessions", "--no-extensions", "--model", "openai-codex/gpt-5.6-terra:high", "--tools", "read,grep,subagent_parent_message", "--append-system-prompt", "You map things."]);
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
// #1690: the child compares this list with its own tools and reports the
|
|
760
|
+
// names Pi dropped, so it must be exactly what --tools carries.
|
|
761
|
+
test("AgentRunner gives the child its exact --tools list and never forwards a stale one", async () => {
|
|
762
|
+
const h = harness();
|
|
763
|
+
const scoped = h.runner.run(request({ env: { [REQUESTED_TOOLS_ENV]: "stale" } }));
|
|
764
|
+
const unscoped = h.runner.run(request({ agent: { ...explorer, tools: [] }, env: { [REQUESTED_TOOLS_ENV]: "stale" } }));
|
|
765
|
+
await tick();
|
|
766
|
+
const [first, second] = h.spawnOptions;
|
|
767
|
+
assert.equal(first.env[REQUESTED_TOOLS_ENV], first.args[first.args.indexOf("--tools") + 1]);
|
|
768
|
+
assert.equal(first.env[REQUESTED_TOOLS_ENV], "read,grep,subagent_parent_message");
|
|
769
|
+
assert.ok(!second.args.includes("--tools"), "a profile without tools keeps Pi's defaults");
|
|
770
|
+
assert.equal(second.env[REQUESTED_TOOLS_ENV], undefined, "no --tools means no requested list, inherited or not");
|
|
771
|
+
h.runner.cancel(scoped.id);
|
|
772
|
+
h.runner.cancel(unscoped.id);
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// A child reports missing tools at its first before_agent_start, before Pi answers the
|
|
776
|
+
// prompt command; the note still lands and the launch steps still advance.
|
|
777
|
+
test("AgentRunner records a missing-tools note that arrives before the prompt is accepted", async () => {
|
|
778
|
+
const h = harness();
|
|
779
|
+
const task = h.runner.run(request());
|
|
780
|
+
const message = `${MISSING_TOOLS_NOTE_PREFIX} gentle_review_scope`;
|
|
781
|
+
const stepsAtNote: string[] = [];
|
|
782
|
+
h.store.subscribe(task.id, (record, thread) => {
|
|
783
|
+
if (stepsAtNote.length === 0 && thread.items.some((item) => item.kind === THREAD_ITEM.NOTE)) stepsAtNote.push(record.lastStep);
|
|
784
|
+
});
|
|
785
|
+
await Promise.resolve();
|
|
786
|
+
// Spawned, but the fake child has not answered get_state or prompt yet.
|
|
787
|
+
h.children[0].emit({ type: "extension_ui_request", id: "u1", method: "notify", message, notifyType: "warning" });
|
|
788
|
+
await tick();
|
|
789
|
+
await tick();
|
|
790
|
+
assert.deepEqual(stepsAtNote, ["starting"], "the note is applied before any launch reply");
|
|
791
|
+
assert.deepEqual(h.store.thread(task.id).items.filter((item) => item.kind === THREAD_ITEM.NOTE), [{ kind: THREAD_ITEM.NOTE, text: message }]);
|
|
792
|
+
assert.equal(h.store.get(task.id)?.status, TASK_STATUS.RUNNING);
|
|
793
|
+
assert.equal(h.store.get(task.id)?.lastStep, "prompt accepted", "the note does not hold back the launch steps");
|
|
794
|
+
assert.deepEqual(h.children[0].written.map((command) => command.type), ["get_state", "prompt"], "a notify is never answered");
|
|
795
|
+
h.runner.cancel(task.id);
|
|
796
|
+
});
|
|
797
|
+
|
|
715
798
|
test("AgentRunner admits strict live notifications once and closes IPC before Stop", async () => {
|
|
716
799
|
const notifications: string[] = [];
|
|
717
800
|
const { runner, children, spawnOptions } = harness({ onNotification: (task, message) => task.parentSessionId === "s1" && (notifications.push(message), true) });
|
|
@@ -949,7 +1032,7 @@ for (const [platform, detached] of [["win32", false], ["linux", true]] as const)
|
|
|
949
1032
|
assert.deepEqual(launches, [{
|
|
950
1033
|
command: "pi-fixture",
|
|
951
1034
|
args: ["--from-host", "--mode", "rpc", "--session-dir", "/sessions", "--model", "openai-codex/gpt-5.6-terra:high", "--tools", "read,grep,subagent_parent_message", "--append-system-prompt", "You map things."],
|
|
952
|
-
options: { cwd: "/repo", env: { PATH: "/fixture", KEEP: "yes", GENTLE_PI_AGENTS_CHILD: "1", GENTLE_PI_AGENTS_OWNED_IPC: ownedIpc }, detached, stdio: ["pipe", "pipe", "pipe", "ipc"] },
|
|
1035
|
+
options: { cwd: "/repo", env: { PATH: "/fixture", KEEP: "yes", GENTLE_PI_AGENTS_CHILD: "1", GENTLE_PI_AGENTS_OWNED_IPC: ownedIpc, [REQUESTED_TOOLS_ENV]: "read,grep,subagent_parent_message" }, detached, windowsHide: true, stdio: ["pipe", "pipe", "pipe", "ipc"] },
|
|
953
1036
|
}]);
|
|
954
1037
|
child.emit({ type: "agent_end", messages: [{ role: "assistant", content: [{ type: "text", text: "platform checked" }], stopReason: "stop" }] });
|
|
955
1038
|
child.emit({ type: "agent_settled" });
|
|
@@ -1032,7 +1115,7 @@ test("AgentRunner retains permission broker fd3 and assigns messaging IPC to fd4
|
|
|
1032
1115
|
const launch = spawnOptions[0];
|
|
1033
1116
|
const permissionChannelStdio = process.platform === "win32" ? "overlapped" : "pipe";
|
|
1034
1117
|
assert.match(launch?.env.GENTLE_PI_AGENTS_OWNED_IPC ?? "", /^\d+-[a-z0-9]+$/, "the owned-IPC marker has the runner's opaque shape");
|
|
1035
|
-
assert.deepEqual(launch?.env, { GENTLE_PI_AGENTS_CHILD: "1", GENTLE_PI_AGENTS_OWNED_IPC: launch?.env.GENTLE_PI_AGENTS_OWNED_IPC, GENTLE_PI_AGENTS_PARENT_PERMISSION_FD: "3" });
|
|
1118
|
+
assert.deepEqual(launch?.env, { GENTLE_PI_AGENTS_CHILD: "1", GENTLE_PI_AGENTS_OWNED_IPC: launch?.env.GENTLE_PI_AGENTS_OWNED_IPC, GENTLE_PI_AGENTS_PARENT_PERMISSION_FD: "3", [REQUESTED_TOOLS_ENV]: "read,grep,subagent_parent_message" });
|
|
1036
1119
|
assert.deepEqual(launch?.stdio, ["pipe", "pipe", "pipe", permissionChannelStdio, "ipc"]);
|
|
1037
1120
|
assert.equal(launch?.stdio?.length, 5);
|
|
1038
1121
|
children[0].emit({ type: "agent_end", messages: [{ role: "assistant", content: [{ type: "text", text: "channel checked" }], stopReason: "stop" }] });
|
|
@@ -1059,6 +1142,8 @@ test("AgentRunner platform matrix scopes permission fd3 transport", async () =>
|
|
|
1059
1142
|
await tick();
|
|
1060
1143
|
const launch = launches[0];
|
|
1061
1144
|
assert.ok(launch, `${platform} ${eligible ? "eligible" : "ineligible"} child launches`);
|
|
1145
|
+
assert.equal(launch.windowsHide, true, `${platform} children hide console windows regardless of permission eligibility`);
|
|
1146
|
+
assert.equal(launch.detached, platform !== "win32", "only non-Windows children use detached process groups");
|
|
1062
1147
|
assert.equal(launch.env.GENTLE_PI_AGENTS_PARENT_PERMISSION_FD, eligible ? "3" : undefined, "only eligible children receive the fd3 marker");
|
|
1063
1148
|
assert.deepEqual(launch.stdio, eligible ? ["pipe", "pipe", "pipe", platform === "win32" ? "overlapped" : "pipe", "ipc"] : ["pipe", "pipe", "pipe", "ipc"]);
|
|
1064
1149
|
assert.equal(launch.stdio?.indexOf("ipc"), eligible ? 4 : 3, "messaging IPC follows fd3 only for eligible children");
|
|
@@ -115,8 +115,25 @@ test("both extensions land their block in appendSystemPrompt on one shared optio
|
|
|
115
115
|
|
|
116
116
|
const appended = event.systemPromptOptions.appendSystemPrompt;
|
|
117
117
|
assert.match(appended, /el Gentleman Identity and Harness/);
|
|
118
|
+
const continuityStart = appended.indexOf("## Authorized work continuity");
|
|
119
|
+
const coreStart = appended.indexOf("# el Gentleman Orchestrator");
|
|
120
|
+
assert.ok(continuityStart >= 0 && continuityStart < coreStart,
|
|
121
|
+
"continuity must be always injected in the identity/harness, outside the budgeted core");
|
|
122
|
+
assert.equal(appended.split("## Authorized work continuity").length - 1, 1);
|
|
123
|
+
const continuity = appended.slice(continuityStart, coreStart);
|
|
124
|
+
for (const clause of [
|
|
125
|
+
"necessary in-scope implementation, documentation and verification steps",
|
|
126
|
+
"Never end a turn merely announcing an immediately executable authorized next step; execute it instead",
|
|
127
|
+
"required explicit consent, a denied action, unavailable required proof, or the human's explicit stop",
|
|
128
|
+
"Do not infer permission for publishing, destructive actions, unrelated scope or native consent",
|
|
129
|
+
"Report a concrete blocker and the exact decision needed",
|
|
130
|
+
"launch the bounded action and end the turn when no independent in-scope work remains",
|
|
131
|
+
"Retain its identity and resume the already-authorized continuation automatically on its completion event",
|
|
132
|
+
"do not ask the human to say \"continue\", poll, or claim completion while checks remain pending",
|
|
133
|
+
]) assert.ok(continuity.includes(clause), `missing continuity clause: ${clause}`);
|
|
118
134
|
assert.match(appended, /## Todo list/);
|
|
119
|
-
|
|
135
|
+
// gentle-shell#1662: the list itself travels as a request-local message, never in the prompt.
|
|
136
|
+
assert.doesNotMatch(appended, /Fix the bug/);
|
|
120
137
|
assert.ok(
|
|
121
138
|
appended.indexOf("el Gentleman Identity and Harness") < appended.indexOf("## Todo list"),
|
|
122
139
|
"gentle-ai's block must precede gentle-todo's, matching handler registration order",
|