gentle-pi 3.7.0 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -10
- package/assets/agents/gentle-ai-explore.md +13 -6
- package/assets/agents/gentle-ai-verify.md +27 -6
- package/assets/agents/gentle-ai-worker.md +13 -15
- package/assets/install-wizard/index.html +47 -0
- package/assets/install-wizard/wizard.css +1279 -0
- package/assets/install-wizard/wizard.js +951 -0
- package/assets/orchestrator-delegation.md +75 -169
- package/assets/orchestrator-memory.md +12 -26
- package/assets/orchestrator-prompts.md +48 -0
- package/assets/orchestrator-skills.md +1 -1
- package/assets/orchestrator-tracking.md +53 -0
- package/assets/orchestrator-verification.md +28 -0
- package/assets/orchestrator-writer.md +38 -0
- package/assets/orchestrator.md +30 -38
- package/assets/sounds/LICENSE.md +52 -0
- package/assets/sounds/attention.wav +0 -0
- package/assets/sounds/error.wav +0 -0
- package/assets/sounds/success.wav +0 -0
- package/assets/support/strict-tdd-verify.md +4 -266
- package/assets/support/strict-tdd.md +8 -360
- package/bin/engram-guard +9 -0
- package/bin/gentle-shell-install.mjs +302 -0
- package/bin/gentle-shell.mjs +956 -50
- package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
- package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
- package/docs/bundled-install.md +397 -0
- package/docs/delegated-verification.md +27 -4
- package/docs/gentle-agents-activity.md +537 -0
- package/docs/gentle-shell-uninstall.md +136 -0
- package/docs/gentle-shell.md +165 -51
- package/docs/install-wizard.md +2040 -0
- package/docs/native-authority-architecture.md +3 -3
- package/docs/permission-modes.md +82 -0
- package/docs/prompt-history.md +280 -0
- package/docs/readme-reference.md +364 -231
- package/docs/render-cache-performance.md +28 -0
- package/docs/sound-notifications-proposal.md +191 -0
- package/docs/sound-notifications.md +114 -0
- package/docs/telemetry.md +1 -1
- package/docs/yolo-mode.md +86 -0
- package/extensions/child-context.ts +35 -0
- package/extensions/child-safety.ts +25 -0
- package/extensions/codegraph-tools.ts +18 -5
- package/extensions/gentle-agents.ts +1093 -326
- package/extensions/gentle-ai.ts +1636 -890
- package/extensions/gentle-herdr-activity.ts +81 -0
- package/extensions/gentle-notifications.ts +104 -0
- package/extensions/gentle-shell.ts +1619 -136
- package/extensions/gentle-stats.ts +101 -0
- package/extensions/gentle-todo.ts +126 -20
- package/extensions/history/atomic-write.ts +38 -0
- package/extensions/history/hide-prompts.ts +183 -0
- package/extensions/history/index.ts +1424 -0
- package/extensions/history/load-shared-history.ts +39 -0
- package/extensions/history/selector-helpers.ts +538 -0
- package/extensions/history/session-scan.ts +233 -0
- package/extensions/history/store.ts +1119 -0
- package/extensions/nan-provider.ts +6 -0
- package/extensions/pi-pretty.ts +39 -3
- package/extensions/quiet-tools.ts +254 -92
- package/extensions/resume-hint.ts +60 -0
- package/extensions/skill-registry.ts +191 -58
- package/extensions/startup-banner.ts +71 -33
- package/lib/agent-assets.ts +604 -0
- package/lib/agent-home.ts +10 -1
- package/lib/agent-model-resolution.ts +80 -0
- package/lib/agent-profile-pin.ts +12 -0
- package/lib/agent-profiles.ts +13 -3
- package/lib/agents-completion-delivery.ts +11 -3
- package/lib/agents-message-delivery.ts +181 -0
- package/lib/agents-messaging.ts +1 -1
- package/lib/agents-protocol.ts +99 -0
- package/lib/agents-runner.ts +180 -107
- package/lib/agents-view.ts +26 -3
- package/lib/agents-widget.ts +16 -9
- package/lib/append-system-prompt.ts +21 -0
- package/lib/auto-permission-classifier.ts +280 -0
- package/lib/auto-permission-runtime.ts +268 -0
- package/lib/background-jobs-tools.ts +202 -0
- package/lib/background-jobs.ts +253 -0
- package/lib/background-monitor.ts +154 -0
- package/lib/bounded-writer-admission.ts +220 -0
- package/lib/builtin-codemode-optout.ts +166 -0
- package/lib/card-content-policy.ts +85 -0
- package/lib/card-style-policy.ts +60 -0
- package/lib/child-context-files.ts +166 -0
- package/lib/child-package-injection.ts +62 -0
- package/lib/codemode-renderer.ts +195 -0
- package/lib/command-palette-catalog.ts +5 -10
- package/lib/command-palette.ts +25 -14
- package/lib/context-threshold.ts +26 -0
- package/lib/destructive-command-guard.ts +144 -0
- package/lib/gentle-ai-binary.ts +109 -4
- package/lib/gentle-ai-elapsed-store.ts +87 -0
- package/lib/gentle-ai-renderer.ts +196 -39
- package/lib/gentle-shell-bundled.ts +517 -0
- package/lib/gentle-shell-launcher.ts +89 -27
- package/lib/gentle-shell-resume-hint.ts +176 -0
- package/lib/gentle-shell-startup-flow.ts +54 -0
- package/lib/gentle-shell-startup-update.ts +92 -0
- package/lib/gentle-shell-uninstall.ts +840 -0
- package/lib/gentle-shell-update-panel.ts +231 -0
- package/lib/herdr-activity.ts +183 -0
- package/lib/history-capture-policy.ts +95 -0
- package/lib/inprocess-reviewer.ts +3 -0
- package/lib/jobs-sidebar-state.ts +20 -0
- package/lib/jobs-view.ts +161 -0
- package/lib/model-price-ratio.ts +83 -0
- package/lib/model-routing-authority.ts +5 -1
- package/lib/nan-provider.ts +249 -0
- package/lib/native-review-cli.ts +242 -180
- package/lib/notification-audio-native.ts +162 -0
- package/lib/notification-audio-windows.ts +399 -0
- package/lib/notification-audio.ts +332 -0
- package/lib/notification-customize.ts +241 -0
- package/lib/notification-events.ts +113 -0
- package/lib/notification-policy.ts +148 -0
- package/lib/notification-pulse-client.ts +367 -0
- package/lib/notification-pulse-protocol.ts +305 -0
- package/lib/notification-pulse-stream.ts +213 -0
- package/lib/notification-pulse-worker.ts +66 -0
- package/lib/notification-scheduler.ts +173 -0
- package/lib/notification-service.ts +202 -0
- package/lib/notification-ui.ts +87 -0
- package/lib/odd-phase-inference.ts +231 -0
- package/lib/odd-phase.ts +141 -0
- package/lib/orchestrator-alias.ts +20 -0
- package/lib/orchestrator-catalog.ts +79 -0
- package/lib/orchestrator-consultation.ts +61 -0
- package/lib/orchestrator-discovery.ts +49 -0
- package/lib/orchestrator-helper-consent.ts +122 -0
- package/lib/orchestrator-helper.ts +167 -0
- package/lib/orchestrator-message-card.ts +105 -0
- package/lib/orchestrator-presence.ts +115 -14
- package/lib/orchestrator-scope.ts +80 -0
- package/lib/orchestrator-state.ts +107 -0
- package/lib/orchestrator-tool-card.ts +280 -0
- package/lib/orchestrator-work-search.ts +218 -0
- package/lib/orchestrator-work.ts +79 -0
- package/lib/overlay-repaint.ts +26 -0
- package/lib/permission-mode-policy.ts +59 -0
- package/lib/pi-tui-keys.ts +53 -0
- package/lib/review-candidate-view-owner.ts +73 -21
- package/lib/review-candidate-view.ts +115 -27
- package/lib/review-compact-contract.ts +2 -2
- package/lib/review-host-relay.ts +7 -2
- package/lib/review-integration-v2.ts +100 -22
- package/lib/review-publication-gate.ts +1 -1
- package/lib/review-reminder-receipt.ts +48 -8
- package/lib/review-risk-assessment.ts +227 -56
- package/lib/review-sidebar-state.ts +224 -0
- package/lib/review-transaction.ts +72 -3
- package/lib/runtime-metrics-children.ts +1 -1
- package/lib/runtime-metrics-native.ts +3 -2
- package/lib/selection-engine.ts +527 -0
- package/lib/session-change-capture.ts +5 -4
- package/lib/session-changes.ts +27 -22
- package/lib/session-messaging-grants.ts +135 -0
- package/lib/session-profile-binding.ts +83 -0
- package/lib/session-profile-freeze.ts +266 -0
- package/lib/session-worktree-registry.ts +87 -9
- package/lib/shell-bar.ts +215 -30
- package/lib/shell-card.ts +287 -18
- package/lib/shell-changes-view.ts +42 -14
- package/lib/shell-changes.ts +86 -71
- package/lib/shell-prompt.ts +102 -6
- package/lib/shell-sidebar-layout.ts +83 -15
- package/lib/shell-sidebar.ts +15 -1
- package/lib/shell-todo.ts +84 -31
- package/lib/shell-usage-view.ts +9 -4
- package/lib/shell-usage.ts +70 -13
- package/lib/stats-collector.ts +381 -0
- package/lib/stats-view.ts +431 -0
- package/lib/status-timing-diagnostics.ts +201 -0
- package/lib/telemetry-trigger.ts +3 -1
- package/lib/theme-customization.ts +52 -0
- package/lib/vim-editor-adapter.ts +383 -0
- package/lib/vim-normal-engine.ts +154 -0
- package/lib/vim-operator-engine.ts +416 -0
- package/lib/vim-policy.ts +49 -0
- package/lib/vim-visual-engine.ts +107 -0
- package/lib/visual-customization-policy.ts +119 -0
- package/lib/visual-customize-view.ts +481 -0
- package/lib/visual-profiles.ts +228 -0
- package/lib/writer-surfaces.ts +162 -0
- package/lib/yolo-session-policy.ts +317 -0
- package/package.json +20 -8
- package/runtime/child-package-injection.mjs +63 -0
- package/runtime/gentle-ai-binary.mjs +109 -4
- package/runtime/gentle-shell-bundled.mjs +518 -0
- package/runtime/gentle-shell-launcher.mjs +87 -25
- package/runtime/gentle-shell-resume-hint.mjs +177 -0
- package/runtime/gentle-shell-startup-flow.mjs +55 -0
- package/runtime/gentle-shell-startup-update.mjs +93 -0
- package/runtime/gentle-shell-uninstall.mjs +841 -0
- package/runtime/gentle-shell-update-panel.mjs +232 -0
- package/runtime/native-review-cli.mjs +239 -177
- package/runtime/review-integration-v2.mjs +100 -22
- package/runtime/review-risk-assessment.mjs +225 -54
- package/runtime/status-timing-diagnostics.mjs +202 -0
- package/runtime/telemetry-trigger.mjs +3 -1
- package/scripts/bootstrap.cmd +255 -0
- package/scripts/bootstrap.sh +168 -0
- package/scripts/build-distribution.mjs +157 -0
- package/scripts/build-installer-bundles.mjs +136 -0
- package/scripts/build-runtime-modules.mjs +8 -0
- package/scripts/bundled-install.mjs +1308 -0
- package/scripts/bundled-wizard.mjs +399 -0
- package/scripts/check-types.mjs +4 -6
- package/scripts/gentle-ai-installer.mjs +83 -35
- package/scripts/install-gentle-ai.mjs +48 -1
- package/scripts/install-tui-mode-setting.mjs +10 -1
- package/scripts/install-wizard-preview.mjs +154 -0
- package/scripts/installer-downloads.mjs +648 -0
- package/scripts/installer-preflight.mjs +387 -0
- package/scripts/installer-probes.mjs +564 -0
- package/scripts/installer-runner.mjs +1102 -0
- package/scripts/installer-server.mjs +881 -0
- package/scripts/installer-windows-artifacts.json +15 -0
- package/scripts/installer-windows.mjs +752 -0
- package/scripts/main-channel.mjs +452 -0
- package/scripts/mirror-odd-routing.mjs +2 -2
- package/scripts/npm-pack-result.mjs +31 -0
- package/scripts/render-cache-performance-worker.mjs +205 -0
- package/scripts/render-cache-performance.mjs +115 -0
- package/scripts/run-test-suite.mjs +90 -0
- package/scripts/test-installer-acceptance.sh +161 -0
- package/scripts/test-packed-runner.mjs +54 -19
- package/scripts/types-baseline.json +2 -92
- package/scripts/verify-package-files.mjs +91 -38
- package/skills/branch-pr/SKILL.md +24 -52
- package/skills/chained-pr/SKILL.md +31 -15
- package/skills/chained-pr/references/chaining-details.md +31 -20
- package/skills/gentle-ai/SKILL.md +16 -21
- package/skills/issue-creation/SKILL.md +8 -2
- package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
- package/skills/work-unit-commits/SKILL.md +4 -3
- package/tests/agent-home.test.ts +56 -3
- package/tests/agent-model-resolution.test.ts +113 -0
- package/tests/agent-profiles.test.ts +30 -0
- package/tests/agent-risk-escalation.test.ts +123 -0
- package/tests/agents-completion-delivery.test.ts +17 -0
- package/tests/agents-fake-child.ts +2 -2
- package/tests/agents-message-delivery.test.ts +106 -0
- package/tests/agents-protocol.test.ts +97 -0
- package/tests/agents-queries.test.ts +9 -9
- package/tests/agents-runner-process.test.ts +31 -0
- package/tests/agents-runner.test.ts +467 -93
- package/tests/agents-view-thread-identity.test.ts +169 -0
- package/tests/agents-view.test.ts +8 -2
- package/tests/agents-widget.test.ts +154 -15
- package/tests/append-system-prompt-route.test.ts +160 -0
- package/tests/append-system-prompt.test.ts +46 -0
- package/tests/artifact-language.test.ts +19 -213
- package/tests/ask-user-question.test.ts +44 -1
- package/tests/asset-installation-runtime.test.ts +5 -16
- package/tests/auto-mode-invariants.test.ts +258 -0
- package/tests/auto-mode-sdk.test.ts +57 -0
- package/tests/auto-mode.test.ts +49 -0
- package/tests/auto-permission-classifier.test.ts +283 -0
- package/tests/auto-permission-runtime.test.ts +196 -0
- package/tests/autonomous-guard.test.ts +69 -1
- package/tests/background-cache-warming.test.ts +2 -1
- package/tests/background-jobs.test.ts +227 -0
- package/tests/background-monitor.test.ts +195 -0
- package/tests/background-subagents-default-mode.test.ts +28 -5
- package/tests/bounded-stop-contract.test.ts +85 -0
- package/tests/bounded-writer-admission.test.ts +146 -0
- package/tests/branch-pr-skill.test.ts +43 -0
- package/tests/bridge-wake-lifecycle.test.ts +129 -0
- package/tests/bridge-wake-rendering.test.ts +56 -0
- package/tests/build-distribution.test.ts +269 -0
- package/tests/builtin-codemode-optout.test.ts +242 -0
- package/tests/bundled-install.test.ts +1319 -0
- package/tests/bundled-wizard.test.ts +505 -0
- package/tests/card-content-policy.test.ts +68 -0
- package/tests/card-style-policy.test.ts +87 -0
- package/tests/chained-pr-skill.test.ts +124 -0
- package/tests/child-codegraph-bootstrap.test.ts +74 -0
- package/tests/child-context-files.test.ts +255 -0
- package/tests/child-package-entrypoints.test.ts +65 -0
- package/tests/child-package-injection.test.ts +74 -0
- package/tests/child-safety.test.ts +94 -0
- package/tests/child-web-package.test.ts +224 -0
- package/tests/codegraph-tools.test.ts +35 -0
- package/tests/codemode-rendering.test.ts +596 -0
- package/tests/command-palette.test.ts +43 -7
- package/tests/context-threshold.test.ts +31 -0
- package/tests/delegate-for-reason-contract.test.ts +327 -0
- package/tests/delegated-key-learnings-contract.test.ts +3 -78
- package/tests/destructive-command-guard.test.ts +84 -0
- package/tests/devbinary/native-review-parity.devtest.ts +171 -3
- package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
- package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
- package/tests/engram-safety.test.ts +288 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
- package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
- package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
- package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
- package/tests/generic-agent-tools.test.ts +69 -0
- package/tests/gentle-agents.test.ts +2804 -346
- package/tests/gentle-ai-binary-isolation.test.ts +135 -0
- package/tests/gentle-ai-binary.test.ts +21 -4
- package/tests/gentle-ai-child-guards.test.ts +202 -0
- package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
- package/tests/gentle-ai-elapsed-store.test.ts +68 -0
- package/tests/gentle-ai-installer.test.ts +566 -62
- package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
- package/tests/gentle-ai-renderer.test.ts +493 -14
- package/tests/gentle-ai.test.ts +1427 -236
- package/tests/gentle-card-text.ts +2 -1
- package/tests/gentle-herdr-activity.test.ts +100 -0
- package/tests/gentle-notifications.test.ts +427 -0
- package/tests/gentle-shell-bin.test.ts +896 -115
- package/tests/gentle-shell-bundled.test.ts +1002 -0
- package/tests/gentle-shell-launcher.test.ts +333 -65
- package/tests/gentle-shell-resume-hint.test.ts +270 -0
- package/tests/gentle-shell-startup-flow.test.ts +75 -0
- package/tests/gentle-shell-startup-update.test.ts +85 -0
- package/tests/gentle-shell-uninstall.test.ts +978 -0
- package/tests/gentle-shell-update-panel.test.ts +158 -0
- package/tests/gentle-shell.test.ts +4216 -119
- package/tests/gentle-stats.test.ts +152 -0
- package/tests/gentle-theme.test.ts +4 -1
- package/tests/gentle-todo.test.ts +333 -10
- package/tests/herdr-activity.test.ts +243 -0
- package/tests/history-agent-home.test.ts +81 -0
- package/tests/history-atomic-write.test.ts +57 -0
- package/tests/history-capture-policy.test.ts +102 -0
- package/tests/history-child-guard.test.ts +70 -0
- package/tests/history-command-registration.test.ts +164 -0
- package/tests/history-dedupe-entries.test.ts +123 -0
- package/tests/history-delete-backfill.test.ts +190 -0
- package/tests/history-delete-confirm.test.ts +460 -0
- package/tests/history-dispatch.test.ts +180 -0
- package/tests/history-drain-hidden.test.ts +110 -0
- package/tests/history-drain-order.test.ts +98 -0
- package/tests/history-expanded-globals.test.ts +62 -0
- package/tests/history-gc.test.ts +832 -0
- package/tests/history-header-layout.test.ts +265 -0
- package/tests/history-hide-prompts.test.ts +275 -0
- package/tests/history-lazy-windowing.test.ts +508 -0
- package/tests/history-legacy-migrate-v2.test.ts +297 -0
- package/tests/history-load-shared-history.test.ts +53 -0
- package/tests/history-max-results-cap.test.ts +76 -0
- package/tests/history-multi-reader.test.ts +203 -0
- package/tests/history-off-path.test.ts +170 -0
- package/tests/history-openflow-integration.test.ts +173 -0
- package/tests/history-overlay-margin.test.ts +326 -0
- package/tests/history-preview-layout.test.ts +93 -0
- package/tests/history-registry.test.ts +143 -0
- package/tests/history-scope-delete.test.ts +411 -0
- package/tests/history-search-caret-keys.test.ts +142 -0
- package/tests/history-seed-bootstrap.test.ts +170 -0
- package/tests/history-seed-regen.test.ts +129 -0
- package/tests/history-selector-windowing.test.ts +94 -0
- package/tests/history-session-scan-directory.test.ts +87 -0
- package/tests/history-session-scan-extract.test.ts +583 -0
- package/tests/history-session-writer.test.ts +351 -0
- package/tests/history-store-paths.test.ts +79 -0
- package/tests/history-tombstone-exact.test.ts +139 -0
- package/tests/history-wheel-mouse.test.ts +242 -0
- package/tests/inprocess-reviewer.test.ts +49 -19
- package/tests/install-gentle-ai-failure.test.ts +69 -0
- package/tests/install-tui-mode-setting.test.ts +15 -0
- package/tests/install-wizard.test.ts +1223 -0
- package/tests/installer-bundles.test.ts +122 -0
- package/tests/installer-posix-bootstrap.test.ts +1201 -0
- package/tests/installer-preflight.test.ts +750 -0
- package/tests/installer-probes.test.ts +1106 -0
- package/tests/installer-runner.test.ts +2158 -0
- package/tests/installer-server.test.ts +1567 -0
- package/tests/installer-windows-bootstrap.test.ts +2410 -0
- package/tests/issue-creation-skill.test.ts +61 -0
- package/tests/jobs-view.test.ts +160 -0
- package/tests/main-channel-upgrade.test.ts +389 -0
- package/tests/main-channel.test.ts +344 -0
- package/tests/maintainer/provider-relay.maintest.ts +13 -12
- package/tests/model-price-ratio.test.ts +116 -0
- package/tests/model-profiles-preservation.test.ts +153 -0
- package/tests/model-routing-authority.test.ts +16 -0
- package/tests/model-routing-package.test.ts +10 -0
- package/tests/nan-provider-runtime.test.ts +156 -0
- package/tests/nan-provider.test.ts +481 -0
- package/tests/native-binary-gate.test.ts +2 -0
- package/tests/native-review-capability-contract.test.ts +13 -1
- package/tests/native-review-cli.test.ts +239 -118
- package/tests/native-review-consent.test.ts +2 -2
- package/tests/native-review-parity-runtime.test.ts +101 -4
- package/tests/native-review-parity.test.ts +12 -48
- package/tests/native-risk-evidence-parity.test.ts +21 -0
- package/tests/no-self-pause-contract.test.ts +87 -0
- package/tests/notification-audio-native.test.ts +281 -0
- package/tests/notification-audio-windows.test.ts +581 -0
- package/tests/notification-audio.test.ts +548 -0
- package/tests/notification-customize.test.ts +655 -0
- package/tests/notification-events.test.ts +115 -0
- package/tests/notification-policy.test.ts +201 -0
- package/tests/notification-pulse-client.test.ts +356 -0
- package/tests/notification-pulse-protocol.test.ts +213 -0
- package/tests/notification-pulse-stream.test.ts +320 -0
- package/tests/notification-scheduler.test.ts +258 -0
- package/tests/notification-ui.test.ts +157 -0
- package/tests/npm-pack-result.test.ts +41 -0
- package/tests/odd-integration.test.ts +76 -0
- package/tests/odd-phase-inference.test.ts +213 -0
- package/tests/odd-phase-loader.test.ts +253 -0
- package/tests/odd-phase.test.ts +307 -0
- package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
- package/tests/odd-routing-contract.test.ts +158 -50
- package/tests/orchestrator-budget.test.ts +43 -45
- package/tests/orchestrator-catalog.test.ts +146 -0
- package/tests/orchestrator-consultation-sdk.test.ts +614 -0
- package/tests/orchestrator-consultation.test.ts +111 -0
- package/tests/orchestrator-discovery.test.ts +241 -0
- package/tests/orchestrator-helper-consent.test.ts +171 -0
- package/tests/orchestrator-helper.test.ts +297 -0
- package/tests/orchestrator-message-card.test.ts +190 -0
- package/tests/orchestrator-presence.test.ts +23 -0
- package/tests/orchestrator-rdd-ownership.test.ts +8 -7
- package/tests/orchestrator-scope.test.ts +107 -0
- package/tests/orchestrator-state.test.ts +114 -0
- package/tests/orchestrator-status-publication-contract.test.ts +34 -0
- package/tests/orchestrator-tool-card.test.ts +398 -0
- package/tests/orchestrator-work-search.test.ts +318 -0
- package/tests/orchestrator-work.test.ts +147 -0
- package/tests/overlay-repaint.test.ts +74 -0
- package/tests/package-manifest.test.ts +344 -123
- package/tests/packed-runner-owned-path.test.ts +80 -0
- package/tests/parallel-reason-contract.test.ts +21 -0
- package/tests/parallel-writer-admission.test.ts +210 -0
- package/tests/permission-mode-policy.test.ts +194 -0
- package/tests/persona-single-channel.test.ts +10 -18
- package/tests/pi-pretty.test.ts +56 -0
- package/tests/provider-defect-handoff.test.ts +7 -14
- package/tests/quiet-bash-runtime.test.ts +76 -0
- package/tests/quiet-tool-rendering.test.ts +569 -187
- package/tests/rdd-aware-verification-contract.test.ts +103 -40
- package/tests/rdd-status-line.test.ts +9 -4
- package/tests/render-cache-performance.test.ts +48 -0
- package/tests/resume-hint-extension.test.ts +122 -0
- package/tests/review-agent-end-preflight.test.ts +237 -21
- package/tests/review-candidate-owner-retry.test.ts +22 -1
- package/tests/review-candidate-view.test.ts +399 -12
- package/tests/review-contract-prompt.test.ts +138 -44
- package/tests/review-controller-lock-status.test.ts +0 -1
- package/tests/review-controller-native-recovery.test.ts +327 -17
- package/tests/review-controller-native-routing.test.ts +866 -6
- package/tests/review-controller-workspace-root.test.ts +206 -6
- package/tests/review-controller.test.ts +2 -2
- package/tests/review-correction-lifecycle.test.ts +2 -1
- package/tests/review-gate.test.ts +7 -4
- package/tests/review-graph-schema.test.ts +2 -2
- package/tests/review-host-relay-routing.test.ts +342 -4
- package/tests/review-host-relay.test.ts +49 -2
- package/tests/review-integration-v2-forward.test.ts +221 -1
- package/tests/review-json-arguments.test.ts +167 -0
- package/tests/review-last-event-closure.test.ts +157 -8
- package/tests/review-ledger-contract.test.ts +14 -37
- package/tests/review-policy-judgment-day.test.ts +1 -1
- package/tests/review-policy-ordinary.test.ts +2 -2
- package/tests/review-relay-transport-agent.test.ts +66 -1
- package/tests/review-reminder-receipt.test.ts +47 -1
- package/tests/review-risk-assessment.test.ts +594 -26
- package/tests/review-session-standing-permission-controller.test.ts +16 -17
- package/tests/review-session-standing-permission-ipc.test.ts +6 -5
- package/tests/review-sidebar-state.test.ts +402 -0
- package/tests/review-test-fixtures.ts +7 -1
- package/tests/review-transaction.test.ts +18 -1
- package/tests/run-test-suite.test.ts +202 -0
- package/tests/runtime-harness.mjs +186 -800
- package/tests/runtime-metrics-children.test.ts +16 -23
- package/tests/runtime-metrics-extension.test.ts +1 -1
- package/tests/runtime-metrics-native.test.ts +31 -1
- package/tests/selection-engine.test.ts +451 -0
- package/tests/session-change-capture.test.ts +10 -10
- package/tests/session-changes-shell.test.ts +242 -48
- package/tests/session-changes.test.ts +17 -24
- package/tests/session-messaging-grants.test.ts +255 -0
- package/tests/session-profile-binding.test.ts +163 -0
- package/tests/session-profile-freeze.test.ts +381 -0
- package/tests/session-subject-routing-contract.test.ts +34 -0
- package/tests/session-worktree-registry.test.ts +134 -2
- package/tests/shell-bar.test.ts +422 -1
- package/tests/shell-card.test.ts +353 -1
- package/tests/shell-changes-view.test.ts +135 -32
- package/tests/shell-changes.test.ts +61 -62
- package/tests/shell-prompt.test.ts +94 -2
- package/tests/shell-render-cache-runtime.test.ts +69 -0
- package/tests/shell-render-cache.test.ts +129 -0
- package/tests/shell-sidebar-layout.test.ts +351 -22
- package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
- package/tests/shell-todo.test.ts +207 -3
- package/tests/shell-usage-view.test.ts +28 -1
- package/tests/shell-usage.test.ts +108 -0
- package/tests/short-check-routing-contract.test.ts +49 -0
- package/tests/skill-registry-watcher.test.ts +119 -0
- package/tests/skill-registry.test.ts +357 -2
- package/tests/startup-banner.test.ts +246 -4
- package/tests/startup-home-repo.test.ts +170 -0
- package/tests/stats-collector.test.ts +195 -0
- package/tests/stats-view.test.ts +202 -0
- package/tests/status-timing-diagnostics.test.ts +283 -0
- package/tests/support/orchestrator-modules.ts +19 -0
- package/tests/task-size-routing-contract.test.ts +241 -0
- package/tests/telemetry-trigger.test.ts +107 -20
- package/tests/test-strength-contract.test.ts +124 -0
- package/tests/theme-customization.test.ts +72 -0
- package/tests/verify-agent-contract.test.ts +90 -0
- package/tests/verify-first-launch-contract.test.ts +139 -0
- package/tests/verify-package-files.test.ts +56 -0
- package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
- package/tests/vim-editor-adapter.test.ts +804 -0
- package/tests/vim-normal-engine.test.ts +101 -0
- package/tests/vim-operator-engine.test.ts +215 -0
- package/tests/vim-policy.test.ts +19 -0
- package/tests/vim-visual-engine.test.ts +52 -0
- package/tests/visual-customization-policy.test.ts +139 -0
- package/tests/visual-customize-view.test.ts +696 -0
- package/tests/visual-profiles.test.ts +87 -0
- package/tests/writer-edit-surface-scope.test.ts +59 -2
- package/tests/writer-surfaces.test.ts +199 -0
- package/tests/yolo-customize.test.ts +271 -0
- package/tests/yolo-mode-runtime.test.ts +188 -0
- package/tests/yolo-mode.test.ts +289 -0
- package/tests/yolo-session-policy.test.ts +59 -0
- package/themes/Gentle.json +2 -1
- package/themes/Gentleman-Cute.json +2 -1
- package/themes/Gentleman-Sexy.json +2 -1
- package/assets/agents/sdd-apply.md +0 -159
- package/assets/agents/sdd-archive.md +0 -228
- package/assets/agents/sdd-design.md +0 -49
- package/assets/agents/sdd-explore.md +0 -48
- package/assets/agents/sdd-init.md +0 -56
- package/assets/agents/sdd-onboard.md +0 -52
- package/assets/agents/sdd-proposal.md +0 -64
- package/assets/agents/sdd-remediate.md +0 -37
- package/assets/agents/sdd-research.md +0 -49
- package/assets/agents/sdd-spec.md +0 -192
- package/assets/agents/sdd-status.md +0 -54
- package/assets/agents/sdd-tasks.md +0 -108
- package/assets/agents/sdd-verify.md +0 -124
- package/assets/chains/sdd-full.chain.md +0 -83
- package/assets/chains/sdd-plan.chain.md +0 -56
- package/assets/chains/sdd-verify.chain.md +0 -43
- package/assets/sdd-orchestrator-workflow.md +0 -319
- package/assets/support/sdd-status-contract.md +0 -77
- package/docs/assets/diagrams/sdd-cycle.svg +0 -14
- package/extensions/sdd-init.ts +0 -816
- package/lib/openspec-deltas.ts +0 -156
- package/lib/sdd-preflight.ts +0 -1066
- package/lib/sdd-research-capabilities.ts +0 -94
- package/lib/sdd-status.ts +0 -26
- package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
- package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
- package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
- package/tests/openspec-deltas.test.ts +0 -209
- package/tests/sdd-agent-tools.test.ts +0 -156
- package/tests/sdd-archive-replay.test.ts +0 -82
- package/tests/sdd-classical-continuation.test.ts +0 -74
- package/tests/sdd-execution-routing-contract.test.ts +0 -44
- package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
- package/tests/sdd-native-managed-uptake.test.ts +0 -243
- package/tests/sdd-no-attempts-contract.test.ts +0 -15
- package/tests/sdd-odd-integration.test.ts +0 -33
- package/tests/sdd-optional-research.test.ts +0 -124
- package/tests/sdd-planning-routing-contract.test.ts +0 -45
- package/tests/sdd-preflight-rpc-input.test.ts +0 -125
- package/tests/sdd-preflight.test.ts +0 -541
- package/tests/sdd-research-capabilities.test.ts +0 -114
- package/tests/sdd-research-live.test.ts +0 -241
- package/tests/sdd-selection-transport.test.ts +0 -653
- package/tests/sdd-status.test.ts +0 -9
- package/tests/sdd-task-truth.test.ts +0 -43
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
- /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
package/lib/jobs-view.ts
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Background jobs overlay (`/gentle:jobs`): this session's jobs beside the
|
|
2
|
+
// selected job's command, status, output file, and output tail. It shares the
|
|
3
|
+
// Agents overlay geometry and roles so both read as one family, but stays a
|
|
4
|
+
// small standalone view: jobs have no peers, groups, or threads.
|
|
5
|
+
|
|
6
|
+
import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
7
|
+
import type { JobRecord } from "./background-jobs.ts";
|
|
8
|
+
import { formatJobDuration, JOB_GLYPH, jobOutcome } from "./background-jobs-tools.ts";
|
|
9
|
+
import { measureAgentsViewLayout } from "./agents-view-layout.ts";
|
|
10
|
+
import { sanitizeTerminalText } from "./terminal-theme.ts";
|
|
11
|
+
|
|
12
|
+
export interface JobsViewDeps {
|
|
13
|
+
theme: { fg(role: string, text: string): string };
|
|
14
|
+
rows: () => number;
|
|
15
|
+
/** This session's jobs; the view sorts a copy by activity and recency. */
|
|
16
|
+
jobs: () => JobRecord[];
|
|
17
|
+
now: () => number;
|
|
18
|
+
onStop(job: JobRecord): void;
|
|
19
|
+
onClose(): void;
|
|
20
|
+
requestRender(): void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ROLE = { FRAME: "border", TITLE: "customMessageLabel", SELECTED: "accent", NAME: "text", META: "dim", KEY: "accent", KEY_TEXT: "dim", EMPTY: "dim" } as const;
|
|
24
|
+
const REFRESH_MS = 1000;
|
|
25
|
+
|
|
26
|
+
function glyph(job: JobRecord): [string, string] {
|
|
27
|
+
if (job.status === "running") return [job.kind === "monitor" ? "◉" : "◐", "accent"];
|
|
28
|
+
if (job.status === "stopped") return ["–", "dim"];
|
|
29
|
+
if (job.status === "exited" && job.exitCode === 0) return ["✓", "success"];
|
|
30
|
+
return ["✗", "error"];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const rule = (length: number) => "─".repeat(Math.max(0, length));
|
|
34
|
+
const fit = (text: string, width: number) => {
|
|
35
|
+
const clipped = truncateToWidth(text, Math.max(0, width), "…");
|
|
36
|
+
return clipped + " ".repeat(Math.max(0, width - visibleWidth(clipped)));
|
|
37
|
+
};
|
|
38
|
+
const clean = (text: string) => sanitizeTerminalText(text).replace(/\s+/g, " ");
|
|
39
|
+
|
|
40
|
+
export class JobsView {
|
|
41
|
+
private readonly deps: JobsViewDeps;
|
|
42
|
+
private selectedId: string | undefined;
|
|
43
|
+
private details = false;
|
|
44
|
+
private listScroll = 0;
|
|
45
|
+
private closed = false;
|
|
46
|
+
private readonly timer: ReturnType<typeof setInterval>;
|
|
47
|
+
|
|
48
|
+
constructor(deps: JobsViewDeps) {
|
|
49
|
+
this.deps = deps;
|
|
50
|
+
// Elapsed time and the output tail move while a job runs.
|
|
51
|
+
this.timer = setInterval(() => {
|
|
52
|
+
if (this.deps.jobs().some((job) => job.status === "running")) this.deps.requestRender();
|
|
53
|
+
}, REFRESH_MS);
|
|
54
|
+
this.timer.unref?.();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
dispose(): void {
|
|
58
|
+
this.closed = true;
|
|
59
|
+
clearInterval(this.timer);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
invalidate(): void {}
|
|
63
|
+
|
|
64
|
+
handleInput(data: string): void {
|
|
65
|
+
if (this.closed) return;
|
|
66
|
+
const jobs = this.orderedJobs();
|
|
67
|
+
const index = Math.max(0, jobs.findIndex((job) => job.id === this.selected(jobs)?.id));
|
|
68
|
+
if (data === "q") this.close();
|
|
69
|
+
else if (matchesKey(data, Key.escape)) {
|
|
70
|
+
if (this.details) this.details = false;
|
|
71
|
+
else this.close();
|
|
72
|
+
} else if (data === "\t" || matchesKey(data, Key.enter)) this.details = !this.details;
|
|
73
|
+
else if (data === "j" || matchesKey(data, Key.down)) this.selectedId = jobs[Math.min(jobs.length - 1, index + 1)]?.id;
|
|
74
|
+
else if (data === "k" || matchesKey(data, Key.up)) this.selectedId = jobs[Math.max(0, index - 1)]?.id;
|
|
75
|
+
else if (data === "s") {
|
|
76
|
+
const job = this.selected(jobs);
|
|
77
|
+
if (job?.status === "running") this.deps.onStop(job);
|
|
78
|
+
} else return;
|
|
79
|
+
this.deps.requestRender();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
render(width: number): string[] {
|
|
83
|
+
const layout = measureAgentsViewLayout(width, this.deps.rows(), this.details);
|
|
84
|
+
if (layout.width === 0 || layout.height === 0) return [];
|
|
85
|
+
if (layout.mode === "fallback") return [fit("× Close Jobs", layout.width)];
|
|
86
|
+
const { theme } = this.deps;
|
|
87
|
+
const jobs = this.orderedJobs();
|
|
88
|
+
const selected = this.selected(jobs);
|
|
89
|
+
const inner = layout.width - 2;
|
|
90
|
+
const running = jobs.filter((job) => job.status === "running").length;
|
|
91
|
+
const title = truncateToWidth(`${JOB_GLYPH} Jobs · ${running} running · ${jobs.length - running} finished`, Math.max(0, inner - 4), "…");
|
|
92
|
+
const top = theme.fg(ROLE.FRAME, "╭─ ") + theme.fg(ROLE.TITLE, title) + theme.fg(ROLE.FRAME, ` ${rule(inner - visibleWidth(title) - 3)}╮`);
|
|
93
|
+
const list = this.listLines(jobs, selected, layout.listWidth, layout.bodyRows);
|
|
94
|
+
const detail = selected ? this.detailLines(selected, layout.bodyRows) : [theme.fg(ROLE.EMPTY, "No background jobs in this session."), theme.fg(ROLE.EMPTY, "The agent starts them with bash_background or monitor.")];
|
|
95
|
+
const bar = theme.fg(ROLE.FRAME, "│");
|
|
96
|
+
const body: string[] = [];
|
|
97
|
+
for (let row = 0; row < layout.bodyRows; row += 1) {
|
|
98
|
+
if (layout.mode === "panes") body.push(`${bar} ${fit(list[row] ?? "", layout.listWidth)} ${bar} ${fit(detail[row] ?? "", layout.threadWidth)}${bar}`);
|
|
99
|
+
else body.push(`${bar} ${fit((this.details ? detail : list)[row] ?? "", layout.listWidth)} ${bar}`);
|
|
100
|
+
}
|
|
101
|
+
const keys = this.keys(selected, layout.mode === "narrow").map(([key, label]) => `${theme.fg(ROLE.KEY, key)} ${theme.fg(ROLE.KEY_TEXT, label)}`).join(" ");
|
|
102
|
+
return [top, ...body, `${bar} ${fit(keys, inner - 2)} ${bar}`, theme.fg(ROLE.FRAME, `╰${rule(inner)}╯`)];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private orderedJobs(): JobRecord[] {
|
|
106
|
+
return [...this.deps.jobs()].sort((a, b) =>
|
|
107
|
+
Number(b.status === "running") - Number(a.status === "running") || b.startedAt - a.startedAt);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private close(): void {
|
|
111
|
+
this.deps.onClose();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private selected(jobs: JobRecord[]): JobRecord | undefined {
|
|
115
|
+
const selected = jobs.find((job) => job.id === this.selectedId) ?? jobs[0];
|
|
116
|
+
this.selectedId = selected?.id;
|
|
117
|
+
return selected;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private elapsed(job: JobRecord): string {
|
|
121
|
+
return formatJobDuration((job.endedAt ?? this.deps.now()) - job.startedAt);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
private listLines(jobs: JobRecord[], selected: JobRecord | undefined, width: number, rows: number): string[] {
|
|
125
|
+
const { theme } = this.deps;
|
|
126
|
+
if (jobs.length === 0) return [theme.fg(ROLE.EMPTY, "No jobs")];
|
|
127
|
+
const index = jobs.findIndex((job) => job.id === selected?.id);
|
|
128
|
+
if (index < this.listScroll) this.listScroll = index;
|
|
129
|
+
else if (index >= this.listScroll + rows) this.listScroll = index - rows + 1;
|
|
130
|
+
this.listScroll = Math.max(0, Math.min(this.listScroll, Math.max(0, jobs.length - rows)));
|
|
131
|
+
return jobs.slice(this.listScroll, this.listScroll + rows).map((job) => {
|
|
132
|
+
const [mark, role] = glyph(job);
|
|
133
|
+
const elapsed = this.elapsed(job);
|
|
134
|
+
const isSelected = job.id === selected?.id;
|
|
135
|
+
const name = fit(`${isSelected ? "›" : " "} ${mark} ${clean(job.label)}`, Math.max(0, width - visibleWidth(elapsed) - 1));
|
|
136
|
+
return `${theme.fg(isSelected ? ROLE.SELECTED : role === "accent" ? ROLE.NAME : role, name)} ${theme.fg(ROLE.META, elapsed)}`;
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private detailLines(job: JobRecord, rows: number): string[] {
|
|
141
|
+
const { theme } = this.deps;
|
|
142
|
+
const head = [
|
|
143
|
+
theme.fg(ROLE.TITLE, `${clean(job.label)} · ${job.id}`),
|
|
144
|
+
`${theme.fg(ROLE.META, "Command")} ${clean(job.command)}`,
|
|
145
|
+
`${theme.fg(ROLE.META, "Status ")} ${clean(jobOutcome(job))} · ${this.elapsed(job)}${job.kind === "monitor" ? ` · ${job.events ?? 0} event${job.events === 1 ? "" : "s"}` : ""}`,
|
|
146
|
+
`${theme.fg(ROLE.META, "Output ")} ${clean(job.outputPath)}`,
|
|
147
|
+
"",
|
|
148
|
+
];
|
|
149
|
+
const room = Math.max(0, rows - head.length - 1);
|
|
150
|
+
const tail = job.tail.slice(-room).map((line) => sanitizeTerminalText(line));
|
|
151
|
+
return [...head, theme.fg(ROLE.META, tail.length > 0 ? "Last output" : "No output yet."), ...tail];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private keys(selected: JobRecord | undefined, narrow: boolean): Array<[string, string]> {
|
|
155
|
+
const keys: Array<[string, string]> = [["↑↓", "move"]];
|
|
156
|
+
if (narrow && selected) keys.push(["tab", this.details ? "list" : "details"]);
|
|
157
|
+
if (selected?.status === "running") keys.push(["s", "Stop"]);
|
|
158
|
+
keys.push(["q", "close"]);
|
|
159
|
+
return keys;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// gentle-shell#1731: the orchestrator/worker price ratio is a runtime fact.
|
|
2
|
+
// The harness renders it as one line computed from catalog prices, so the
|
|
3
|
+
// model routes implementation by a number it never has to estimate.
|
|
4
|
+
|
|
5
|
+
export interface ModelIdentity {
|
|
6
|
+
provider: string | undefined;
|
|
7
|
+
id: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Catalog price in USD per 1M tokens, as Pi's model registry reports it. */
|
|
11
|
+
export interface ModelCost {
|
|
12
|
+
input: number;
|
|
13
|
+
output: number;
|
|
14
|
+
cacheRead: number;
|
|
15
|
+
cacheWrite: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type ModelPriceLookup = (model: ModelIdentity) => ModelCost | undefined;
|
|
19
|
+
|
|
20
|
+
export type ModelPriceRatio =
|
|
21
|
+
| { kind: "known"; orchestrator: string; worker: string; input: number; output: number; blended: number }
|
|
22
|
+
| { kind: "unknown"; reason: string };
|
|
23
|
+
|
|
24
|
+
function label(model: ModelIdentity): string {
|
|
25
|
+
return model.provider ? `${model.provider}/${model.id}` : model.id;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function validPrice(value: unknown): value is number {
|
|
29
|
+
return typeof value === "number" && Number.isFinite(value) && value > 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function computeModelPriceRatio(
|
|
33
|
+
orchestrator: ModelIdentity | undefined,
|
|
34
|
+
worker: ModelIdentity | undefined,
|
|
35
|
+
lookup: ModelPriceLookup,
|
|
36
|
+
): ModelPriceRatio {
|
|
37
|
+
if (!orchestrator) return { kind: "unknown", reason: "orchestrator model unknown" };
|
|
38
|
+
if (!worker) return { kind: "unknown", reason: "worker model unknown" };
|
|
39
|
+
const prices: ModelCost[] = [];
|
|
40
|
+
for (const model of [orchestrator, worker]) {
|
|
41
|
+
let cost: ModelCost | undefined;
|
|
42
|
+
try {
|
|
43
|
+
cost = lookup(model);
|
|
44
|
+
} catch {
|
|
45
|
+
return { kind: "unknown", reason: "price lookup failed" };
|
|
46
|
+
}
|
|
47
|
+
if (!cost) return { kind: "unknown", reason: `no catalog price for ${label(model)}` };
|
|
48
|
+
if (!validPrice(cost.input) || !validPrice(cost.output)) return { kind: "unknown", reason: `zero or invalid price for ${label(model)}` };
|
|
49
|
+
prices.push(cost);
|
|
50
|
+
}
|
|
51
|
+
const [orchestratorCost, workerCost] = prices;
|
|
52
|
+
const input = orchestratorCost.input / workerCost.input;
|
|
53
|
+
const output = orchestratorCost.output / workerCost.output;
|
|
54
|
+
// The blended ratio is the larger of the two. Catalogs usually keep input
|
|
55
|
+
// and output prices proportional, so both agree; when they diverge, the
|
|
56
|
+
// session's token mix is unknown up front, and the dimension where the
|
|
57
|
+
// orchestrator is relatively pricier is the one delegation saves on.
|
|
58
|
+
return { kind: "known", orchestrator: label(orchestrator), worker: label(worker), input, output, blended: Math.max(input, output) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Rounds half up at one decimal; toFixed alone misrounds values
|
|
62
|
+
// such as 2.95 because of their binary representation.
|
|
63
|
+
function times(value: number): string {
|
|
64
|
+
return `${(Math.round(value * 10) / 10).toFixed(1)}x`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function renderModelRoutingLine(ratio: ModelPriceRatio): string {
|
|
68
|
+
if (ratio.kind === "unknown") return `Model routing: price ratio unknown (${ratio.reason}).`;
|
|
69
|
+
return `Model routing: orchestrator ${ratio.orchestrator} costs ${times(ratio.blended)} the worker ${ratio.worker} (input ${times(ratio.input)}, output ${times(ratio.output)}).`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The harness fact line. Never throws: any failure renders the unknown line. */
|
|
73
|
+
export function modelRoutingLine(
|
|
74
|
+
orchestrator: ModelIdentity | undefined,
|
|
75
|
+
worker: ModelIdentity | undefined,
|
|
76
|
+
lookup: ModelPriceLookup,
|
|
77
|
+
): string {
|
|
78
|
+
try {
|
|
79
|
+
return renderModelRoutingLine(computeModelPriceRatio(orchestrator, worker, lookup));
|
|
80
|
+
} catch {
|
|
81
|
+
return renderModelRoutingLine({ kind: "unknown", reason: "price ratio failed" });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -63,7 +63,11 @@ export function normalizeRoutingEntry(value: unknown): AgentRoutingEntry | undef
|
|
|
63
63
|
}
|
|
64
64
|
if (!isRecord(value)) return undefined;
|
|
65
65
|
const model = normalizeModelId(value.model);
|
|
66
|
-
const thinking = isThinkingLevel(value.thinking)
|
|
66
|
+
const thinking = isThinkingLevel(value.thinking)
|
|
67
|
+
? value.thinking
|
|
68
|
+
: isThinkingLevel(value.effort)
|
|
69
|
+
? value.effort
|
|
70
|
+
: undefined;
|
|
67
71
|
if (!model && !thinking) {
|
|
68
72
|
return Object.keys(value).length === 0 ? {} : undefined;
|
|
69
73
|
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import * as piAi from "@earendil-works/pi-ai";
|
|
2
|
+
import type { Provider, ProviderStreams, RefreshModelsContext } from "@earendil-works/pi-ai";
|
|
3
|
+
import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
export const NAN_PROVIDER_ID = "nan";
|
|
6
|
+
export const NAN_PROVIDER_BASE_URL = "https://api.nan.builders/v1";
|
|
7
|
+
export const NAN_MODELS_TIMEOUT_MS = 3_000;
|
|
8
|
+
|
|
9
|
+
export interface NanProviderOptions {
|
|
10
|
+
fetchImpl?: typeof fetch;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type NanChatModelConfig = Extract<ProviderModelConfig, { type?: "chat" }>;
|
|
15
|
+
|
|
16
|
+
// Pi requires numeric rates; NaN access is quota-based, so zero avoids inventing per-token pricing.
|
|
17
|
+
const ZERO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
18
|
+
|
|
19
|
+
// Maintained chat subset from https://nan.builders/docs/models.
|
|
20
|
+
// Decimal bounds conservatively interpret the documented 1M/262K/131K labels.
|
|
21
|
+
// Pi models text/image inputs only; documented audio/video inputs are not advertised.
|
|
22
|
+
// Reasoning shares max_tokens with the answer: 65,536 leaves answer room after a
|
|
23
|
+
// 32,768 reasoning budget; DeepSeek uses NaN's 16,384 floor. Qwen 3.8 retains its
|
|
24
|
+
// published 131K output maximum. The remaining 32,768 caps are configured, not NaN-published limits.
|
|
25
|
+
// Pi requires explicit max mappings to offer that level; a null hides a level Pi would otherwise
|
|
26
|
+
// offer, and missing ordinary levels pass through. Each map exposes exactly the efforts NaN's
|
|
27
|
+
// reasoning contract declares and sends only values NaN accepts.
|
|
28
|
+
// GLM 5.3 / GLM 5.3 Flash: low · medium · high · max, fully controllable and without an off.
|
|
29
|
+
const GLM_THINKING_LEVEL_MAP: NanChatModelConfig["thinkingLevelMap"] = {
|
|
30
|
+
off: null, minimal: null, xhigh: null, max: "max",
|
|
31
|
+
};
|
|
32
|
+
// Qwen 3.6 and Gemma 4 honor none · minimal · low · medium · high · max as depth controls.
|
|
33
|
+
// DeepSeek V4 Flash, Qwen 3.8 Flash, and MiMo accept the same values while NaN manages their
|
|
34
|
+
// depth itself; none disables reasoning only where NaN honors it.
|
|
35
|
+
const EXTENDED_THINKING_LEVEL_MAP: NanChatModelConfig["thinkingLevelMap"] = {
|
|
36
|
+
off: "none", xhigh: null, max: "max",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* NaN accepts `reasoning_effort` for these models but manages their depth itself, so the selected
|
|
41
|
+
* level never changes it. Qwen 3.6 and Gemma 4 share the extended map yet honor the level as a
|
|
42
|
+
* reasoning budget, so the distinction lives here and the shell labels only these three automatic.
|
|
43
|
+
*/
|
|
44
|
+
export const NAN_ADAPTIVE_REASONING_MODEL_IDS: ReadonlySet<string> = new Set([
|
|
45
|
+
"deepseek-v4-flash", "qwen3.8-flash", "mimo-v2.6-flash",
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
export function isNanAdaptiveReasoningModel(model: { provider: string; id: string } | undefined): boolean {
|
|
49
|
+
return model !== undefined && model.provider === NAN_PROVIDER_ID && NAN_ADAPTIVE_REASONING_MODEL_IDS.has(model.id);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const CHAT_MODELS: NanChatModelConfig[] = ([
|
|
53
|
+
{
|
|
54
|
+
id: "glm5.3", name: "GLM 5.3", input: ["text", "image"], contextWindow: 1_000_000,
|
|
55
|
+
thinkingLevelMap: GLM_THINKING_LEVEL_MAP,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "deepseek-v4-flash", name: "DeepSeek V4 Flash", input: ["text", "image"], contextWindow: 1_000_000,
|
|
59
|
+
thinkingLevelMap: EXTENDED_THINKING_LEVEL_MAP, maxTokens: 16_384,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: "glm5.3-flash", name: "GLM 5.3 Flash", input: ["text", "image"], contextWindow: 1_000_000,
|
|
63
|
+
thinkingLevelMap: GLM_THINKING_LEVEL_MAP,
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "qwen3.8-flash", name: "Qwen 3.8 Flash", input: ["text", "image"], contextWindow: 1_048_576,
|
|
67
|
+
thinkingLevelMap: EXTENDED_THINKING_LEVEL_MAP, maxTokens: 131_000,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: "mimo-v2.6-flash", name: "MiMo V2.6 Flash", input: ["text", "image"], contextWindow: 1_000_000,
|
|
71
|
+
thinkingLevelMap: EXTENDED_THINKING_LEVEL_MAP,
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: "gemma4", name: "Gemma 4", input: ["text", "image"], contextWindow: 262_000,
|
|
75
|
+
thinkingLevelMap: EXTENDED_THINKING_LEVEL_MAP, maxTokens: 65_536,
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "qwen3.6", name: "Qwen 3.6", input: ["text", "image"], contextWindow: 262_000,
|
|
79
|
+
thinkingLevelMap: EXTENDED_THINKING_LEVEL_MAP, maxTokens: 65_536,
|
|
80
|
+
},
|
|
81
|
+
] satisfies Partial<NanChatModelConfig>[]).map((model) => ({
|
|
82
|
+
...model,
|
|
83
|
+
api: "openai-completions",
|
|
84
|
+
reasoning: true,
|
|
85
|
+
cost: ZERO_COST,
|
|
86
|
+
maxTokens: model.maxTokens ?? 32_768,
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
// The cold/offline baseline declares documented chat support, not key entitlement.
|
|
90
|
+
// A successful live catalog remains authoritative for the credential that fetched it.
|
|
91
|
+
const OFFLINE_MODELS = CHAT_MODELS;
|
|
92
|
+
|
|
93
|
+
function cloneModel(model: NanChatModelConfig): NanChatModelConfig {
|
|
94
|
+
return {
|
|
95
|
+
...model,
|
|
96
|
+
input: [...model.input],
|
|
97
|
+
cost: { ...model.cost },
|
|
98
|
+
thinkingLevelMap: model.thinkingLevelMap ? { ...model.thinkingLevelMap } : undefined,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function knownChatModels(ids: readonly string[]): NanChatModelConfig[] {
|
|
103
|
+
return ids.flatMap((id) => {
|
|
104
|
+
const known = CHAT_MODELS.find((model) => model.id === id);
|
|
105
|
+
return known ? [cloneModel(known)] : [];
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
110
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Returns undefined on an unusable response; an empty data array is authoritative. */
|
|
114
|
+
async function fetchLiveModelIds(options: {
|
|
115
|
+
apiKey?: string;
|
|
116
|
+
signal: AbortSignal;
|
|
117
|
+
fetchImpl: typeof fetch;
|
|
118
|
+
timeoutMs: number;
|
|
119
|
+
}): Promise<string[] | undefined> {
|
|
120
|
+
const { apiKey, signal, fetchImpl, timeoutMs } = options;
|
|
121
|
+
if (signal.aborted) return undefined;
|
|
122
|
+
|
|
123
|
+
const controller = new AbortController();
|
|
124
|
+
const abort = () => controller.abort(signal.reason);
|
|
125
|
+
if (signal.aborted) abort();
|
|
126
|
+
else signal.addEventListener("abort", abort, { once: true });
|
|
127
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const headers: Record<string, string> = { Accept: "application/json" };
|
|
131
|
+
if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
|
|
132
|
+
const response = await fetchImpl(`${NAN_PROVIDER_BASE_URL}/models`, {
|
|
133
|
+
method: "GET",
|
|
134
|
+
headers,
|
|
135
|
+
signal: controller.signal,
|
|
136
|
+
redirect: "error",
|
|
137
|
+
cache: "no-store",
|
|
138
|
+
});
|
|
139
|
+
if (!response.ok) return undefined;
|
|
140
|
+
|
|
141
|
+
const payload: unknown = await response.json();
|
|
142
|
+
if (!isRecord(payload) || !Array.isArray(payload.data)) return undefined;
|
|
143
|
+
if (payload.data.length === 0) return [];
|
|
144
|
+
|
|
145
|
+
const ids = new Set<string>();
|
|
146
|
+
for (const row of payload.data) {
|
|
147
|
+
if (!isRecord(row) || typeof row.id !== "string") continue;
|
|
148
|
+
const id = row.id.trim();
|
|
149
|
+
if (id) ids.add(id);
|
|
150
|
+
}
|
|
151
|
+
return ids.size > 0 ? [...ids] : undefined;
|
|
152
|
+
} catch {
|
|
153
|
+
// Discovery is best-effort. Never log request or response data: it may contain credentials.
|
|
154
|
+
return undefined;
|
|
155
|
+
} finally {
|
|
156
|
+
clearTimeout(timeout);
|
|
157
|
+
signal.removeEventListener("abort", abort);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function cloneCatalog(models: readonly NanChatModelConfig[]): NanChatModelConfig[] {
|
|
162
|
+
return models.map(cloneModel);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Native auth belongs to the provider; Pi persists only a successful login result. */
|
|
166
|
+
export function createNanProviderConfig(options: NanProviderOptions = {}): Provider<"openai-completions"> {
|
|
167
|
+
const config = createCatalogConfig(options);
|
|
168
|
+
// Pi's extension loader aliases the bare root to compat, which exposes this
|
|
169
|
+
// host-owned lazy API factory. Do not import an SDK implementation subpath.
|
|
170
|
+
const api = piAi.lazyApi(async () => {
|
|
171
|
+
const runtime = piAi as typeof piAi & { openAICompletionsApi?: () => ProviderStreams };
|
|
172
|
+
if (!runtime.openAICompletionsApi) throw new Error("NaN requires Pi's OpenAI completions API factory");
|
|
173
|
+
return runtime.openAICompletionsApi();
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
id: NAN_PROVIDER_ID,
|
|
177
|
+
name: "NaN",
|
|
178
|
+
baseUrl: NAN_PROVIDER_BASE_URL,
|
|
179
|
+
auth: { apiKey: {
|
|
180
|
+
name: "NaN API key",
|
|
181
|
+
async login(interaction) {
|
|
182
|
+
interaction.signal.throwIfAborted();
|
|
183
|
+
const entered = await interaction.prompt({
|
|
184
|
+
type: "secret", message: "Enter API key", signal: interaction.signal,
|
|
185
|
+
});
|
|
186
|
+
interaction.signal.throwIfAborted();
|
|
187
|
+
const key = entered.trim();
|
|
188
|
+
if (!key) throw new Error("NaN requires a non-empty API key");
|
|
189
|
+
return { type: "api_key", key };
|
|
190
|
+
},
|
|
191
|
+
async resolve({ ctx, credential, signal }) {
|
|
192
|
+
signal.throwIfAborted();
|
|
193
|
+
const stored = credential?.key?.trim();
|
|
194
|
+
const key = stored || (await ctx.env("NAN_API_KEY"))?.trim();
|
|
195
|
+
signal.throwIfAborted();
|
|
196
|
+
return key ? { auth: { apiKey: key }, source: stored ? "API key" : "NAN_API_KEY" } : undefined;
|
|
197
|
+
},
|
|
198
|
+
} },
|
|
199
|
+
getModels: () => config.getModels().map((model) => ({
|
|
200
|
+
...cloneModel(model), provider: NAN_PROVIDER_ID,
|
|
201
|
+
baseUrl: NAN_PROVIDER_BASE_URL, api: "openai-completions" as const,
|
|
202
|
+
})),
|
|
203
|
+
refreshModels: (context) => config.refreshModels(context),
|
|
204
|
+
stream: api.stream,
|
|
205
|
+
streamSimple: api.streamSimple,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function createCatalogConfig(options: NanProviderOptions = {}): {
|
|
210
|
+
getModels(): NanChatModelConfig[];
|
|
211
|
+
refreshModels(context: RefreshModelsContext): Promise<void>;
|
|
212
|
+
} {
|
|
213
|
+
let catalog = cloneCatalog(OFFLINE_MODELS);
|
|
214
|
+
let catalogKey: string | undefined;
|
|
215
|
+
let credentialRevision = 0;
|
|
216
|
+
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
// One source of truth lets Pi snapshot the new catalog inside publish.update.
|
|
220
|
+
getModels: () => cloneCatalog(catalog),
|
|
221
|
+
refreshModels: async (context: RefreshModelsContext) => {
|
|
222
|
+
const apiKey = context.credential?.type === "api_key" ? context.credential.key : undefined;
|
|
223
|
+
if (apiKey !== catalogKey) {
|
|
224
|
+
// A live catalog is authoritative only for the credential that discovered it.
|
|
225
|
+
catalogKey = apiKey;
|
|
226
|
+
credentialRevision++;
|
|
227
|
+
catalog = cloneCatalog(OFFLINE_MODELS);
|
|
228
|
+
}
|
|
229
|
+
const revision = credentialRevision;
|
|
230
|
+
if (!context.allowNetwork || context.signal.aborted || typeof fetchImpl !== "function") {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const ids = await fetchLiveModelIds({
|
|
235
|
+
apiKey,
|
|
236
|
+
signal: context.signal,
|
|
237
|
+
fetchImpl,
|
|
238
|
+
timeoutMs: options.timeoutMs ?? NAN_MODELS_TIMEOUT_MS,
|
|
239
|
+
});
|
|
240
|
+
if (revision !== credentialRevision || ids === undefined || context.signal.aborted) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
await context.publish({ update: () => {
|
|
245
|
+
if (revision === credentialRevision && !context.signal.aborted) catalog = knownChatModels(ids);
|
|
246
|
+
} });
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|