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,297 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createAssistantMessageEventStream } from "@earendil-works/pi-ai";
|
|
4
|
+
import type { AssistantMessage, Model, Api, Context, ModelsSimpleStreamOptions } from "@earendil-works/pi-ai";
|
|
5
|
+
import { preflightHelper, OrchestratorHelper } from "../lib/orchestrator-helper.ts";
|
|
6
|
+
import type { MetadataReceipt } from "../lib/orchestrator-consultation.ts";
|
|
7
|
+
|
|
8
|
+
const model: Model<Api> = { id: "local", provider: "fixture", api: "fixture", name: "Local",
|
|
9
|
+
baseUrl: "http://invalid.local", reasoning: true, input: ["text"], contextWindow: 32000, maxTokens: 1024,
|
|
10
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } };
|
|
11
|
+
const receipt = (): MetadataReceipt => ({ schema: "gentle-agents.consultation/v1", kind: "metadata", status: "available",
|
|
12
|
+
source: "published_snapshot", ownerReply: false, authority: "none", targetSessionId: "owner", observedAt: 123,
|
|
13
|
+
freshness: "recent", digest: "a".repeat(64), snapshot: { label: "Owner", workspace: "/recorded", tasks: [],
|
|
14
|
+
omittedTasks: 2, scope: null, catalog: null, state: null }, unknowns: ["owner-decision"], omissions: ["private-context"] });
|
|
15
|
+
const message = (patch: Partial<AssistantMessage> = {}): AssistantMessage => ({ role: "assistant", api: "fixture",
|
|
16
|
+
provider: "fixture", model: "local", timestamp: 1, stopReason: "stop", content: [{ type: "text", text: "Advice" }],
|
|
17
|
+
usage: { input: 1, output: 2, cacheRead: 0, cacheWrite: 0, totalTokens: 3,
|
|
18
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0.01 } }, ...patch });
|
|
19
|
+
function fixture(deadlineMs = 1000) {
|
|
20
|
+
const calls: { context: Context; options?: ModelsSimpleStreamOptions; resolve: (m: AssistantMessage) => void;
|
|
21
|
+
reject: (e: unknown) => void }[] = [];
|
|
22
|
+
const engine = new OrchestratorHelper({ streamSimple(_model, context, options) {
|
|
23
|
+
const stream = createAssistantMessageEventStream();
|
|
24
|
+
let resolve!: (m: AssistantMessage) => void, reject!: (e: unknown) => void;
|
|
25
|
+
const pending = new Promise<AssistantMessage>((yes, no) => { resolve = yes; reject = no; });
|
|
26
|
+
stream.result = () => pending; // public SDK signature, deliberately ignores abort
|
|
27
|
+
calls.push({ context, options, resolve, reject });
|
|
28
|
+
return stream;
|
|
29
|
+
} }, { deadlineMs });
|
|
30
|
+
const run = (patch = {}) => engine.run({ receipt: receipt(), question: "What is recorded?", model, isCurrent: () => true, ...patch });
|
|
31
|
+
return { engine, calls, run };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function classifiedReceipt(): MetadataReceipt {
|
|
35
|
+
const r = receipt(), s = r.snapshot!;
|
|
36
|
+
s.catalog = { tasks: [{ id: "Task-A", label: "Active", status: "running", cwd: "/launch" }],
|
|
37
|
+
registered: [], omittedTasks: 1, omittedRegistered: 0, cursor: "PRIVATE_CURSOR" };
|
|
38
|
+
s.tasks = [{ id: "historical", label: "Old", status: "running", workspace: "/old" }];
|
|
39
|
+
s.state = { schema: 2, sessionId: "owner", recordedAt: 5, cwd: "/recorded", source: "owner-curated",
|
|
40
|
+
ownerReply: false, authority: "none", state: { objective: "Recorded objective", work: {
|
|
41
|
+
area: "Auténtica", topic: "Login", tags: ["approved", "granted"], refs: [
|
|
42
|
+
{ kind: "issue", repository: "github.com/Owner/Repo", id: "12" },
|
|
43
|
+
{ kind: "pr", repository: "github.com/Owner/Repo", id: "12" },
|
|
44
|
+
{ kind: "task", repository: "github.com/Owner/Repo", id: "Task-é" }],
|
|
45
|
+
tasks: { "Task-A": { tags: ["Child"], refs: [{ kind: "task", repository: "github.com/Other/Repo", id: "Exact-ID" }] },
|
|
46
|
+
"task-a": { area: "Wrong case" }, historical: { area: "History" }, ghost: { area: "Ghost" }, outside: { area: "Outside page" } },
|
|
47
|
+
} } };
|
|
48
|
+
return r;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
test("published root and exact current-page task work reach the detached helper payload", async () => {
|
|
52
|
+
const f = fixture(), r = classifiedReceipt(), original = structuredClone(r.snapshot!.state!.state!);
|
|
53
|
+
const rawWork = r.snapshot!.state!.state!.work!;
|
|
54
|
+
for (const object of [rawWork, rawWork.tasks, rawWork.tasks!["Task-A"], ...rawWork.refs!]) {
|
|
55
|
+
for (const key of ["toJSON", "privateHistory", "cursor", "humanApproved"])
|
|
56
|
+
Object.defineProperty(object, key, { get() { throw Error("PRIVATE_GETTER"); } });
|
|
57
|
+
}
|
|
58
|
+
const pending = f.run({ receipt: r });
|
|
59
|
+
assert.equal(f.calls.length, 1);
|
|
60
|
+
const payload = JSON.parse(f.calls[0].context.messages[0].content as string);
|
|
61
|
+
const captured = payload.source.snapshot.state.state;
|
|
62
|
+
assert.equal(captured.work?.area, "Auténtica");
|
|
63
|
+
assert.equal(captured.objective, original.objective);
|
|
64
|
+
assert.deepEqual(captured.work.refs, original.work!.refs);
|
|
65
|
+
assert.deepEqual(captured.work.tags, ["approved", "granted"]);
|
|
66
|
+
assert.deepEqual(captured.work.tasks, { "Task-A": original.work!.tasks!["Task-A"] });
|
|
67
|
+
assert.equal(captured.work.tasks["Task-A"].area, undefined, "root classification is not inherited");
|
|
68
|
+
assert.ok(payload.source.omissions.includes("unmatched-task-annotations:4"));
|
|
69
|
+
assert.doesNotMatch(JSON.stringify(payload), /Wrong case|History|Ghost|Outside page|PRIVATE_CURSOR/);
|
|
70
|
+
r.snapshot!.state!.state!.work!.refs![0].id = "99";
|
|
71
|
+
r.snapshot!.state!.state!.work!.tasks!["Task-A"].tags![0] = "Mutated";
|
|
72
|
+
assert.equal(captured.work.refs[0].id, "12");
|
|
73
|
+
assert.deepEqual(captured.work.tasks["Task-A"].tags, ["Child"]);
|
|
74
|
+
f.calls[0].resolve(message());
|
|
75
|
+
assert.equal((await pending).authority, "none");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("class-only records omit unmatched-only work and never join historical or child session IDs", () => {
|
|
79
|
+
for (const catalog of [null, { tasks: [], registered: [], omittedTasks: 8, omittedRegistered: 0 }]) {
|
|
80
|
+
const r = classifiedReceipt();
|
|
81
|
+
r.snapshot!.catalog = catalog;
|
|
82
|
+
r.snapshot!.state!.state = { work: { tasks: { historical: { area: "Old" }, childSID: { area: "Child" } } } };
|
|
83
|
+
const prepared = preflightHelper(r, "Question", model);
|
|
84
|
+
assert.equal(prepared.code, undefined);
|
|
85
|
+
const payload = JSON.parse(prepared.content!);
|
|
86
|
+
assert.deepEqual(payload.source.snapshot.state.state, {}, "no fake empty work classification");
|
|
87
|
+
assert.ok(payload.source.omissions.includes("unmatched-task-annotations:2"));
|
|
88
|
+
}
|
|
89
|
+
const root = classifiedReceipt();
|
|
90
|
+
root.snapshot!.catalog = null;
|
|
91
|
+
root.snapshot!.state!.state = { work: { refs: [{ kind: "task", repository: "github.com/Owner/Repo", id: "Exact root" }] } };
|
|
92
|
+
assert.deepEqual(JSON.parse(preflightHelper(root, "Question", model).content!).source.snapshot.state.state,
|
|
93
|
+
root.snapshot!.state!.state, "root-only classification needs no task catalog");
|
|
94
|
+
const r = classifiedReceipt();
|
|
95
|
+
r.snapshot!.state!.state = { work: { tasks: { "Task-A": { refs: [{ kind: "pr", repository: "github.com/Owner/Repo", id: "12" }] } } } };
|
|
96
|
+
const payload = JSON.parse(preflightHelper(r, "Question", model).content!);
|
|
97
|
+
assert.deepEqual(payload.source.snapshot.state.state, r.snapshot!.state!.state, "active-only classification is useful");
|
|
98
|
+
for (const state of [{ progress: "Legacy" }, null]) {
|
|
99
|
+
r.snapshot!.state!.schema = 1; r.snapshot!.state!.state = state;
|
|
100
|
+
assert.deepEqual(JSON.parse(preflightHelper(r, "Question", model).content!).source.snapshot.state.state, state);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("malformed work and foreign owners fail preflight without any stream or private serialization", async () => {
|
|
105
|
+
const f = fixture();
|
|
106
|
+
const malformed = [null, {}, [], { topic: "No area" }, { area: "bad\u0000" }, { tags: ["duplicate", "duplicate"] },
|
|
107
|
+
{ refs: [{ kind: "issue", repository: "github.com/Owner/Repo", id: "012" }] }, { tasks: { ghost: {} } }];
|
|
108
|
+
for (const work of malformed) {
|
|
109
|
+
const r = classifiedReceipt();
|
|
110
|
+
r.snapshot!.state!.state!.work = work as any;
|
|
111
|
+
assert.equal(preflightHelper(r, "Question", model).code, "invalid-source");
|
|
112
|
+
assert.equal((await f.run({ receipt: r })).code, "invalid-source");
|
|
113
|
+
}
|
|
114
|
+
for (const field of ["sessionId", "schema", "source", "ownerReply", "authority"] as const) {
|
|
115
|
+
const r = classifiedReceipt();
|
|
116
|
+
Object.assign(r.snapshot!.state!, { [field]: field === "schema" ? 1 : field === "ownerReply" ? true : "foreign" });
|
|
117
|
+
assert.equal(preflightHelper(r, "Question", model).code, "invalid-source");
|
|
118
|
+
}
|
|
119
|
+
for (const nested of ["root", "task", "ref"] as const) {
|
|
120
|
+
for (const key of ["toJSON", "privateHistory", "cursor", "humanApproved", "endpoint"]) {
|
|
121
|
+
const r = classifiedReceipt(), work = r.snapshot!.state!.state!.work!;
|
|
122
|
+
const target = nested === "root" ? work : nested === "task" ? work.tasks!["Task-A"] : work.refs![0];
|
|
123
|
+
let accessed = false;
|
|
124
|
+
Object.defineProperty(target, key, { enumerable: true, get() { accessed = true; throw Error("PRIVATE"); } });
|
|
125
|
+
assert.equal(preflightHelper(r, "Question", model).code, "invalid-source");
|
|
126
|
+
assert.equal(accessed, false, "unknown nested fields are rejected, never read or serialized");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
assert.equal(f.calls.length, 0);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("structured work consumes the same UTF-8 input budget without truncation", async () => {
|
|
133
|
+
const f = fixture(), r = classifiedReceipt();
|
|
134
|
+
const prepared = preflightHelper(r, "Question", model);
|
|
135
|
+
const plain = receipt(); plain.snapshot!.state = { ...r.snapshot!.state!, schema: 1, state: { objective: "Recorded objective" } };
|
|
136
|
+
plain.snapshot!.catalog = r.snapshot!.catalog; plain.snapshot!.tasks = r.snapshot!.tasks;
|
|
137
|
+
const overhead = Buffer.byteLength(prepared.content!) - Buffer.byteLength(preflightHelper(plain, "Question", model).content!);
|
|
138
|
+
assert.ok(overhead > 0);
|
|
139
|
+
const pending = f.run({ receipt: plain, question: "Question" });
|
|
140
|
+
const call = f.calls[0]; call.resolve(message()); await pending;
|
|
141
|
+
const bytes = Buffer.byteLength(call.context.systemPrompt!) + Buffer.byteLength(call.context.messages[0].content as string);
|
|
142
|
+
r.snapshot!.label += "x".repeat(16384 - bytes);
|
|
143
|
+
assert.equal((await f.run({ receipt: r, question: "Question" })).code, "input-too-large");
|
|
144
|
+
assert.equal(f.calls.length, 1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("one in-flight lease prevents repeated/concurrent billable streams", async () => {
|
|
148
|
+
const f = fixture();
|
|
149
|
+
assert.equal(f.calls.length, 0, "construction does not start a model");
|
|
150
|
+
const first = f.run();
|
|
151
|
+
const second = f.run();
|
|
152
|
+
for (const call of f.calls) call.resolve(message());
|
|
153
|
+
assert.equal((await second).code, "busy");
|
|
154
|
+
assert.equal(f.calls.length, 1);
|
|
155
|
+
assert.equal((await first).status, "available");
|
|
156
|
+
const future = f.run();
|
|
157
|
+
f.calls[1].resolve(message());
|
|
158
|
+
assert.equal((await future).status, "available");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("hard deadline returns while ignored abort retains lease, including late rejection", async () => {
|
|
162
|
+
const f = fixture(15), start = Date.now();
|
|
163
|
+
assert.equal((await f.run()).code, "timeout");
|
|
164
|
+
assert.ok(Date.now() - start < 500);
|
|
165
|
+
assert.equal(f.calls[0].options?.signal?.aborted, true);
|
|
166
|
+
f.engine.cancel();
|
|
167
|
+
assert.equal((await f.run()).code, "busy");
|
|
168
|
+
assert.equal(f.calls.length, 1);
|
|
169
|
+
f.calls[0].reject(new Error("PRIVATE_CREDENTIAL"));
|
|
170
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
171
|
+
const next = f.run();
|
|
172
|
+
f.calls[1].resolve(message());
|
|
173
|
+
assert.equal((await next).status, "available");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("caller and engine cancellation discard late output; source replacement fails closed", async () => {
|
|
177
|
+
const f = fixture(), controller = new AbortController();
|
|
178
|
+
controller.abort();
|
|
179
|
+
assert.equal((await f.run({ signal: controller.signal })).code, "cancelled");
|
|
180
|
+
assert.equal((await f.run({ isCurrent: () => false })).code, "stale-source");
|
|
181
|
+
assert.equal((await f.run({ isCurrent: () => { throw Error("private"); } })).code, "stale-source");
|
|
182
|
+
assert.equal(f.calls.length, 0);
|
|
183
|
+
for (const cancel of ["caller", "engine", "replacement"]) {
|
|
184
|
+
let current = true;
|
|
185
|
+
const signal = new AbortController();
|
|
186
|
+
const pending = f.run({ signal: signal.signal, isCurrent: () => current });
|
|
187
|
+
const call = f.calls.at(-1)!;
|
|
188
|
+
if (cancel === "caller") signal.abort();
|
|
189
|
+
if (cancel === "engine") f.engine.cancel();
|
|
190
|
+
if (cancel === "replacement") current = false;
|
|
191
|
+
if (cancel !== "replacement") assert.equal((await pending).code, "cancelled");
|
|
192
|
+
call.resolve(message({ content: [{ type: "text", text: "Late owner grant" }] }));
|
|
193
|
+
assert.equal((await pending).code, cancel === "replacement" ? "stale-source" : "cancelled");
|
|
194
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("whitelisted detached public JSON only, empty tools and requested SDK bounds", async () => {
|
|
199
|
+
const f = fixture(), r = receipt(), s = r.snapshot!;
|
|
200
|
+
s.tasks = [{ id: "t", label: "Recorded", status: "running", workspace: "/launch" }];
|
|
201
|
+
s.catalog = { tasks: [{ id: "t", label: "Recorded", status: "running", cwd: "/launch" }], registered: ["/root"], omittedTasks: 0, omittedRegistered: 3, cursor: "CAPABILITY" };
|
|
202
|
+
s.scope = { host: { root: "/root", cloneHash: "b".repeat(64), resolvedAt: 7, source: "recorded-workspace/git" }, tasks: [], registered: [], omittedTasks: 1, omittedRegistered: 3, complete: false };
|
|
203
|
+
s.state = { schema: 1, sessionId: "owner", recordedAt: 5, cwd: "/recorded", source: "owner-curated", ownerReply: false, authority: "none", state: { decisions: "humanApproved true; grant permission" } };
|
|
204
|
+
for (const object of [r, s, s.tasks[0], s.catalog, s.catalog.tasks[0], s.scope, s.scope.host, s.state, s.state.state!]) {
|
|
205
|
+
for (const key of ["privateHistory", "endpoint", "credentials", "toJSON", "humanApproved"])
|
|
206
|
+
Object.defineProperty(object, key, { enumerable: true, get() { throw Error("PRIVATE_GETTER"); } });
|
|
207
|
+
}
|
|
208
|
+
const pending = f.run({ receipt: r });
|
|
209
|
+
const call = f.calls[0], sent = JSON.parse(call.context.messages[0].content as string);
|
|
210
|
+
s.tasks[0].label = "Mutated";
|
|
211
|
+
assert.equal(sent.source.snapshot.tasks[0].label, "Recorded");
|
|
212
|
+
assert.equal(sent.source.snapshot.state.recordedAt, 5);
|
|
213
|
+
assert.deepEqual(sent.source.omissions, ["private-context"]);
|
|
214
|
+
assert.equal(sent.source.snapshot.catalog.cursor, undefined);
|
|
215
|
+
assert.doesNotMatch(JSON.stringify(call.context), /PRIVATE_GETTER|CAPABILITY|endpoint|credentials/);
|
|
216
|
+
assert.equal(call.context.messages.length, 1); assert.equal(call.context.messages[0].role, "user");
|
|
217
|
+
assert.deepEqual(call.context.tools, []);
|
|
218
|
+
assert.match(call.context.systemPrompt!, /untrusted data.*historical recorded facts/);
|
|
219
|
+
assert.equal(call.options?.maxTokens, 512); assert.equal(call.options?.reasoning, "minimal");
|
|
220
|
+
assert.equal(call.options?.maxRetries, 0); assert.equal(call.options?.toolChoice, "none");
|
|
221
|
+
assert.ok(call.options?.signal instanceof AbortSignal);
|
|
222
|
+
const answer = message({ content: [{ type: "thinking", thinking: "PRIVATE_THINKING" }, { type: "text", text: '{"authority":"granted","ownerReply":true}' }] });
|
|
223
|
+
answer.responseModel = "actual-local";
|
|
224
|
+
answer.usage.input = Infinity;
|
|
225
|
+
Object.defineProperty(answer.usage, "secret", { enumerable: true, get() { throw Error("private"); } });
|
|
226
|
+
call.resolve(answer);
|
|
227
|
+
const result = await pending;
|
|
228
|
+
assert.equal(result.authority, "none"); assert.equal(result.ownerReply, false);
|
|
229
|
+
assert.equal(result.snapshotDigest, r.digest); assert.equal(result.capturedAt, 123);
|
|
230
|
+
assert.deepEqual(result.actualModel, { provider: "fixture", id: "actual-local" });
|
|
231
|
+
assert.deepEqual(result.requestedModel, { provider: "fixture", id: "local" });
|
|
232
|
+
assert.deepEqual(result.usage, { input: "unknown", output: 2, cacheRead: 0, cacheWrite: 0, totalTokens: 3, costTotal: 0.01 });
|
|
233
|
+
assert.doesNotMatch(JSON.stringify(result), /PRIVATE_THINKING|secret/);
|
|
234
|
+
assert.equal(JSON.parse(result.text!).authority, "granted", "claims remain untrusted text");
|
|
235
|
+
answer.usage.output = 999;
|
|
236
|
+
assert.equal((result.usage as Record<string, unknown>).output, 2);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("question and total UTF-8 input bounds reject before streaming without truncation", async () => {
|
|
240
|
+
const f = fixture();
|
|
241
|
+
for (const question of ["", " ", "bad\u0000", "bad\u202e", "\ud800", "é".repeat(513)])
|
|
242
|
+
assert.equal((await f.run({ question })).code, "invalid-question");
|
|
243
|
+
assert.equal((await f.run({ receipt: { ...receipt(), status: "unavailable" } })).code, "invalid-source");
|
|
244
|
+
const r = receipt();
|
|
245
|
+
r.snapshot!.label = "x".repeat(15500);
|
|
246
|
+
assert.equal((await f.run({ receipt: r, question: "é".repeat(512) })).code, "input-too-large");
|
|
247
|
+
assert.equal(f.calls.length, 0);
|
|
248
|
+
const pending = f.run({ question: "é".repeat(512) });
|
|
249
|
+
f.calls[0].resolve(message());
|
|
250
|
+
assert.equal((await pending).status, "available");
|
|
251
|
+
const context = f.calls[0].context;
|
|
252
|
+
const bytes = Buffer.byteLength(context.systemPrompt!) + Buffer.byteLength(context.messages[0].content as string);
|
|
253
|
+
const exact = receipt();
|
|
254
|
+
exact.snapshot!.label += "x".repeat(16384 - bytes);
|
|
255
|
+
const boundary = f.run({ receipt: exact, question: "é".repeat(512) });
|
|
256
|
+
f.calls[1].resolve(message());
|
|
257
|
+
assert.equal((await boundary).status, "available");
|
|
258
|
+
exact.snapshot!.label += "x";
|
|
259
|
+
assert.equal((await f.run({ receipt: exact, question: "é".repeat(512) })).code, "input-too-large");
|
|
260
|
+
assert.equal(f.calls.length, 2);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("terminal outcomes are explicit; no tools execute, raw errors or oversized text escape", async () => {
|
|
264
|
+
const cases: [Partial<AssistantMessage>, string | undefined][] = [
|
|
265
|
+
[{ stopReason: "length" }, undefined], [{ stopReason: "error", errorMessage: "CREDENTIAL" }, "provider-error"],
|
|
266
|
+
[{ content: [{ type: "text", text: "é".repeat(2048) }] }, undefined],
|
|
267
|
+
[{ stopReason: "aborted" }, "cancelled"], [{ content: [] }, "empty-output"],
|
|
268
|
+
[{ content: [{ type: "text", text: "é".repeat(2049) }] }, "output-too-large"],
|
|
269
|
+
[{ content: [{ type: "toolCall", id: "x", name: "exec", arguments: {} }] }, "tool-call"],
|
|
270
|
+
];
|
|
271
|
+
for (const [patch, code] of cases) {
|
|
272
|
+
const f = fixture(), pending = f.run();
|
|
273
|
+
f.calls[0].resolve(message(patch));
|
|
274
|
+
const result = await pending;
|
|
275
|
+
assert.equal(result.code, code); assert.doesNotMatch(JSON.stringify(result), /CREDENTIAL/);
|
|
276
|
+
if (code) { assert.equal(result.status, "unavailable"); assert.equal(result.text, undefined); }
|
|
277
|
+
else { assert.equal(result.partial, patch.stopReason === "length"); assert.ok(result.text); }
|
|
278
|
+
assert.equal(f.calls.length, 1);
|
|
279
|
+
}
|
|
280
|
+
const f = fixture(), pending = f.run();
|
|
281
|
+
f.calls[0].reject(Error("CREDENTIAL"));
|
|
282
|
+
assert.equal((await pending).code, "provider-error");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("local deadline options never exceed 20 seconds and synchronous setup errors are sanitized", async () => {
|
|
286
|
+
for (const deadlineMs of [undefined, 50_000, NaN]) {
|
|
287
|
+
let calls = 0;
|
|
288
|
+
const engine = new OrchestratorHelper({ streamSimple() { calls++; throw Error("CREDENTIAL"); } }, { deadlineMs });
|
|
289
|
+
for (let i = 0; i < 2; i++) {
|
|
290
|
+
const result = await engine.run({ receipt: receipt(), question: "Question", model, isCurrent: () => true });
|
|
291
|
+
assert.equal(result.requestCaps.deadlineMs, 20_000);
|
|
292
|
+
assert.equal(result.code, "provider-error");
|
|
293
|
+
assert.doesNotMatch(JSON.stringify(result), /CREDENTIAL/);
|
|
294
|
+
}
|
|
295
|
+
assert.equal(calls, 2, "synchronous failure has no pending lease");
|
|
296
|
+
}
|
|
297
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
4
|
+
import { incomingMessageCard, outgoingMessageCall, outgoingMessageResult, type OrchestratorMessageDetails } from "../lib/orchestrator-message-card.ts";
|
|
5
|
+
import { CARD_STYLE, cardStyle, setCardStyle } from "../lib/shell-card.ts";
|
|
6
|
+
import { stripAnsi } from "../lib/terminal-theme.ts";
|
|
7
|
+
|
|
8
|
+
const theme = { fg: (_role: string, value: string) => value, bg: (_role: string, value: string) => `\x1b[48;2;20;20;20m${value}\x1b[49m` };
|
|
9
|
+
const data = {
|
|
10
|
+
message: "Ready for integration\nTests passed ✅\n第三行\nFinal line",
|
|
11
|
+
senderLabel: "API team", recipientLabel: "Integration",
|
|
12
|
+
senderSessionId: "sender-private-id", recipientSessionId: "recipient-private-id",
|
|
13
|
+
messageId: "message-private-id", reason: "Integration needs the test result", state: "accepted",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function outgoing(expanded = false, detail: OrchestratorMessageDetails = data) {
|
|
17
|
+
const args = { message: "mutable argument", reason: "mutable reason" };
|
|
18
|
+
const context = { state: {}, args, expanded, isPartial: false };
|
|
19
|
+
const call = outgoingMessageCall(args, theme, context, "expand");
|
|
20
|
+
const result = outgoingMessageResult({ content: [], details: { gentleAgents: detail } }, expanded, false, theme, context);
|
|
21
|
+
return { render: (width: number) => [...call.render(width), ...result.render(width)], invalidate: () => { call.invalidate(); result.invalidate(); } };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test("both directions share compact cards, labels, and expanded-only technical details", () => {
|
|
25
|
+
const incoming = incomingMessageCard({ ...data, correlationId: data.messageId }, "model envelope", false, theme, "expand");
|
|
26
|
+
for (const card of [incoming, outgoing()]) {
|
|
27
|
+
const text = stripAnsi(card.render(100).join("\n"));
|
|
28
|
+
assert.match(text, /🤖 API team → 🤖 Integration/);
|
|
29
|
+
assert.match(text, /Ready for integration/);
|
|
30
|
+
assert.doesNotMatch(text, /private-id|Integration needs|model envelope|mutable argument|Final line/);
|
|
31
|
+
}
|
|
32
|
+
for (const card of [incomingMessageCard(data, "model envelope", true, theme, "collapse"), outgoing(true)]) {
|
|
33
|
+
const text = stripAnsi(card.render(100).join("\n"));
|
|
34
|
+
for (const line of data.message.split("\n")) assert.ok(text.includes(line));
|
|
35
|
+
assert.match(text, /Sender session: sender-private-id/);
|
|
36
|
+
assert.match(text, /Recipient session: recipient-private-id/);
|
|
37
|
+
assert.match(text, /Message ID: message-private-id/);
|
|
38
|
+
}
|
|
39
|
+
assert.match(stripAnsi(outgoing(true).render(100).join("\n")), /not a delivery or read receipt/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("both peer labels and fallbacks have one renderer-owned robot icon without mutating stored aliases", () => {
|
|
43
|
+
const previous = cardStyle();
|
|
44
|
+
const cases = [
|
|
45
|
+
{ detail: Object.freeze({ ...data, senderLabel: "Recepción de handoff", recipientLabel: "Mejorar mensajes entre orquestadores" }), incoming: "🤖 Recepción de handoff → 🤖 Mejorar mensajes entre orquestadores", outgoing: "🤖 Recepción de handoff → 🤖 Mejorar mensajes entre orquestadores" },
|
|
46
|
+
{ detail: Object.freeze({ ...data, senderLabel: "ÁPI 团队", recipientLabel: "集成 e\u0301" }), incoming: "🤖 ÁPI 团队 → 🤖 集成 e\u0301", outgoing: "🤖 ÁPI 团队 → 🤖 集成 e\u0301" },
|
|
47
|
+
{ detail: Object.freeze({ message: data.message, state: data.state }), incoming: "🤖 Orchestrator → 🤖 You", outgoing: "🤖 You → 🤖 Orchestrator" },
|
|
48
|
+
];
|
|
49
|
+
try {
|
|
50
|
+
for (const style of Object.values(CARD_STYLE)) {
|
|
51
|
+
setCardStyle(style);
|
|
52
|
+
for (const entry of cases) {
|
|
53
|
+
const snapshot = { ...entry.detail };
|
|
54
|
+
for (const expanded of [false, true]) {
|
|
55
|
+
const cards = [
|
|
56
|
+
{ card: incomingMessageCard(entry.detail, "model envelope", expanded, theme, "expand"), route: entry.incoming },
|
|
57
|
+
{ card: outgoing(expanded, entry.detail), route: entry.outgoing },
|
|
58
|
+
];
|
|
59
|
+
for (const { card, route } of cards) {
|
|
60
|
+
for (const width of [150, 52, 60, 70, 12, 2, 1, 0, 150]) {
|
|
61
|
+
card.invalidate();
|
|
62
|
+
const rows = card.render(width);
|
|
63
|
+
for (const row of rows) assert.ok(visibleWidth(row) <= width, `${style} width ${width}: ${row}`);
|
|
64
|
+
if (width === 150) {
|
|
65
|
+
const text = stripAnsi(rows.join("\n"));
|
|
66
|
+
assert.ok(text.includes(route), `${style} route: ${route}`);
|
|
67
|
+
assert.equal(text.match(/🤖/gu)?.length, 2, "one icon per peer after repeated renders");
|
|
68
|
+
for (const line of data.message.split("\n").slice(0, expanded ? 4 : 3)) assert.ok(text.includes(line));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
assert.deepEqual(entry.detail, snapshot);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} finally { setCardStyle(previous); }
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("short outgoing results expand into readable routing and reason without duplicated payloads", () => {
|
|
80
|
+
const args = { message: "Ready", reason: "Peer needs this result", recipient_session_id: "requested-peer" };
|
|
81
|
+
const payload = { content: [{ type: "text", text: "Accepted notification; receipt-only-marker" }], details: { gentleAgents: { ...data, message: "Ready" } } };
|
|
82
|
+
const render = (expanded: boolean) => {
|
|
83
|
+
const context = { state: {}, args, expanded, isPartial: false };
|
|
84
|
+
const call = outgoingMessageCall(args, theme, context, expanded ? "collapse" : "expand");
|
|
85
|
+
const result = outgoingMessageResult(payload, expanded, false, theme, context);
|
|
86
|
+
return stripAnsi([...call.render(140), ...result.render(140)].join("\n"));
|
|
87
|
+
};
|
|
88
|
+
assert.doesNotMatch(render(false), /requested-peer|receipt-only-marker|Arguments:/);
|
|
89
|
+
assert.match(render(true), /Requested recipient: requested-peer/);
|
|
90
|
+
assert.match(render(true), /Reason: Integration needs the test result/);
|
|
91
|
+
assert.doesNotMatch(render(true), /Arguments:|Result details:|receipt-only-marker|[{}]/);
|
|
92
|
+
assert.equal(render(true).match(/Ready/g)?.length, 1, "message appears only once");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("legacy incoming envelopes are removed only when their exact stored binding matches", () => {
|
|
96
|
+
const old = { senderSessionId: "old-sender", correlationId: "old-message" };
|
|
97
|
+
const prefix = "Session message from old-sender (correlation old-message): ";
|
|
98
|
+
const render = (content: string) => incomingMessageCard(old, content, false, theme, "").render(150).join("\n");
|
|
99
|
+
assert.doesNotMatch(render(`${prefix}Hello`), /Session message from|old-sender|old-message/);
|
|
100
|
+
assert.match(render(`${prefix}Hello`), /Hello/);
|
|
101
|
+
assert.match(render("Session message from someone else: user text"), /someone else: user text/);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("message text, aliases, and expanded metadata cannot inject terminal escapes", () => {
|
|
105
|
+
const unsafe = "\x1b]52;c;clipboard\x07\x1b[31mVisible\x1b[0m\r\u0000\x9b2J";
|
|
106
|
+
const detail = { ...data, message: unsafe, senderLabel: unsafe, recipientLabel: unsafe, senderSessionId: unsafe, messageId: unsafe, reason: unsafe };
|
|
107
|
+
const context = { state: {}, args: {}, isPartial: false };
|
|
108
|
+
for (const card of [incomingMessageCard(detail, "", true, theme, ""), outgoingMessageResult({ content: [], details: { gentleAgents: detail } }, true, false, theme, context), outgoingMessageCall({}, theme, context, "")]) {
|
|
109
|
+
const text = stripAnsi(card.render(100).join("\n"));
|
|
110
|
+
assert.match(text, /Visible/);
|
|
111
|
+
assert.doesNotMatch(text, /clipboard|\x1b|\x9b|\r|\u0000/);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("cards respect widths and resize in neon and float styles, including wide text", () => {
|
|
116
|
+
const previous = cardStyle();
|
|
117
|
+
try {
|
|
118
|
+
for (const style of Object.values(CARD_STYLE)) {
|
|
119
|
+
setCardStyle(style);
|
|
120
|
+
for (const expanded of [false, true]) {
|
|
121
|
+
for (const card of [incomingMessageCard(data, "", expanded, theme, "expand"), outgoing(expanded)]) {
|
|
122
|
+
for (const width of [0, 1, 2, 4, 8, 12, 24, 80, 120, 24]) {
|
|
123
|
+
card.invalidate();
|
|
124
|
+
const rows = card.render(width);
|
|
125
|
+
if (width === 0) assert.deepEqual(rows, []);
|
|
126
|
+
for (const row of rows) assert.ok(visibleWidth(row) <= width, `${style} width ${width}: ${row}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
} finally { setCardStyle(previous); }
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
for (const style of Object.values(CARD_STYLE)) {
|
|
135
|
+
for (const state of ["pending", "accepted", "error"] as const) {
|
|
136
|
+
test(`long alias routes preserve single-row headings and body in ${style}/${state}`, () => {
|
|
137
|
+
const previous = cardStyle();
|
|
138
|
+
const detail = Object.freeze({ ...data, senderLabel: "Recepción de handoff", recipientLabel: "Mejorar mensajes entre orquestadores" });
|
|
139
|
+
const pending = state === "pending";
|
|
140
|
+
const title = pending ? "Sending message" : state === "error" ? "Message not sent" : "Message queued";
|
|
141
|
+
try {
|
|
142
|
+
setCardStyle(style);
|
|
143
|
+
for (const expanded of [false, true]) {
|
|
144
|
+
const context = { state: {}, args: { message: detail.message }, isPartial: pending, expanded };
|
|
145
|
+
const call = outgoingMessageCall(context.args, theme, context, "expand");
|
|
146
|
+
const result = outgoingMessageResult({ content: [{ type: "text", text: detail.message }], details: { gentleAgents: detail, ...(state === "error" ? { error: "not accepted" } : {}) } }, expanded, pending, theme, context);
|
|
147
|
+
for (const width of [52, 60, 70, 80, 100, 52]) {
|
|
148
|
+
call.invalidate();
|
|
149
|
+
const callRows = call.render(width);
|
|
150
|
+
const bodyRows = expanded ? 4 : 3;
|
|
151
|
+
const expectedRows = pending ? bodyRows + (style === CARD_STYLE.FLOAT ? 5 : 3) : style === CARD_STYLE.FLOAT ? 2 : 1;
|
|
152
|
+
assert.equal(callRows.length, expectedRows, `${style}/${state} call height at width ${width}`);
|
|
153
|
+
const rows = [...callRows, ...(pending ? [] : result.render(width))];
|
|
154
|
+
const text = stripAnsi(rows.join("\n"));
|
|
155
|
+
assert.ok(text.includes(title));
|
|
156
|
+
assert.ok(!rows.some(row => /^[│▎]\s*[A-Za-z]\s*[│ ]?$/.test(stripAnsi(row).trim())), "no single-letter heading continuation");
|
|
157
|
+
for (const row of rows) assert.ok(visibleWidth(row) <= width);
|
|
158
|
+
for (const line of detail.message.split("\n").slice(0, expanded ? 4 : 3)) assert.ok(text.includes(line), `body line preserved: ${line}`);
|
|
159
|
+
if (!expanded) assert.doesNotMatch(text, /Final line/);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} finally { setCardStyle(previous); }
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
test("pending, cancelled, failed, and recipient-selection results do not claim acceptance", () => {
|
|
168
|
+
const args = { message: "Preparing update" };
|
|
169
|
+
const context = { state: {}, args, isPartial: true, expanded: false };
|
|
170
|
+
const call = outgoingMessageCall(args, theme, context, "expand");
|
|
171
|
+
assert.match(stripAnsi(call.render(100).join("\n")), /Sending message/);
|
|
172
|
+
assert.match(stripAnsi(call.render(100).join("\n")), /Preparing update/);
|
|
173
|
+
for (const error of ["cancelled", "denied", "not accepted"]) {
|
|
174
|
+
const result = outgoingMessageResult({ content: [{ type: "text", text: `Error: ${error}` }], details: { error } }, false, false, theme, context);
|
|
175
|
+
const text = stripAnsi([...call.render(100), ...result.render(100)].join("\n"));
|
|
176
|
+
assert.match(text, /Message not sent/);
|
|
177
|
+
assert.ok(text.includes(`Error: ${error}`));
|
|
178
|
+
assert.doesNotMatch(text, /Message queued|Preparing update/);
|
|
179
|
+
}
|
|
180
|
+
const selection = outgoingMessageResult({ content: [{ type: "text", text: "Choose recipient" }], details: { gentleAgents: { candidates: [] } } }, false, false, theme, context);
|
|
181
|
+
const text = stripAnsi([...call.render(100), ...selection.render(100)].join("\n"));
|
|
182
|
+
assert.match(text, /Choose recipient/);
|
|
183
|
+
assert.doesNotMatch(text, /Message queued|Message not sent/);
|
|
184
|
+
const thrownContext = { ...context, isError: true };
|
|
185
|
+
const thrownCall = outgoingMessageCall(args, theme, thrownContext, "");
|
|
186
|
+
const thrown = outgoingMessageResult({ content: [{ type: "text", text: "transport exception" }] }, false, false, theme, thrownContext);
|
|
187
|
+
const thrownText = stripAnsi([...thrownCall.render(100), ...thrown.render(100)].join("\n"));
|
|
188
|
+
assert.match(thrownText, /Message not sent/);
|
|
189
|
+
assert.match(thrownText, /transport exception/);
|
|
190
|
+
});
|
|
@@ -33,6 +33,29 @@ function fixture(t: test.TestContext) {
|
|
|
33
33
|
return profile;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
test("heartbeat refreshes a bounded canonical label and stops an invalid source", (t) => {
|
|
37
|
+
const profile = fixture(t);
|
|
38
|
+
let heartbeat: (() => void) | undefined;
|
|
39
|
+
const interval = globalThis.setInterval;
|
|
40
|
+
t.mock.method(globalThis, "setInterval", (callback: () => void, ms: number) => {
|
|
41
|
+
if (ms === 5000) heartbeat = callback;
|
|
42
|
+
return interval(callback, ms);
|
|
43
|
+
});
|
|
44
|
+
let current = "Initial";
|
|
45
|
+
let valid = true;
|
|
46
|
+
const p = PresencePublisher.start({ profile, sessionId: "label-session", label: "Fallback", activity: [],
|
|
47
|
+
labelSource: () => { if (!valid) throw new Error("stale-session"); return current; } });
|
|
48
|
+
t.after(() => p.dispose());
|
|
49
|
+
current = "\u001b[31m Human\n rename\u202e ";
|
|
50
|
+
heartbeat!();
|
|
51
|
+
assert.equal(listPresence(profile).entries[0].label, "Human rename");
|
|
52
|
+
assert.equal(listPresence(profile).entries[0].generation, 1);
|
|
53
|
+
valid = false;
|
|
54
|
+
heartbeat!();
|
|
55
|
+
assert.equal(p.error, "stale-session");
|
|
56
|
+
assert.deepEqual(listPresence(profile).entries, []);
|
|
57
|
+
});
|
|
58
|
+
|
|
36
59
|
function rows(text = "full retained output") {
|
|
37
60
|
return [{ task: { id: "t", agent: "worker", label: "summary", status: "running", model: "model",
|
|
38
61
|
createdAt: 1000, startedAt: 1100, endedAt: null, lastActivityAt: 1200,
|
|
@@ -2,6 +2,7 @@ import assert from "node:assert/strict";
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import test from "node:test";
|
|
5
|
+
import { readDelegationDetail } from "./support/orchestrator-modules.ts";
|
|
5
6
|
|
|
6
7
|
const ROOT = join(import.meta.dirname, "..");
|
|
7
8
|
const ASSETS = join(ROOT, "assets");
|
|
@@ -13,7 +14,7 @@ function read(relativePath: string): string {
|
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
const core = read("assets/orchestrator.md");
|
|
16
|
-
const delegation =
|
|
17
|
+
const delegation = readDelegationDetail();
|
|
17
18
|
const staticPrompts = `${core}\n${delegation}`;
|
|
18
19
|
|
|
19
20
|
test("static prompts omit stale native RDD lifecycle mirrors", () => {
|
|
@@ -62,18 +63,18 @@ test("rendered parent prompt keeps the RDD boundary while omitting lifecycle mir
|
|
|
62
63
|
}
|
|
63
64
|
// gentle-pi#661: getOrchestratorPrompt()'s no-argument default renders the
|
|
64
65
|
// "unknown (native status unavailable)" RDD status line -- the longest of
|
|
65
|
-
// the three renderable forms -- so this IS the worst-case render the
|
|
66
|
+
// the three renderable forms -- so this IS the worst-case render the core
|
|
66
67
|
// budget below must cover, not a smaller placeholder production later
|
|
67
|
-
// exceeds.
|
|
68
|
+
// exceeds. gentle-shell#1731 T10 (user decision): budget 8,192 -> 8,400 B.
|
|
68
69
|
assert.ok(
|
|
69
70
|
rendered.includes("Receipt-driven development: unknown (native status unavailable)"),
|
|
70
71
|
"the default render must include the worst-case RDD status line",
|
|
71
72
|
);
|
|
72
|
-
assert.ok(Buffer.byteLength(rendered, "utf8") <=
|
|
73
|
+
assert.ok(Buffer.byteLength(rendered, "utf8") <= 8400, "the rendered parent prompt must stay within the 8,400 B core budget");
|
|
73
74
|
});
|
|
74
75
|
|
|
75
76
|
test("static prompts retain ODD and delegated-work guidance without SDD", () => {
|
|
76
|
-
for (const heading of ["## Memory Contract", "##
|
|
77
|
+
for (const heading of ["## Memory Contract", "## Task Size", "## Mechanisms"]) {
|
|
77
78
|
assert.ok(core.includes(heading), `core lost ${heading}`);
|
|
78
79
|
}
|
|
79
80
|
for (const heading of [
|