gentle-pi 3.7.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/assets/agents/gentle-ai-explore.md +13 -6
- package/assets/agents/gentle-ai-verify.md +27 -6
- package/assets/agents/gentle-ai-worker.md +13 -15
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +75 -169
- package/assets/orchestrator-memory.md +12 -26
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +30 -38
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +956 -50
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/bundled-install.md +397 -0
- package/docs/delegated-verification.md +27 -4
- package/docs/gentle-agents-activity.md +537 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +165 -51
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +3 -3
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +364 -231
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +35 -0
- package/extensions/child-safety.ts +25 -0
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +1093 -326
- package/extensions/gentle-ai.ts +1636 -890
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +1619 -136
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +126 -20
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1424 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +254 -92
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +191 -58
- package/extensions/startup-banner.ts +71 -33
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +99 -0
- package/lib/agents-runner.ts +180 -107
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +220 -0
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +195 -0
- package/lib/command-palette-catalog.ts +5 -10
- package/lib/command-palette.ts +25 -14
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +89 -27
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +249 -0
- package/lib/native-review-cli.ts +242 -180
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +73 -21
- package/lib/review-candidate-view.ts +115 -27
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +227 -56
- package/lib/review-sidebar-state.ts +224 -0
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +527 -0
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +87 -9
- package/lib/shell-bar.ts +215 -30
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +42 -14
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +83 -15
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +84 -31
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +70 -13
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +383 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +119 -0
- package/lib/visual-customize-view.ts +481 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +317 -0
- package/package.json +20 -8
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +87 -25
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +239 -177
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +225 -54
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +8 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +83 -35
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +90 -0
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +54 -19
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +91 -38
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +16 -21
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +30 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +97 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +467 -93
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +146 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +87 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +94 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +596 -0
- package/tests/command-palette.test.ts +43 -7
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -78
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +171 -3
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +69 -0
- package/tests/gentle-agents.test.ts +2804 -346
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +21 -4
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +566 -62
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +493 -14
- package/tests/gentle-ai.test.ts +1427 -236
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +896 -115
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +333 -65
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +4216 -119
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +333 -10
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +49 -19
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +481 -0
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/native-review-cli.test.ts +239 -118
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +101 -4
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +76 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
- package/tests/odd-routing-contract.test.ts +158 -50
- package/tests/orchestrator-budget.test.ts +43 -45
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +8 -7
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +344 -123
- package/tests/packed-runner-owned-path.test.ts +80 -0
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +10 -18
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +7 -14
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +569 -187
- package/tests/rdd-aware-verification-contract.test.ts +103 -40
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +237 -21
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +399 -12
- package/tests/review-contract-prompt.test.ts +138 -44
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +866 -6
- package/tests/review-controller-workspace-root.test.ts +206 -6
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +342 -4
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +221 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +14 -37
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +594 -26
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +202 -0
- package/tests/runtime-harness.mjs +186 -800
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +451 -0
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +134 -2
- package/tests/shell-bar.test.ts +422 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +135 -32
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +351 -22
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +207 -3
- package/tests/shell-usage-view.test.ts +28 -1
- package/tests/shell-usage.test.ts +108 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +357 -2
- package/tests/startup-banner.test.ts +246 -4
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +107 -20
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/theme-customization.test.ts +72 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +139 -0
- package/tests/visual-customize-view.test.ts +696 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +271 -0
- package/tests/yolo-mode-runtime.test.ts +188 -0
- package/tests/yolo-mode.test.ts +289 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
|
@@ -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
|
+
});
|
|
@@ -9,7 +9,7 @@ export interface FakeChild {
|
|
|
9
9
|
child: ChildLike;
|
|
10
10
|
written: Array<Record<string, unknown>>;
|
|
11
11
|
emit(event: Record<string, unknown>): void;
|
|
12
|
-
exit(code: number): void;
|
|
12
|
+
exit(code: number | null, signal?: string | null): void;
|
|
13
13
|
fail(message: string): void;
|
|
14
14
|
killed: string[];
|
|
15
15
|
sent: Array<Record<string, unknown>>;
|
|
@@ -62,5 +62,5 @@ export function fakeChild(options: { exitOnKill?: boolean; pid?: number } = {}):
|
|
|
62
62
|
return child;
|
|
63
63
|
},
|
|
64
64
|
};
|
|
65
|
-
return { child, written, killed, sent, get disconnects() { return disconnects; }, message: (event) => emitter.emit("message", event), emit: (event) => stdout.write(`${JSON.stringify(event)}\n`), exit: (code) => emitter.emit("exit", code,
|
|
65
|
+
return { child, written, killed, sent, get disconnects() { return disconnects; }, message: (event) => emitter.emit("message", event), emit: (event) => stdout.write(`${JSON.stringify(event)}\n`), exit: (code, signal = null) => emitter.emit("exit", code, signal), fail: (message) => emitter.emit("error", new Error(message)) };
|
|
66
66
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createAgentMessageQueue, STALE_NOTIFICATION_MS } from "../lib/agents-message-delivery.ts";
|
|
4
|
+
|
|
5
|
+
test("delivers live notifications and queries in order for the active session", () => {
|
|
6
|
+
const queue = createAgentMessageQueue();
|
|
7
|
+
queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "First progress", 1000);
|
|
8
|
+
queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Which branch?", 1050);
|
|
9
|
+
|
|
10
|
+
assert.equal(queue.pendingCount(), 2);
|
|
11
|
+
const deliverable = queue.takeDeliverable(1100, "s1", () => true);
|
|
12
|
+
assert.equal(deliverable.length, 2);
|
|
13
|
+
|
|
14
|
+
assert.equal(deliverable[0]?.kind, "notification");
|
|
15
|
+
assert.equal(deliverable[0]?.content, "First progress");
|
|
16
|
+
assert.equal(deliverable[0]?.display, false);
|
|
17
|
+
|
|
18
|
+
assert.equal(deliverable[1]?.kind, "query");
|
|
19
|
+
assert.match(deliverable[1]?.content ?? "", /Which branch\?/);
|
|
20
|
+
assert.equal(deliverable[1]?.display, true);
|
|
21
|
+
assert.equal(deliverable[1]?.requestId, "q1");
|
|
22
|
+
|
|
23
|
+
assert.equal(queue.pendingCount(), 0);
|
|
24
|
+
assert.equal(queue.takeDeliverable(1200, "s1", () => true).length, 0, "messages are delivered at most once");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("a notification from a task that settled before delivery is dropped silently", () => {
|
|
28
|
+
const queue = createAgentMessageQueue();
|
|
29
|
+
queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "Late notice", 1000);
|
|
30
|
+
|
|
31
|
+
// The task finishes and is invalidated before the parent turn boundary.
|
|
32
|
+
queue.invalidateTask("task-1");
|
|
33
|
+
|
|
34
|
+
const deliverable = queue.takeDeliverable(1050, "s1", () => false);
|
|
35
|
+
assert.equal(deliverable.length, 0, "notification from settled task must never enter conversation");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("an answered query is consumed and never delivered", () => {
|
|
39
|
+
const queue = createAgentMessageQueue();
|
|
40
|
+
queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Which target?", 1000);
|
|
41
|
+
|
|
42
|
+
// Parent replies before queue flush.
|
|
43
|
+
queue.consumeQuery("task-1", "q1");
|
|
44
|
+
|
|
45
|
+
const deliverable = queue.takeDeliverable(1050, "s1", () => true);
|
|
46
|
+
assert.equal(deliverable.length, 0, "answered query must not be delivered");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("an expired query is dropped and never delivered", () => {
|
|
50
|
+
const queue = createAgentMessageQueue();
|
|
51
|
+
queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Need approval", 1000);
|
|
52
|
+
|
|
53
|
+
// Query times out.
|
|
54
|
+
queue.expireQuery("task-1", "q1");
|
|
55
|
+
|
|
56
|
+
const deliverable = queue.takeDeliverable(1050, "s1", () => true);
|
|
57
|
+
assert.equal(deliverable.length, 0, "expired query must not be delivered");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a notification exceeding the stale window is dropped", () => {
|
|
61
|
+
const queue = createAgentMessageQueue();
|
|
62
|
+
queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "Slow notice", 1000);
|
|
63
|
+
|
|
64
|
+
// Held past the 90-second threshold.
|
|
65
|
+
const deliverable = queue.takeDeliverable(1000 + STALE_NOTIFICATION_MS + 10, "s1", () => true);
|
|
66
|
+
assert.equal(deliverable.length, 0, "stale notification must not be delivered");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("isTaskLive returning false drops messages for non-live tasks", () => {
|
|
70
|
+
const queue = createAgentMessageQueue();
|
|
71
|
+
queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "Dead task notice", 1000);
|
|
72
|
+
queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Dead task query", 1000);
|
|
73
|
+
|
|
74
|
+
const deliverable = queue.takeDeliverable(1050, "s1", (id) => id !== "task-1");
|
|
75
|
+
assert.equal(deliverable.length, 0, "non-live task messages must be dropped");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("session mismatch drops foreign session messages", () => {
|
|
79
|
+
const queue = createAgentMessageQueue();
|
|
80
|
+
queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "session-old" }, "Old session notice", 1000);
|
|
81
|
+
|
|
82
|
+
const deliverable = queue.takeDeliverable(1050, "session-new", () => true);
|
|
83
|
+
assert.equal(deliverable.length, 0, "foreign session message must be dropped");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("deduplicates identical query requests for the same task and requestId", () => {
|
|
87
|
+
const queue = createAgentMessageQueue();
|
|
88
|
+
queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "First ask", 1000);
|
|
89
|
+
queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Duplicate ask", 1001);
|
|
90
|
+
|
|
91
|
+
assert.equal(queue.pendingCount(), 1);
|
|
92
|
+
const deliverable = queue.takeDeliverable(1050, "s1", () => true);
|
|
93
|
+
assert.equal(deliverable.length, 1);
|
|
94
|
+
assert.match(deliverable[0]!.content, /First ask/);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("dropAll clears everything across session reset", () => {
|
|
98
|
+
const queue = createAgentMessageQueue();
|
|
99
|
+
queue.enqueueNotification({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "Notice", 1000);
|
|
100
|
+
queue.enqueueQuery({ id: "task-1", agent: "scout", parentSessionId: "s1" }, "q1", "Query", 1000);
|
|
101
|
+
assert.equal(queue.pendingCount(), 2);
|
|
102
|
+
|
|
103
|
+
queue.dropAll();
|
|
104
|
+
assert.equal(queue.pendingCount(), 0);
|
|
105
|
+
assert.equal(queue.takeDeliverable(1050, "s1", () => true).length, 0);
|
|
106
|
+
});
|
|
@@ -3,7 +3,9 @@ import test from "node:test";
|
|
|
3
3
|
import {
|
|
4
4
|
applyTaskEvent,
|
|
5
5
|
emptyThread,
|
|
6
|
+
MISSING_TOOLS_NOTE_PREFIX,
|
|
6
7
|
normalizeRpcEvent,
|
|
8
|
+
ToolArgumentProgress,
|
|
7
9
|
TASK_EVENT,
|
|
8
10
|
TASK_STATUS,
|
|
9
11
|
taskLabel,
|
|
@@ -12,6 +14,51 @@ import {
|
|
|
12
14
|
type TaskRecord,
|
|
13
15
|
} from "../lib/agents-protocol.ts";
|
|
14
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
|
+
|
|
15
62
|
// Gentle Agents protocol: the child pi process streams RPC events; the host
|
|
16
63
|
// normalizes them into small typed deltas, applies them to an append-only
|
|
17
64
|
// thread, and notifies only the listeners of the task that changed.
|
|
@@ -67,6 +114,56 @@ test("normalizeRpcEvent maps pi RPC events to task deltas and ignores the rest",
|
|
|
67
114
|
assert.deepEqual(normalizeRpcEvent("garbage"), []);
|
|
68
115
|
});
|
|
69
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
|
+
|
|
128
|
+
test("argument liveness accepts current and legacy Pi starts without display or usage deltas", () => {
|
|
129
|
+
for (const start of [{ type: "toolcall_start", contentIndex: 0, id: "c1", toolName: "write" },
|
|
130
|
+
{ type: "toolcall_start", contentIndex: 0, partial: { content: [{ type: "toolCall", id: "c1", name: "write", arguments: {} }] } }]) {
|
|
131
|
+
const progress = new ToolArgumentProgress();
|
|
132
|
+
const update = (inner: Record<string, unknown>) => ({ type: "message_update", assistantMessageEvent: inner });
|
|
133
|
+
const delta = update({ type: "toolcall_delta", contentIndex: 0, delta: "private" });
|
|
134
|
+
assert.equal(progress.observe(delta), false, "no active generation");
|
|
135
|
+
progress.observe({ type: "message_start", message: { role: "assistant", timestamp: 1 } });
|
|
136
|
+
assert.equal(progress.observe(delta), false, "no announced block");
|
|
137
|
+
assert.equal(progress.observe(update(start)), false, "announcement alone is not progress");
|
|
138
|
+
assert.equal(progress.observe(delta), true);
|
|
139
|
+
assert.equal(progress.observe(delta), false);
|
|
140
|
+
assert.deepEqual(normalizeRpcEvent(delta), [], "no arguments in display events or provisional totals");
|
|
141
|
+
progress.observe(update({ type: "toolcall_end", contentIndex: 0 }));
|
|
142
|
+
assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: "late" })), false);
|
|
143
|
+
progress.observe(update(start));
|
|
144
|
+
assert.equal(progress.observe(delta), false, "a closed block cannot be reopened");
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("argument liveness fails closed on malformed starts, stale generations and fingerprint exhaustion", () => {
|
|
149
|
+
const progress = new ToolArgumentProgress();
|
|
150
|
+
const update = (inner: Record<string, unknown>) => ({ type: "message_update", assistantMessageEvent: inner });
|
|
151
|
+
for (const timestamp of [undefined, "1", -1, 1.5]) progress.observe({ type: "message_start", message: { role: "assistant", timestamp } });
|
|
152
|
+
assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: "x" })), false);
|
|
153
|
+
progress.observe({ type: "message_start", message: { role: "assistant", timestamp: 1 } });
|
|
154
|
+
for (const fields of [{ id: 1, toolName: "write" }, { id: "c", toolName: "" }, { partial: null }]) {
|
|
155
|
+
progress.observe(update({ type: "toolcall_start", contentIndex: 0, ...fields }));
|
|
156
|
+
assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: "x" })), false);
|
|
157
|
+
}
|
|
158
|
+
progress.observe(update({ type: "toolcall_start", contentIndex: 0, id: "c", toolName: "write" }));
|
|
159
|
+
for (let index = 0; index < 4096; index++) assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: String(index) })), true);
|
|
160
|
+
assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: "over-cap" })), false);
|
|
161
|
+
progress.observe({ type: "message_end" });
|
|
162
|
+
progress.observe({ type: "message_start", message: { role: "assistant", timestamp: 1 } });
|
|
163
|
+
progress.observe(update({ type: "toolcall_start", contentIndex: 0, id: "c", toolName: "write" }));
|
|
164
|
+
assert.equal(progress.observe(update({ type: "toolcall_delta", contentIndex: 0, delta: "stale" })), false);
|
|
165
|
+
});
|
|
166
|
+
|
|
70
167
|
test("response observations are opt-in, finalized, field-specific and content-free", () => {
|
|
71
168
|
const message = { role: "assistant", provider: "openai", model: "gpt-4o", responseModel: "gpt-4o-2024-08-06",
|
|
72
169
|
providerThinkingLevel: "high", stopReason: "stop", content: [{ type: "text", text: "private response" }],
|
|
@@ -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;
|