@vellumai/assistant 0.8.1 → 0.8.2
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/ARCHITECTURE.md +2 -7
- package/Dockerfile +75 -1
- package/bun.lock +11 -1
- package/docker-entrypoint.sh +5 -0
- package/docker-init-apt-root.sh +94 -0
- package/docker-kata-apt-env.sh +39 -0
- package/docs/plugins.md +88 -47
- package/docs/skills.md +9 -7
- package/examples/plugins/echo/README.md +27 -27
- package/examples/plugins/echo/package.json +3 -0
- package/examples/plugins/echo/register.ts +31 -31
- package/node_modules/@vellumai/slack-text/src/index.test.ts +114 -14
- package/node_modules/@vellumai/slack-text/src/index.ts +82 -18
- package/openapi.yaml +325 -3
- package/package.json +3 -1
- package/scripts/generate-openapi.ts +83 -10
- package/scripts/sync-llm-catalog.ts +2 -2
- package/scripts/sync-web-search-catalog.ts +47 -25
- package/src/__tests__/agent-image-optimize.test.ts +11 -3
- package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +131 -0
- package/src/__tests__/anthropic-provider.test.ts +45 -0
- package/src/__tests__/app-builder-tool-scripts.test.ts +9 -3
- package/src/__tests__/app-executors.test.ts +220 -4
- package/src/__tests__/auto-analysis-end-to-end.test.ts +35 -0
- package/src/__tests__/bundled-asset.test.ts +6 -6
- package/src/__tests__/channel-availability-routes.test.ts +206 -0
- package/src/__tests__/channel-delivery-store.test.ts +289 -1
- package/src/__tests__/circuit-breaker-pipeline.test.ts +0 -1
- package/src/__tests__/clawhub.test.ts +75 -16
- package/src/__tests__/compactor-tail-resolution.test.ts +41 -0
- package/src/__tests__/config-schema.test.ts +21 -0
- package/src/__tests__/config-set-route.test.ts +80 -0
- package/src/__tests__/config-sounds-sync.test.ts +97 -0
- package/src/__tests__/config-watcher-skill-reseed.test.ts +453 -0
- package/src/__tests__/context-search-conversations-source.test.ts +117 -2
- package/src/__tests__/context-search-memory-v2-source.test.ts +0 -1
- package/src/__tests__/context-search-workspace-source.test.ts +7 -0
- package/src/__tests__/context-token-estimator.test.ts +1 -0
- package/src/__tests__/conversation-abort-tool-results.test.ts +4 -1
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +92 -92
- package/src/__tests__/conversation-agent-loop.test.ts +2 -0
- package/src/__tests__/conversation-error.test.ts +42 -3
- package/src/__tests__/conversation-fork-crud.test.ts +82 -0
- package/src/__tests__/conversation-inference-profile-route.test.ts +40 -4
- package/src/__tests__/conversation-lifecycle.test.ts +173 -0
- package/src/__tests__/conversation-message-sync-tags.test.ts +97 -0
- package/src/__tests__/conversation-pairing.test.ts +54 -0
- package/src/__tests__/conversation-process-callsite.test.ts +4 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -1
- package/src/__tests__/conversation-queue.test.ts +4 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +76 -9
- package/src/__tests__/conversation-slash-queue.test.ts +59 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +4 -1
- package/src/__tests__/conversation-surfaces-table-action.test.ts +360 -0
- package/src/__tests__/conversation-sync-tags.test.ts +235 -0
- package/src/__tests__/conversation-workspace-injection.test.ts +5 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +3 -2
- package/src/__tests__/db-slack-external-content-normalization.test.ts +301 -0
- package/src/__tests__/delete-managed-skill-tool.test.ts +55 -13
- package/src/__tests__/disk-pressure-tools.test.ts +1 -0
- package/src/__tests__/dm-backfill.test.ts +121 -10
- package/src/__tests__/document-tool-security.test.ts +258 -0
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +0 -1
- package/src/__tests__/edit-propagation.test.ts +33 -0
- package/src/__tests__/empty-response-pipeline.test.ts +0 -4
- package/src/__tests__/external-plugin-loader.test.ts +60 -36
- package/src/__tests__/filing-service.test.ts +140 -0
- package/src/__tests__/get-skill-detail-audit.test.ts +0 -4
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +43 -62
- package/src/__tests__/helpers/tar-fixtures.ts +39 -0
- package/src/__tests__/helpers/wait-for.ts +21 -0
- package/src/__tests__/history-repair-pipeline.test.ts +0 -3
- package/src/__tests__/history-repair.test.ts +73 -0
- package/src/__tests__/host-app-control-proxy.test.ts +266 -10
- package/src/__tests__/image-credentials.test.ts +1 -1
- package/src/__tests__/inbound-slack-persistence.test.ts +2 -0
- package/src/__tests__/inference-no-mode-boot-e2e.test.ts +1 -1
- package/src/__tests__/inference-profile-reaper.test.ts +4 -2
- package/src/__tests__/inference-profile-session-handler.test.ts +18 -6
- package/src/__tests__/inference-profile-session-ipc.test.ts +17 -5
- package/src/__tests__/injector-chain.test.ts +10 -8
- package/src/__tests__/install-skill-routing.test.ts +155 -37
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +92 -3
- package/src/__tests__/list-messages-page-latest.test.ts +55 -0
- package/src/__tests__/llm-call-pipeline.test.ts +0 -3
- package/src/__tests__/llm-catalog-parity.test.ts +55 -13
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +34 -0
- package/src/__tests__/llm-request-log-source-factory.test.ts +29 -53
- package/src/__tests__/llm-usage-store.test.ts +114 -0
- package/src/__tests__/managed-profile-guard.test.ts +31 -29
- package/src/__tests__/managed-skill-lifecycle.test.ts +109 -18
- package/src/__tests__/managed-store.test.ts +84 -192
- package/src/__tests__/media-generate-image.test.ts +1 -1
- package/src/__tests__/memory-retrieval-pipeline.test.ts +0 -2
- package/src/__tests__/messages-after-tiebreaker.test.ts +122 -0
- package/src/__tests__/oauth-commands-routes.test.ts +168 -16
- package/src/__tests__/oauth-provider-profiles.test.ts +9 -0
- package/src/__tests__/openai-provider.test.ts +24 -0
- package/src/__tests__/openai-responses-cutover-guard.test.ts +17 -9
- package/src/__tests__/overflow-reduce-pipeline.test.ts +0 -2
- package/src/__tests__/persistence-pipeline.test.ts +0 -2
- package/src/__tests__/{managed-proxy-context.test.ts → platform-proxy-context.test.ts} +1 -1
- package/src/__tests__/platform.test.ts +2 -0
- package/src/__tests__/plugin-api-shim.test.ts +125 -0
- package/src/__tests__/plugin-bootstrap.test.ts +10 -36
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/plugin-registry.test.ts +0 -77
- package/src/__tests__/plugin-route-contribution.test.ts +0 -1
- package/src/__tests__/plugin-skill-contribution.test.ts +0 -2
- package/src/__tests__/plugin-tool-contribution.test.ts +16 -15
- package/src/__tests__/plugin-types.test.ts +3 -13
- package/src/__tests__/process-message-background-slack.test.ts +8 -1
- package/src/__tests__/process-message-display-content.test.ts +421 -0
- package/src/__tests__/provider-catalog-visibility.test.ts +142 -0
- package/src/__tests__/provider-error-scenarios.test.ts +111 -0
- package/src/__tests__/{provider-managed-proxy-integration.test.ts → provider-platform-proxy-integration.test.ts} +8 -8
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +65 -13
- package/src/__tests__/schedule-routes.test.ts +50 -3
- package/src/__tests__/schedule-store.test.ts +94 -0
- package/src/__tests__/scheduler-reuse-conversation.test.ts +54 -7
- package/src/__tests__/schema-transforms.test.ts +20 -0
- package/src/__tests__/search-skills-unified.test.ts +0 -5
- package/src/__tests__/server-history-render.test.ts +43 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +0 -12
- package/src/__tests__/skill-load-tool.test.ts +27 -89
- package/src/__tests__/skill-memory.test.ts +23 -3
- package/src/__tests__/skills-file-content-endpoint.test.ts +9 -38
- package/src/__tests__/skills-files-catalog-fallback.test.ts +0 -3
- package/src/__tests__/skills-install-extract.test.ts +49 -38
- package/src/__tests__/skills-install-staging.test.ts +159 -0
- package/src/__tests__/skills-uninstall.test.ts +9 -41
- package/src/__tests__/skills.test.ts +51 -58
- package/src/__tests__/slack-channel-config.test.ts +9 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
- package/src/__tests__/system-prompt.test.ts +737 -63
- package/src/__tests__/terminal-tools.test.ts +28 -1
- package/src/__tests__/thread-backfill.test.ts +557 -27
- package/src/__tests__/title-generate-pipeline.test.ts +0 -13
- package/src/__tests__/token-estimate-pipeline.test.ts +0 -3
- package/src/__tests__/tool-error-pipeline.test.ts +0 -3
- package/src/__tests__/tool-execute-pipeline.test.ts +0 -5
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -1
- package/src/__tests__/tool-executor.test.ts +16 -4
- package/src/__tests__/tool-result-truncate-pipeline.test.ts +0 -12
- package/src/__tests__/turn-events-store.test.ts +256 -0
- package/src/__tests__/twilio-routes.test.ts +4 -0
- package/src/__tests__/user-plugin-loader.test.ts +0 -7
- package/src/__tests__/voice-session-bridge.test.ts +198 -0
- package/src/__tests__/web-search-catalog-parity.test.ts +32 -10
- package/src/__tests__/workspace-migration-057-repair-stale-gemini-model-ids.test.ts +115 -3
- package/src/__tests__/workspace-migration-072-seed-reply-suggestion-callsite.test.ts +50 -0
- package/src/__tests__/workspace-migration-073-repair-recall-callsite-empty-profile.test.ts +153 -0
- package/src/__tests__/workspace-migration-085-memory-v2-bm25-b-reembed-disabled-v2-pages.test.ts +220 -0
- package/src/__tests__/workspace-migration-086-revert-stale-gemini-mis-rewrites.test.ts +269 -0
- package/src/__tests__/workspace-migration-remove-legacy-skills-index.test.ts +309 -0
- package/src/__tests__/workspace-migrations-runner.test.ts +111 -3
- package/src/acp/resolve-agent.ts +1 -1
- package/src/agent/image-optimize.ts +13 -5
- package/src/calls/voice-session-bridge.ts +61 -42
- package/src/channels/types.ts +108 -0
- package/src/cli/__tests__/unknown-command.test.ts +24 -0
- package/src/cli/commands/__tests__/changelog.test.ts +304 -319
- package/src/cli/commands/__tests__/schedules.test.ts +491 -0
- package/src/cli/commands/changelog.ts +106 -42
- package/src/cli/commands/conversations.ts +102 -17
- package/src/cli/commands/default-action.ts +10 -53
- package/src/cli/commands/notifications.ts +329 -317
- package/src/cli/commands/plugins.ts +185 -0
- package/src/cli/commands/schedules.ts +391 -0
- package/src/cli/commands/telemetry.ts +40 -0
- package/src/cli/lib/__tests__/cli-colors.test.ts +48 -0
- package/src/cli/lib/__tests__/confirm-prompt.test.ts +159 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +355 -0
- package/src/cli/lib/__tests__/list-installed-plugins.test.ts +154 -0
- package/src/cli/lib/__tests__/uninstall-plugin.test.ts +124 -0
- package/src/cli/lib/__tests__/unknown-command.test.ts +106 -0
- package/src/cli/lib/cli-colors.ts +12 -0
- package/src/cli/lib/confirm-prompt.ts +79 -0
- package/src/cli/lib/install-from-github.ts +304 -0
- package/src/cli/lib/list-installed-plugins.ts +137 -0
- package/src/cli/lib/uninstall-plugin.ts +82 -0
- package/src/cli/lib/unknown-command.ts +111 -0
- package/src/cli/program.ts +38 -2
- package/src/config/bundled-skills/app-builder/SKILL.md +23 -21
- package/src/config/bundled-skills/app-builder/TOOLS.json +7 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +15 -52
- package/src/config/bundled-skills/document/SKILL.md +23 -3
- package/src/config/bundled-skills/document/TOOLS.json +53 -0
- package/src/config/bundled-skills/document/tools/document-delete.ts +12 -0
- package/src/config/bundled-skills/document/tools/document-list.ts +12 -0
- package/src/config/bundled-skills/document/tools/document-read.ts +12 -0
- package/src/config/bundled-skills/skill-management/SKILL.md +2 -2
- package/src/config/bundled-skills/skill-management/TOOLS.json +7 -7
- package/src/config/bundled-tool-registry.ts +6 -0
- package/src/config/feature-flag-registry.json +41 -1
- package/src/config/loader.ts +64 -38
- package/src/config/schema.ts +7 -10
- package/src/config/schemas/__tests__/llm-request-logs.test.ts +36 -0
- package/src/config/schemas/channels.ts +8 -0
- package/src/config/schemas/compaction.ts +28 -0
- package/src/config/schemas/heartbeat.ts +9 -0
- package/src/config/schemas/llm-request-logs.ts +31 -7
- package/src/config/schemas/llm.ts +3 -0
- package/src/config/schemas/memory-retrieval.ts +18 -0
- package/src/config/schemas/tools.ts +14 -0
- package/src/config/skills.ts +3 -96
- package/src/context/compactor.ts +1047 -0
- package/src/context/token-estimator.ts +2 -2
- package/src/context/window-manager.ts +197 -1520
- package/src/credential-execution/managed-catalog.ts +37 -0
- package/src/credential-health/credential-health-service.ts +280 -19
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +34 -0
- package/src/daemon/__tests__/conversation-tool-setup-exclude.test.ts +138 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +74 -0
- package/src/daemon/approval-generators.ts +8 -6
- package/src/daemon/config-watcher.ts +94 -31
- package/src/daemon/conversation-agent-loop.ts +169 -9
- package/src/daemon/conversation-error.ts +171 -37
- package/src/daemon/conversation-lifecycle.ts +53 -40
- package/src/daemon/conversation-messaging.ts +25 -6
- package/src/daemon/conversation-process.ts +49 -12
- package/src/daemon/conversation-runtime-assembly.ts +16 -1
- package/src/daemon/conversation-slash.ts +12 -5
- package/src/daemon/conversation-store.ts +11 -4
- package/src/daemon/conversation-tool-setup.ts +39 -7
- package/src/daemon/conversation.ts +33 -1
- package/src/daemon/external-plugins-bootstrap.ts +217 -181
- package/src/daemon/first-greeting.ts +22 -2
- package/src/daemon/handlers/config-model.ts +6 -5
- package/src/daemon/handlers/config-slack-channel.ts +15 -3
- package/src/daemon/handlers/shared.ts +14 -5
- package/src/daemon/handlers/skills.ts +111 -108
- package/src/daemon/history-repair.ts +28 -1
- package/src/daemon/host-app-control-proxy.ts +98 -23
- package/src/daemon/lifecycle.ts +45 -35
- package/src/daemon/meet-host-supervisor.ts +5 -4
- package/src/daemon/memory-v2-startup.ts +49 -0
- package/src/daemon/message-protocol.ts +1 -0
- package/src/daemon/message-types/conversations.ts +25 -0
- package/src/daemon/message-types/messages.ts +61 -0
- package/src/daemon/message-types/subagents.ts +1 -0
- package/src/daemon/message-types/sync.ts +1 -0
- package/src/daemon/pkb-reminder-builder.test.ts +1 -1
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/daemon/plugin-source-watcher.ts +146 -0
- package/src/daemon/process-message.ts +21 -3
- package/src/daemon/server.ts +11 -2
- package/src/daemon/skill-memory-refresh.ts +29 -0
- package/src/documents/document-store.ts +221 -3
- package/src/embedded/plugin-api.ts +40 -0
- package/src/filing/filing-service.ts +39 -0
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +91 -6
- package/src/heartbeat/heartbeat-run-store.ts +2 -1
- package/src/heartbeat/heartbeat-service.ts +41 -0
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +22 -0
- package/src/home/post-connect-feed.ts +1 -0
- package/src/index.ts +18 -1
- package/src/live-voice/__tests__/live-voice-stt.test.ts +57 -0
- package/src/mcp/client.ts +20 -4
- package/src/media/image-credentials.ts +3 -3
- package/src/memory/__tests__/bookmark-crud.test.ts +33 -27
- package/src/memory/__tests__/conversation-queries.test.ts +263 -0
- package/src/memory/__tests__/jobs-worker-v2-graph-trigger-embed.test.ts +113 -0
- package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +119 -14
- package/src/memory/__tests__/message-content.test.ts +35 -0
- package/src/memory/bookmark-crud.ts +42 -10
- package/src/memory/context-search/sources/conversations.ts +62 -2
- package/src/memory/context-search/sources/workspace.ts +4 -0
- package/src/memory/conversation-crud.ts +63 -19
- package/src/memory/conversation-queries.ts +110 -10
- package/src/memory/db-init.ts +6 -0
- package/src/memory/delivery-crud.ts +152 -5
- package/src/memory/embedding-backend.ts +4 -4
- package/src/memory/external-conversation-store.ts +66 -5
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +66 -9
- package/src/memory/graph/conversation-graph-memory.ts +31 -15
- package/src/memory/graph/tools.ts +3 -3
- package/src/memory/indexer.ts +34 -29
- package/src/memory/jobs/__tests__/embed-concept-page.test.ts +73 -0
- package/src/memory/jobs/embed-concept-page.ts +20 -11
- package/src/memory/jobs-worker.ts +6 -1
- package/src/memory/llm-request-log-source-clickhouse.ts +17 -10
- package/src/memory/llm-request-log-source.ts +19 -52
- package/src/memory/llm-usage-store.ts +125 -5
- package/src/memory/memory-retrospective-startup-cleanup.ts +72 -5
- package/src/memory/message-content.ts +1 -1
- package/src/memory/migrations/109-external-conversation-bindings.ts +15 -4
- package/src/memory/migrations/229-delete-private-conversations.test.ts +38 -1
- package/src/memory/migrations/229-delete-private-conversations.ts +7 -0
- package/src/memory/migrations/247-external-conversation-binding-thread-id.ts +78 -0
- package/src/memory/migrations/248-create-onboarding-events.ts +21 -0
- package/src/memory/migrations/249-normalize-slack-external-content.ts +240 -0
- package/src/memory/migrations/index.ts +6 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/onboarding-events-store.ts +106 -0
- package/src/memory/schema/bookmarks.ts +0 -2
- package/src/memory/schema/calls.ts +1 -0
- package/src/memory/schema/inference.ts +1 -3
- package/src/memory/schema/infrastructure.ts +12 -0
- package/src/memory/turn-events-store.ts +127 -2
- package/src/memory/v2/__tests__/activation.test.ts +0 -8
- package/src/memory/v2/__tests__/injection.test.ts +98 -8
- package/src/memory/v2/__tests__/migration.test.ts +87 -0
- package/src/memory/v2/__tests__/page-index.test.ts +83 -0
- package/src/memory/v2/__tests__/prompts-router.test.ts +58 -6
- package/src/memory/v2/__tests__/qdrant.test.ts +66 -3
- package/src/memory/v2/__tests__/router.test.ts +15 -0
- package/src/memory/v2/__tests__/skill-store.test.ts +387 -8
- package/src/memory/v2/injection.ts +32 -6
- package/src/memory/v2/migration.ts +49 -19
- package/src/memory/v2/page-index.ts +35 -5
- package/src/memory/v2/prompts/router.ts +11 -8
- package/src/memory/v2/prompts/sweep.ts +2 -2
- package/src/memory/v2/qdrant.ts +135 -7
- package/src/memory/v2/router.ts +9 -8
- package/src/memory/v2/skill-store.ts +120 -35
- package/src/messaging/providers/slack/__tests__/adapter-token-routing.test.ts +45 -5
- package/src/messaging/providers/slack/__tests__/download.test.ts +231 -0
- package/src/messaging/providers/slack/adapter.ts +43 -5
- package/src/messaging/providers/slack/client.ts +27 -0
- package/src/messaging/providers/slack/deep-link.ts +65 -0
- package/src/messaging/providers/slack/download.ts +104 -0
- package/src/messaging/providers/slack/message-metadata.test.ts +32 -0
- package/src/messaging/providers/slack/message-metadata.ts +27 -0
- package/src/messaging/providers/slack/render-transcript.test.ts +134 -0
- package/src/messaging/providers/slack/render-transcript.ts +69 -5
- package/src/messaging/providers/slack/types.ts +20 -1
- package/src/notifications/conversation-pairing.ts +2 -1
- package/src/notifications/decision-engine.ts +2 -1
- package/src/notifications/emit-signal.ts +20 -1
- package/src/notifications/home-feed-side-effect.ts +54 -0
- package/src/notifications/signal.ts +3 -1
- package/src/oauth/connection-resolver.ts +8 -4
- package/src/oauth/platform-connection.ts +6 -2
- package/src/oauth/seed-providers.ts +10 -1
- package/src/permissions/checker.ts +2 -0
- package/src/permissions/ipc-risk-types.ts +1 -0
- package/src/permissions/question-prompter.test.ts +416 -0
- package/src/permissions/question-prompter.ts +294 -0
- package/src/platform/client.test.ts +1 -1
- package/src/platform/client.ts +1 -1
- package/src/plugin-api/constants.ts +26 -0
- package/src/plugin-api/index.ts +34 -1
- package/src/plugin-api/types.ts +104 -22
- package/src/plugins/defaults/circuit-breaker.ts +0 -5
- package/src/plugins/defaults/compaction.ts +0 -4
- package/src/plugins/defaults/empty-response.ts +0 -2
- package/src/plugins/defaults/history-repair.ts +0 -2
- package/src/plugins/defaults/injectors.ts +36 -3
- package/src/plugins/defaults/llm-call.ts +0 -2
- package/src/plugins/defaults/memory-retrieval.ts +0 -1
- package/src/plugins/defaults/overflow-reduce.ts +0 -1
- package/src/plugins/defaults/persistence.ts +0 -2
- package/src/plugins/defaults/title-generate.ts +0 -5
- package/src/plugins/defaults/token-estimate.ts +0 -2
- package/src/plugins/defaults/tool-error.ts +0 -7
- package/src/plugins/defaults/tool-execute.ts +0 -2
- package/src/plugins/defaults/tool-result-truncate.ts +0 -4
- package/src/plugins/ensure-plugin-api-shim.ts +96 -0
- package/src/plugins/external-api.ts +104 -0
- package/src/plugins/external-plugin-loader.ts +105 -32
- package/src/plugins/feature-gate.ts +22 -0
- package/src/plugins/pipeline.ts +37 -0
- package/src/plugins/registry.ts +48 -80
- package/src/plugins/types.ts +31 -26
- package/src/plugins/user-loader.ts +21 -2
- package/src/proactive-artifact/aux-message-injector.ts +11 -0
- package/src/proactive-artifact/job.test.ts +37 -5
- package/src/prompts/__tests__/system-prompt.test.ts +12 -0
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +99 -0
- package/src/prompts/normalize-onboarding.ts +27 -0
- package/src/prompts/sections.ts +302 -0
- package/src/prompts/system-prompt.ts +63 -166
- package/src/prompts/templates/BOOTSTRAP.md +17 -1
- package/src/prompts/templates/system-sections.ts +173 -0
- package/src/providers/__tests__/inference.test.ts +22 -7
- package/src/providers/anthropic/client.ts +28 -28
- package/src/providers/connection-resolution.ts +7 -0
- package/src/providers/inference/adapter-factory.ts +41 -4
- package/src/providers/inference/connections.ts +74 -29
- package/src/providers/inference/resolve-auth.ts +12 -4
- package/src/providers/model-catalog.ts +294 -12
- package/src/providers/openai/chat-completions-provider.ts +10 -2
- package/src/providers/openrouter/client.ts +7 -0
- package/src/providers/{managed-proxy → platform-proxy}/constants.ts +4 -1
- package/src/providers/{managed-proxy → platform-proxy}/context.ts +3 -3
- package/src/providers/provider-availability.ts +17 -2
- package/src/providers/provider-catalog-visibility.ts +36 -0
- package/src/providers/registry.ts +22 -14
- package/src/providers/retry.ts +47 -1
- package/src/runtime/__tests__/agent-wake.test.ts +152 -0
- package/src/runtime/agent-wake.ts +42 -14
- package/src/runtime/auth/route-policy.ts +8 -1
- package/src/runtime/btw-sidechain.ts +2 -0
- package/src/runtime/http-types.ts +19 -0
- package/src/runtime/migrations/origin-mode.ts +1 -1
- package/src/runtime/pending-interactions.ts +1 -0
- package/src/runtime/routes/__tests__/bookmark-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/conversation-management-routes.test.ts +5 -1
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +107 -20
- package/src/runtime/routes/__tests__/question-routes.test.ts +395 -0
- package/src/runtime/routes/__tests__/tts-routes.test.ts +64 -1
- package/src/runtime/routes/acp-routes-list.test.ts +143 -0
- package/src/runtime/routes/acp-routes.ts +5 -3
- package/src/runtime/routes/auth-routes.ts +1 -1
- package/src/runtime/routes/bookmark-routes.ts +5 -3
- package/src/runtime/routes/btw-routes.ts +5 -1
- package/src/runtime/routes/channel-availability-routes.ts +121 -0
- package/src/runtime/routes/conversation-cli-routes.ts +44 -3
- package/src/runtime/routes/conversation-list-routes.ts +3 -20
- package/src/runtime/routes/conversation-management-routes.ts +17 -42
- package/src/runtime/routes/conversation-query-routes.ts +40 -35
- package/src/runtime/routes/conversation-routes.ts +90 -11
- package/src/runtime/routes/documents-routes.ts +25 -86
- package/src/runtime/routes/group-routes.ts +5 -0
- package/src/runtime/routes/inbound-conversation.ts +28 -8
- package/src/runtime/routes/inbound-message-handler.ts +236 -41
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +111 -0
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +32 -1
- package/src/runtime/routes/inbound-stages/edit-intercept.ts +17 -4
- package/src/runtime/routes/index.ts +6 -0
- package/src/runtime/routes/inference-profile-session-handler.ts +17 -44
- package/src/runtime/routes/inference-profile-session-reaper.ts +7 -21
- package/src/runtime/routes/inference-provider-connection-routes.ts +65 -21
- package/src/runtime/routes/integrations/slack/share.ts +4 -52
- package/src/runtime/routes/integrations/slack/token.ts +43 -0
- package/src/runtime/routes/integrations/twilio.ts +6 -13
- package/src/runtime/routes/notification-routes.ts +1 -1
- package/src/runtime/routes/oauth-commands-routes.ts +105 -15
- package/src/runtime/routes/oauth-lifecycle-routes.ts +43 -0
- package/src/runtime/routes/question-routes.ts +259 -0
- package/src/runtime/routes/rename-conversation-routes.ts +2 -33
- package/src/runtime/routes/schedule-routes.ts +4 -7
- package/src/runtime/routes/subagents-routes.ts +57 -18
- package/src/runtime/routes/telemetry-routes.ts +27 -0
- package/src/runtime/routes/tts-routes.ts +27 -2
- package/src/runtime/routes/workspace-routes.test.ts +43 -0
- package/src/runtime/routes/workspace-routes.ts +28 -0
- package/src/runtime/services/conversation-serializer.ts +39 -7
- package/src/runtime/sync/resource-sync-events.ts +93 -1
- package/src/schedule/schedule-store.ts +27 -2
- package/src/schedule/scheduler.ts +9 -1
- package/src/security/__tests__/untrusted-content.test.ts +86 -0
- package/src/security/untrusted-content.ts +93 -8
- package/src/skills/catalog-files.ts +1 -1
- package/src/skills/catalog-install.ts +233 -116
- package/src/skills/clawhub.ts +70 -13
- package/src/skills/managed-store.ts +4 -119
- package/src/skills/skillssh-registry.ts +27 -48
- package/src/subagent/manager.ts +15 -7
- package/src/telemetry/types.ts +113 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +312 -5
- package/src/telemetry/usage-telemetry-reporter.ts +113 -7
- package/src/tools/apps/executors.ts +58 -7
- package/src/tools/ask-question/ask-question-tool.test.ts +509 -0
- package/src/tools/ask-question/ask-question-tool.ts +304 -0
- package/src/tools/browser/browser-execution.ts +15 -11
- package/src/tools/computer-use/definitions.ts +3 -3
- package/src/tools/credentials/vault.ts +1 -1
- package/src/tools/document/document-tool.ts +124 -1
- package/src/tools/filesystem/edit.ts +1 -1
- package/src/tools/filesystem/list.ts +1 -1
- package/src/tools/filesystem/read.ts +1 -1
- package/src/tools/filesystem/write.ts +5 -2
- package/src/tools/host-filesystem/transfer.ts +1 -1
- package/src/tools/host-terminal/host-shell.ts +1 -1
- package/src/tools/permission-checker.ts +1 -1
- package/src/tools/registry.ts +17 -7
- package/src/tools/schedule/create.ts +2 -2
- package/src/tools/schema-transforms.ts +7 -2
- package/src/tools/side-effects.ts +1 -0
- package/src/tools/skills/delete-managed.ts +4 -4
- package/src/tools/skills/execute.ts +1 -1
- package/src/tools/skills/scaffold-managed.ts +3 -2
- package/src/tools/subagent/notify-parent.ts +1 -1
- package/src/tools/system/request-permission.ts +2 -2
- package/src/tools/terminal/safe-env.ts +60 -1
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/types.ts +72 -21
- package/src/tools/ui-surface/definitions.ts +6 -5
- package/src/tts/__tests__/provider-adapters.test.ts +76 -2
- package/src/tts/providers/elevenlabs-provider.ts +75 -1
- package/src/types/onboarding-context.ts +2 -0
- package/src/util/errors.ts +17 -0
- package/src/util/platform.ts +10 -0
- package/src/watcher/__tests__/engine.test.ts +22 -0
- package/src/watcher/engine.ts +6 -2
- package/src/workspace/migrations/057-repair-stale-gemini-model-ids.ts +80 -15
- package/src/workspace/migrations/072-seed-reply-suggestion-callsite.ts +35 -22
- package/src/workspace/migrations/073-repair-recall-callsite-empty-profile.ts +3 -1
- package/src/workspace/migrations/083-system-prompt-prefix-to-file.ts +191 -0
- package/src/workspace/migrations/084-remove-legacy-skills-index.ts +276 -0
- package/src/workspace/migrations/085-memory-v2-bm25-b-reembed-disabled-v2-pages.ts +137 -0
- package/src/workspace/migrations/086-revert-stale-gemini-mis-rewrites.ts +198 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/workspace/migrations/runner.ts +39 -9
- package/src/workspace/migrations/types.ts +4 -0
- package/examples/plugins/echo/bun.lock +0 -25
- package/src/__tests__/context-window-manager.test.ts +0 -2481
- package/src/context/__tests__/compact-prompt.test.ts +0 -63
- package/src/context/prompts/compact.md +0 -26
- package/src/prompts/__tests__/build-cli-reference-section.test.ts +0 -37
- /package/src/__tests__/{secret-routes-managed-proxy.test.ts → secret-routes-platform-proxy.test.ts} +0 -0
|
@@ -32,7 +32,7 @@ mock.module("../util/logger.js", () => ({
|
|
|
32
32
|
}));
|
|
33
33
|
|
|
34
34
|
// Mutable config used by the mocked loader so individual tests can override
|
|
35
|
-
// specific fields
|
|
35
|
+
// specific fields without touching other sections.
|
|
36
36
|
const mockLoadedConfig: Record<string, unknown> = {};
|
|
37
37
|
|
|
38
38
|
mock.module("../config/loader.js", () => ({
|
|
@@ -78,12 +78,13 @@ const {
|
|
|
78
78
|
} = await import("../prompts/system-prompt.js");
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
|
-
* Extract
|
|
82
|
-
*
|
|
83
|
-
*
|
|
81
|
+
* Extract IDENTITY.md / BOOTSTRAP.md content + the user persona from the
|
|
82
|
+
* dynamic block of the system prompt, stripping configuration, skills
|
|
83
|
+
* catalog, and connected services.
|
|
84
84
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
85
|
+
* SOUL.md no longer flows through this helper — it renders as the
|
|
86
|
+
* `09-soul` workspace-backed section in the static (cached) prefix.
|
|
87
|
+
* Tests that assert on SOUL.md content slice the static block directly.
|
|
87
88
|
*/
|
|
88
89
|
function basePrompt(result: string): string {
|
|
89
90
|
// The workspace files are in the dynamic block after the cache boundary.
|
|
@@ -127,7 +128,9 @@ describe("buildSystemPrompt", () => {
|
|
|
127
128
|
const p = join(TEST_DIR, name);
|
|
128
129
|
if (existsSync(p)) rmSync(p, { recursive: true, force: true });
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
+
for (const key of Object.keys(mockLoadedConfig)) {
|
|
132
|
+
delete mockLoadedConfig[key];
|
|
133
|
+
}
|
|
131
134
|
});
|
|
132
135
|
|
|
133
136
|
test("returns empty string when no files exist", () => {
|
|
@@ -138,7 +141,11 @@ describe("buildSystemPrompt", () => {
|
|
|
138
141
|
test("uses SOUL.md when it exists", () => {
|
|
139
142
|
writeFileSync(join(TEST_DIR, "SOUL.md"), "# My Soul\n\nBe awesome.");
|
|
140
143
|
const result = buildSystemPrompt();
|
|
141
|
-
|
|
144
|
+
// SOUL.md renders as the `09-soul` workspace-backed section in the
|
|
145
|
+
// static (cached) prefix before SYSTEM_PROMPT_CACHE_BOUNDARY.
|
|
146
|
+
const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
|
147
|
+
expect(boundaryIdx).toBeGreaterThan(-1);
|
|
148
|
+
expect(result.slice(0, boundaryIdx)).toContain("# My Soul\n\nBe awesome.");
|
|
142
149
|
});
|
|
143
150
|
|
|
144
151
|
test("uses IDENTITY.md when it exists", () => {
|
|
@@ -154,9 +161,11 @@ describe("buildSystemPrompt", () => {
|
|
|
154
161
|
writeFileSync(join(TEST_DIR, "IDENTITY.md"), "# Identity\n\nI am Vellum.");
|
|
155
162
|
writeFileSync(join(TEST_DIR, "SOUL.md"), "# Soul\n\nBe thoughtful.");
|
|
156
163
|
const result = buildSystemPrompt();
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
);
|
|
164
|
+
// SOUL renders as the workspace-backed section in the static prefix;
|
|
165
|
+
// IDENTITY renders in the dynamic suffix.
|
|
166
|
+
const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
|
167
|
+
expect(result.slice(0, boundaryIdx)).toContain("# Soul\n\nBe thoughtful.");
|
|
168
|
+
expect(basePrompt(result)).toBe("# Identity\n\nI am Vellum.");
|
|
160
169
|
});
|
|
161
170
|
|
|
162
171
|
test("ignores empty SOUL.md", () => {
|
|
@@ -174,7 +183,12 @@ describe("buildSystemPrompt", () => {
|
|
|
174
183
|
test("trims whitespace from file content", () => {
|
|
175
184
|
writeFileSync(join(TEST_DIR, "SOUL.md"), "\n Be kind \n\n");
|
|
176
185
|
const result = buildSystemPrompt();
|
|
177
|
-
|
|
186
|
+
// SOUL.md renders via the `09-soul` workspace-backed section;
|
|
187
|
+
// stripCommentLines + trim run inside the section renderer.
|
|
188
|
+
const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
|
189
|
+
const staticBlock = result.slice(0, boundaryIdx);
|
|
190
|
+
expect(staticBlock).toContain("Be kind");
|
|
191
|
+
expect(staticBlock).not.toContain("\n Be kind \n");
|
|
178
192
|
});
|
|
179
193
|
|
|
180
194
|
test("does not include skills catalog in system prompt", () => {
|
|
@@ -184,7 +198,6 @@ describe("buildSystemPrompt", () => {
|
|
|
184
198
|
join(skillsDir, "release-checklist", "SKILL.md"),
|
|
185
199
|
'---\nname: "Release Checklist"\ndescription: "Deployment checks."\n---\n\nRun checks.\n',
|
|
186
200
|
);
|
|
187
|
-
writeFileSync(join(skillsDir, "SKILLS.md"), "- release-checklist\n");
|
|
188
201
|
|
|
189
202
|
writeFileSync(join(TEST_DIR, "IDENTITY.md"), "Custom identity");
|
|
190
203
|
const result = buildSystemPrompt();
|
|
@@ -200,29 +213,19 @@ describe("buildSystemPrompt", () => {
|
|
|
200
213
|
join(skillsDir, "incident-response", "SKILL.md"),
|
|
201
214
|
'---\nname: "Incident Response"\ndescription: "Triage and mitigation."\n---\n\nFollow runbook.\n',
|
|
202
215
|
);
|
|
203
|
-
writeFileSync(join(skillsDir, "SKILLS.md"), "- incident-response\n");
|
|
204
216
|
writeFileSync(join(TEST_DIR, "IDENTITY.md"), "Identity content");
|
|
205
217
|
writeFileSync(join(TEST_DIR, "SOUL.md"), "Soul content");
|
|
206
218
|
|
|
207
219
|
const result = buildSystemPrompt();
|
|
208
|
-
|
|
220
|
+
// After SOUL.md became the `09-soul` workspace-backed section, it
|
|
221
|
+
// renders in the static prefix while IDENTITY stays in the dynamic
|
|
222
|
+
// suffix — the two are no longer adjacent. Verify both are present
|
|
223
|
+
// and the skills catalog is still suppressed.
|
|
224
|
+
expect(result).toContain("Identity content");
|
|
225
|
+
expect(result).toContain("Soul content");
|
|
209
226
|
expect(result).not.toContain("## Available Skills");
|
|
210
227
|
});
|
|
211
228
|
|
|
212
|
-
test("includes external service access section", () => {
|
|
213
|
-
const result = buildSystemPrompt();
|
|
214
|
-
expect(result).toContain("## External Service Access");
|
|
215
|
-
expect(result).toContain("browser automation as last resort");
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
test("includes inline media attachment guidance", () => {
|
|
219
|
-
const result = buildSystemPrompt();
|
|
220
|
-
expect(result).toContain(
|
|
221
|
-
"Image and video attachments can render inline in chat.",
|
|
222
|
-
);
|
|
223
|
-
expect(result).toContain("attach it instead of only printing its path");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
229
|
test("does not include removed sections", () => {
|
|
227
230
|
const result = buildSystemPrompt();
|
|
228
231
|
expect(result).not.toContain("## External Communications Identity");
|
|
@@ -259,7 +262,12 @@ describe("buildSystemPrompt", () => {
|
|
|
259
262
|
writeFileSync(join(TEST_DIR, "IDENTITY.md"), "Identity");
|
|
260
263
|
writeFileSync(join(TEST_DIR, "SOUL.md"), "Soul");
|
|
261
264
|
const result = buildSystemPrompt();
|
|
262
|
-
|
|
265
|
+
// SOUL.md now renders in the static (cached) prefix via the 09-soul
|
|
266
|
+
// section, so it doesn't flow through basePrompt. Assert that
|
|
267
|
+
// IDENTITY is the only dynamic content and that SOUL is still in the
|
|
268
|
+
// full prompt.
|
|
269
|
+
expect(basePrompt(result)).toBe("Identity");
|
|
270
|
+
expect(result).toContain("Soul");
|
|
263
271
|
});
|
|
264
272
|
|
|
265
273
|
test("does not read USER.md content from disk even when the file is present", () => {
|
|
@@ -281,9 +289,12 @@ describe("buildSystemPrompt", () => {
|
|
|
281
289
|
const result = buildSystemPrompt({
|
|
282
290
|
userPersona: "# User persona\n\nName: Alice",
|
|
283
291
|
});
|
|
292
|
+
// SOUL.md renders in the static (cached) prefix via the 09-soul section
|
|
293
|
+
// and is no longer part of the dynamic block sliced by basePrompt.
|
|
284
294
|
expect(basePrompt(result)).toBe(
|
|
285
|
-
"Identity\n\
|
|
295
|
+
"Identity\n\n# User persona\n\nName: Alice",
|
|
286
296
|
);
|
|
297
|
+
expect(result).toContain("Soul");
|
|
287
298
|
});
|
|
288
299
|
|
|
289
300
|
describe("BOOTSTRAP.md user persona placeholder", () => {
|
|
@@ -478,7 +489,11 @@ describe("buildSystemPrompt", () => {
|
|
|
478
489
|
"First paragraph\n\n_ Comment between paragraphs\n\nSecond paragraph",
|
|
479
490
|
);
|
|
480
491
|
const result = buildSystemPrompt();
|
|
481
|
-
|
|
492
|
+
// SOUL.md renders in the static prefix via the 09-soul section, so we
|
|
493
|
+
// assert against the full prompt rather than basePrompt. Comment lines
|
|
494
|
+
// are stripped and surrounding whitespace collapsed by renderSection.
|
|
495
|
+
expect(result).toContain("First paragraph\n\nSecond paragraph");
|
|
496
|
+
expect(result).not.toContain("Comment between paragraphs");
|
|
482
497
|
});
|
|
483
498
|
|
|
484
499
|
test("file with only comment lines is treated as empty", () => {
|
|
@@ -487,55 +502,100 @@ describe("buildSystemPrompt", () => {
|
|
|
487
502
|
expect(basePrompt(result)).toBe("");
|
|
488
503
|
});
|
|
489
504
|
|
|
490
|
-
describe("
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
505
|
+
describe("workspace system prompt sections", () => {
|
|
506
|
+
const SYSTEM_PROMPTS_DIR = join(TEST_DIR, "prompts", "system");
|
|
507
|
+
const PREFIX_FILE = join(SYSTEM_PROMPTS_DIR, "00-prefix.md");
|
|
508
|
+
const PARALLEL_FILE = join(SYSTEM_PROMPTS_DIR, "01-parallel-tool-calls.md");
|
|
509
|
+
const PREFIX_FRONTMATTER = '---\nenabled: "!excludeCustomPrefix"\n---\n';
|
|
510
|
+
|
|
511
|
+
afterEach(() => {
|
|
512
|
+
if (existsSync(SYSTEM_PROMPTS_DIR))
|
|
513
|
+
rmSync(SYSTEM_PROMPTS_DIR, { recursive: true, force: true });
|
|
496
514
|
});
|
|
497
515
|
|
|
498
|
-
test("
|
|
499
|
-
|
|
516
|
+
test("no workspace section files → bundled defaults render directly", () => {
|
|
517
|
+
// Bundled `templates/system/` files are the source of default truth.
|
|
518
|
+
// With no workspace overrides in place, the renderer falls through to
|
|
519
|
+
// the bundled body so `01-parallel-tool-calls.md` ships its default
|
|
520
|
+
// guidance even though `ensurePromptFiles()` no longer seeds section
|
|
521
|
+
// files into the workspace.
|
|
500
522
|
const result = buildSystemPrompt();
|
|
501
|
-
expect(result.
|
|
523
|
+
expect(result).toContain("<use_parallel_tool_calls>");
|
|
524
|
+
expect(result).toContain("Batch independent tool calls");
|
|
502
525
|
});
|
|
503
526
|
|
|
504
|
-
test("
|
|
505
|
-
|
|
527
|
+
test("workspace prefix with frontmatter renders body at the very top", () => {
|
|
528
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
529
|
+
writeFileSync(
|
|
530
|
+
PREFIX_FILE,
|
|
531
|
+
PREFIX_FRONTMATTER + "You are operating in demo mode.\n",
|
|
532
|
+
);
|
|
506
533
|
const result = buildSystemPrompt();
|
|
507
|
-
expect(result.startsWith("
|
|
534
|
+
expect(result.startsWith("You are operating in demo mode.")).toBe(true);
|
|
535
|
+
// Prefix lives in the static (cached) block.
|
|
536
|
+
const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
|
537
|
+
expect(boundaryIdx).toBeGreaterThan(-1);
|
|
538
|
+
const staticBlock = result.slice(0, boundaryIdx);
|
|
539
|
+
expect(staticBlock).toContain("You are operating in demo mode.");
|
|
508
540
|
});
|
|
509
541
|
|
|
510
|
-
test("
|
|
511
|
-
|
|
542
|
+
test("workspace file without frontmatter is rendered as-is (always-on)", () => {
|
|
543
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
544
|
+
writeFileSync(PREFIX_FILE, "Plain prefix, no frontmatter.\n");
|
|
512
545
|
const result = buildSystemPrompt();
|
|
513
|
-
expect(result.startsWith("
|
|
546
|
+
expect(result.startsWith("Plain prefix, no frontmatter.")).toBe(true);
|
|
514
547
|
});
|
|
515
548
|
|
|
516
|
-
test("
|
|
517
|
-
|
|
549
|
+
test("renders nothing when workspace prefix body is empty after stripping", () => {
|
|
550
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
551
|
+
writeFileSync(PREFIX_FILE, PREFIX_FRONTMATTER);
|
|
518
552
|
const result = buildSystemPrompt();
|
|
519
|
-
|
|
520
|
-
//
|
|
553
|
+
// Frontmatter-only override → workspace wins (existsSync(workspace) is
|
|
554
|
+
// true) but body strips to empty → prefix renders nothing. No leaked
|
|
555
|
+
// frontmatter at top, but the bundled `01-parallel-tool-calls.md`
|
|
556
|
+
// default still renders because that slot has no workspace override.
|
|
557
|
+
expect(result.startsWith("---")).toBe(false);
|
|
521
558
|
expect(result).toContain("<use_parallel_tool_calls>");
|
|
522
|
-
// Prefix lives in the static (cached) block, not the dynamic block.
|
|
523
|
-
const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
|
524
|
-
expect(boundaryIdx).toBeGreaterThan(-1);
|
|
525
|
-
const staticBlock = result.slice(0, boundaryIdx);
|
|
526
|
-
expect(staticBlock).toContain("You are operating in demo mode.");
|
|
527
559
|
});
|
|
528
560
|
|
|
529
|
-
test("
|
|
530
|
-
|
|
531
|
-
|
|
561
|
+
test("comment-only workspace prefix body strips to nothing — no comment text leaks", () => {
|
|
562
|
+
// Bundled `00-prefix.md` ships frontmatter-only (empty body), so
|
|
563
|
+
// either way the prefix slot contributes nothing — workspace
|
|
564
|
+
// override stripped to empty by `_` comment lines, or bundled
|
|
565
|
+
// fallback already empty. This test asserts only that the
|
|
566
|
+
// `_`-prefixed comment text does not bleed into the output.
|
|
567
|
+
// Bundled sections at higher slots still render (covered by
|
|
568
|
+
// other tests).
|
|
569
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
570
|
+
writeFileSync(
|
|
571
|
+
PREFIX_FILE,
|
|
572
|
+
PREFIX_FRONTMATTER +
|
|
573
|
+
"_ UNIQUE_COMMENT_MARKER_PURPLE_OCTOPUS\n_ UNIQUE_COMMENT_MARKER_GREEN_HELICOPTER\n",
|
|
574
|
+
);
|
|
575
|
+
const result = buildSystemPrompt();
|
|
576
|
+
expect(result).not.toContain("UNIQUE_COMMENT_MARKER_PURPLE_OCTOPUS");
|
|
577
|
+
expect(result).not.toContain("UNIQUE_COMMENT_MARKER_GREEN_HELICOPTER");
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test("strips comment lines and trims whitespace from rendered body", () => {
|
|
581
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
582
|
+
writeFileSync(
|
|
583
|
+
PREFIX_FILE,
|
|
584
|
+
PREFIX_FRONTMATTER +
|
|
585
|
+
"_ inline note\n\n Pretend you are a pirate. \n\n",
|
|
586
|
+
);
|
|
532
587
|
const result = buildSystemPrompt();
|
|
533
588
|
expect(result.startsWith("Pretend you are a pirate.")).toBe(true);
|
|
589
|
+
expect(result).not.toContain("inline note");
|
|
534
590
|
});
|
|
535
591
|
|
|
536
|
-
test("multi-line
|
|
537
|
-
|
|
538
|
-
|
|
592
|
+
test("multi-line bodies are preserved verbatim after stripping", () => {
|
|
593
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
594
|
+
writeFileSync(
|
|
595
|
+
PREFIX_FILE,
|
|
596
|
+
PREFIX_FRONTMATTER +
|
|
597
|
+
"# Org Guardrails\n\n- Never discuss pricing.\n- Escalate refunds.\n",
|
|
598
|
+
);
|
|
539
599
|
const result = buildSystemPrompt();
|
|
540
600
|
expect(
|
|
541
601
|
result.startsWith(
|
|
@@ -545,12 +605,613 @@ describe("buildSystemPrompt", () => {
|
|
|
545
605
|
});
|
|
546
606
|
|
|
547
607
|
test("workspace file content still appears after prefix", () => {
|
|
548
|
-
|
|
608
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
609
|
+
writeFileSync(PREFIX_FILE, PREFIX_FRONTMATTER + "Custom prefix\n");
|
|
549
610
|
writeFileSync(join(TEST_DIR, "IDENTITY.md"), "I am Vellum.");
|
|
550
611
|
const result = buildSystemPrompt();
|
|
551
612
|
expect(result.startsWith("Custom prefix")).toBe(true);
|
|
552
613
|
expect(basePrompt(result)).toBe("I am Vellum.");
|
|
553
614
|
});
|
|
615
|
+
|
|
616
|
+
test("parallel tool calls section is sourced from workspace when present", () => {
|
|
617
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
618
|
+
writeFileSync(
|
|
619
|
+
PARALLEL_FILE,
|
|
620
|
+
"<use_parallel_tool_calls>\nCustomized parallel guidance.\n</use_parallel_tool_calls>\n",
|
|
621
|
+
);
|
|
622
|
+
const result = buildSystemPrompt();
|
|
623
|
+
expect(result).toContain("Customized parallel guidance.");
|
|
624
|
+
// Body of the bundled file must not leak in.
|
|
625
|
+
expect(result).not.toContain("Batch independent tool calls");
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
test("comment-only parallel file suppresses the section entirely", () => {
|
|
629
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
630
|
+
writeFileSync(PARALLEL_FILE, "_ silenced\n");
|
|
631
|
+
const result = buildSystemPrompt();
|
|
632
|
+
expect(result).not.toContain("<use_parallel_tool_calls>");
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test("frontmatter `enabled: !excludeCustomPrefix` suppresses prefix when flag is true", () => {
|
|
636
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
637
|
+
writeFileSync(
|
|
638
|
+
PREFIX_FILE,
|
|
639
|
+
PREFIX_FRONTMATTER + "Should be excluded by sidechain.\n",
|
|
640
|
+
);
|
|
641
|
+
const result = buildSystemPrompt({ excludeCustomPrefix: true });
|
|
642
|
+
expect(result).not.toContain("Should be excluded by sidechain.");
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test("frontmatter `enabled: !excludeCustomPrefix` renders prefix when flag is false", () => {
|
|
646
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
647
|
+
writeFileSync(PREFIX_FILE, PREFIX_FRONTMATTER + "Default-on prefix.\n");
|
|
648
|
+
const result = buildSystemPrompt({ excludeCustomPrefix: false });
|
|
649
|
+
expect(result.startsWith("Default-on prefix.")).toBe(true);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
test("frontmatter `enabled: <unknown-key>` treats key as falsy → suppresses", () => {
|
|
653
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
654
|
+
writeFileSync(
|
|
655
|
+
PREFIX_FILE,
|
|
656
|
+
"---\nenabled: someUnknownFlag\n---\nShould not render.\n",
|
|
657
|
+
);
|
|
658
|
+
const result = buildSystemPrompt();
|
|
659
|
+
expect(result).not.toContain("Should not render.");
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
test("frontmatter `enabled: false` (literal boolean) suppresses the section", () => {
|
|
663
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
664
|
+
writeFileSync(
|
|
665
|
+
PREFIX_FILE,
|
|
666
|
+
"---\nenabled: false\n---\nShould not render.\n",
|
|
667
|
+
);
|
|
668
|
+
const result = buildSystemPrompt();
|
|
669
|
+
expect(result).not.toContain("Should not render.");
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
test("workspace `enabled: false` on a slot WITH a bundled file suppresses the bundled default", () => {
|
|
673
|
+
// Override wins regardless of body — the workspace file's `enabled: false`
|
|
674
|
+
// frontmatter wins over the bundled `01-parallel-tool-calls.md` default,
|
|
675
|
+
// so the bundled body must not leak into the rendered output. This is
|
|
676
|
+
// the explicit "user silenced this section" path.
|
|
677
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
678
|
+
writeFileSync(
|
|
679
|
+
PARALLEL_FILE,
|
|
680
|
+
"---\nenabled: false\n---\nIgnored body.\n",
|
|
681
|
+
);
|
|
682
|
+
const result = buildSystemPrompt();
|
|
683
|
+
expect(result).not.toContain("<use_parallel_tool_calls>");
|
|
684
|
+
expect(result).not.toContain("Batch independent tool calls");
|
|
685
|
+
expect(result).not.toContain("Ignored body.");
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
test("workspace-only sections (no bundled counterpart) render — discovery union covers both dirs", () => {
|
|
689
|
+
// The renderer collects section ids as the union of bundled and
|
|
690
|
+
// workspace filenames, so any numbered `.md` a user drops into
|
|
691
|
+
// `<workspace>/prompts/system/` joins the render order automatically
|
|
692
|
+
// even when no bundled file shares its id.
|
|
693
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
694
|
+
writeFileSync(
|
|
695
|
+
join(SYSTEM_PROMPTS_DIR, "99-org-policy.md"),
|
|
696
|
+
"# Org policy\n\nUnique workspace-only marker A1B2C3.\n",
|
|
697
|
+
);
|
|
698
|
+
const result = buildSystemPrompt();
|
|
699
|
+
expect(result).toContain("Unique workspace-only marker A1B2C3.");
|
|
700
|
+
// Sort order is filename-driven; the new section sorts after `01-`,
|
|
701
|
+
// so it must appear after the parallel-tool-calls block when both
|
|
702
|
+
// are present.
|
|
703
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
704
|
+
writeFileSync(
|
|
705
|
+
PARALLEL_FILE,
|
|
706
|
+
"<use_parallel_tool_calls>\nbatched.\n</use_parallel_tool_calls>\n",
|
|
707
|
+
);
|
|
708
|
+
const ordered = buildSystemPrompt();
|
|
709
|
+
const parallelIdx = ordered.indexOf("batched.");
|
|
710
|
+
const orgIdx = ordered.indexOf("Unique workspace-only marker A1B2C3.");
|
|
711
|
+
expect(parallelIdx).toBeGreaterThan(-1);
|
|
712
|
+
expect(orgIdx).toBeGreaterThan(parallelIdx);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
describe("containerized section (slot 02)", () => {
|
|
716
|
+
const CONTAINERIZED_FILE = join(SYSTEM_PROMPTS_DIR, "02-containerized.md");
|
|
717
|
+
|
|
718
|
+
// The runtime gate is `isContainerized` on the render context, sourced
|
|
719
|
+
// from `getIsContainerized()` which reads `process.env.IS_CONTAINERIZED`.
|
|
720
|
+
// Tests toggle the env var directly and restore it in `finally`.
|
|
721
|
+
let priorIsContainerized: string | undefined;
|
|
722
|
+
|
|
723
|
+
beforeEach(() => {
|
|
724
|
+
priorIsContainerized = process.env.IS_CONTAINERIZED;
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
afterEach(() => {
|
|
728
|
+
if (priorIsContainerized === undefined)
|
|
729
|
+
delete process.env.IS_CONTAINERIZED;
|
|
730
|
+
else process.env.IS_CONTAINERIZED = priorIsContainerized;
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
test("renders the section when IS_CONTAINERIZED=true with {{workspaceDir}} interpolated", () => {
|
|
734
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
735
|
+
writeFileSync(
|
|
736
|
+
CONTAINERIZED_FILE,
|
|
737
|
+
"---\nenabled: isContainerized\n---\n" +
|
|
738
|
+
"Container mounted at `{{workspaceDir}}`. Persist accordingly.\n",
|
|
739
|
+
);
|
|
740
|
+
process.env.IS_CONTAINERIZED = "true";
|
|
741
|
+
const result = buildSystemPrompt();
|
|
742
|
+
expect(result).toContain(
|
|
743
|
+
`Container mounted at \`${TEST_DIR}\`. Persist accordingly.`,
|
|
744
|
+
);
|
|
745
|
+
// The literal `{{workspaceDir}}` must be substituted, not leaked.
|
|
746
|
+
expect(result).not.toContain("{{workspaceDir}}");
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
test("omits the section when IS_CONTAINERIZED is unset", () => {
|
|
750
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
751
|
+
writeFileSync(
|
|
752
|
+
CONTAINERIZED_FILE,
|
|
753
|
+
"---\nenabled: isContainerized\n---\nContainer guidance body.\n",
|
|
754
|
+
);
|
|
755
|
+
delete process.env.IS_CONTAINERIZED;
|
|
756
|
+
const result = buildSystemPrompt();
|
|
757
|
+
expect(result).not.toContain("Container guidance body.");
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
test("omits the section when IS_CONTAINERIZED=false (string)", () => {
|
|
761
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
762
|
+
writeFileSync(
|
|
763
|
+
CONTAINERIZED_FILE,
|
|
764
|
+
"---\nenabled: isContainerized\n---\nContainer guidance body.\n",
|
|
765
|
+
);
|
|
766
|
+
process.env.IS_CONTAINERIZED = "false";
|
|
767
|
+
const result = buildSystemPrompt();
|
|
768
|
+
expect(result).not.toContain("Container guidance body.");
|
|
769
|
+
});
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
describe("cli-reference section (slot 03)", () => {
|
|
773
|
+
const CLI_REFERENCE_FILE = join(
|
|
774
|
+
SYSTEM_PROMPTS_DIR,
|
|
775
|
+
"03-cli-reference.md",
|
|
776
|
+
);
|
|
777
|
+
|
|
778
|
+
test("workspace cli-reference file is rendered into the static block", () => {
|
|
779
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
780
|
+
writeFileSync(
|
|
781
|
+
CLI_REFERENCE_FILE,
|
|
782
|
+
"## Assistant CLI\n\nRun `assistant --help` to discover commands.\n",
|
|
783
|
+
);
|
|
784
|
+
const result = buildSystemPrompt();
|
|
785
|
+
expect(result).toContain("## Assistant CLI");
|
|
786
|
+
expect(result).toContain(
|
|
787
|
+
"Run `assistant --help` to discover commands.",
|
|
788
|
+
);
|
|
789
|
+
// Section lives in the static (cached) block.
|
|
790
|
+
const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
|
791
|
+
expect(boundaryIdx).toBeGreaterThan(-1);
|
|
792
|
+
const staticBlock = result.slice(0, boundaryIdx);
|
|
793
|
+
expect(staticBlock).toContain("## Assistant CLI");
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
test("bundled cli-reference default renders when no workspace override", () => {
|
|
797
|
+
// Bundled `03-cli-reference.md` is the source of default truth. No
|
|
798
|
+
// workspace override → renderer falls through to bundled body, so
|
|
799
|
+
// `## Assistant CLI` lands in the static block automatically.
|
|
800
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
801
|
+
const result = buildSystemPrompt();
|
|
802
|
+
expect(result).toContain("## Assistant CLI");
|
|
803
|
+
expect(result).toContain("`assistant` CLI is available");
|
|
804
|
+
});
|
|
805
|
+
});
|
|
806
|
+
|
|
807
|
+
describe("access-preference section (slot 05)", () => {
|
|
808
|
+
const ACCESS_FILE = join(SYSTEM_PROMPTS_DIR, "05-access-preference.md");
|
|
809
|
+
// Mirrors the bundled `templates/system/05-access-preference.md` — both
|
|
810
|
+
// variants live in the markdown body and the renderer picks one via
|
|
811
|
+
// mustache-style `{{#hasNoClient}}` / `{{^hasNoClient}}` conditionals.
|
|
812
|
+
const TEMPLATE_BODY = [
|
|
813
|
+
"## External Service Access",
|
|
814
|
+
"",
|
|
815
|
+
"{{#hasNoClient}}",
|
|
816
|
+
"Priority: (1) sandbox `bash` — install tools yourself; (2) browser automation as last resort (no API, visual interaction, or OAuth consent).",
|
|
817
|
+
"{{/hasNoClient}}",
|
|
818
|
+
"{{^hasNoClient}}",
|
|
819
|
+
"Priority: (1) sandbox `bash` - install tools yourself, only fall back to host when you need local files/auth; (2) `host_bash` with CLIs (gh, aws, etc.) using --json flags; (3) browser automation as last resort (no API, visual interaction, or OAuth consent).",
|
|
820
|
+
"{{/hasNoClient}}",
|
|
821
|
+
"",
|
|
822
|
+
].join("\n");
|
|
823
|
+
|
|
824
|
+
test("with-client (default) renders the three-tier priority list", () => {
|
|
825
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
826
|
+
writeFileSync(ACCESS_FILE, TEMPLATE_BODY);
|
|
827
|
+
const result = buildSystemPrompt();
|
|
828
|
+
expect(result).toContain("## External Service Access");
|
|
829
|
+
expect(result).toContain("`host_bash` with CLIs");
|
|
830
|
+
expect(result).toContain("browser automation as last resort");
|
|
831
|
+
// The no-client body (em-dash separator after sandbox `bash`) must
|
|
832
|
+
// not leak when the with-client variant is active.
|
|
833
|
+
expect(result).not.toContain("install tools yourself; (2) browser");
|
|
834
|
+
// Section lives in the static (cached) block.
|
|
835
|
+
const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
|
836
|
+
expect(boundaryIdx).toBeGreaterThan(-1);
|
|
837
|
+
const staticBlock = result.slice(0, boundaryIdx);
|
|
838
|
+
expect(staticBlock).toContain("## External Service Access");
|
|
839
|
+
});
|
|
840
|
+
|
|
841
|
+
test("hasNoClient=true renders the two-tier (no host_bash) priority list", () => {
|
|
842
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
843
|
+
writeFileSync(ACCESS_FILE, TEMPLATE_BODY);
|
|
844
|
+
const result = buildSystemPrompt({ hasNoClient: true });
|
|
845
|
+
expect(result).toContain("## External Service Access");
|
|
846
|
+
expect(result).toContain("browser automation as last resort");
|
|
847
|
+
// The host_bash tier must be absent in the no-client variant.
|
|
848
|
+
expect(result).not.toContain("`host_bash` with CLIs");
|
|
849
|
+
// The no-client body uses an em-dash + semicolon separator after
|
|
850
|
+
// sandbox `bash`; the with-client body uses a comma — guard against
|
|
851
|
+
// the wrong variant leaking through.
|
|
852
|
+
expect(result).toContain("install tools yourself; (2) browser");
|
|
853
|
+
expect(result).not.toContain(
|
|
854
|
+
"only fall back to host when you need local files/auth",
|
|
855
|
+
);
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
test("standalone tag lines do not bleed extra blank lines into output", () => {
|
|
859
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
860
|
+
writeFileSync(ACCESS_FILE, TEMPLATE_BODY);
|
|
861
|
+
const result = buildSystemPrompt();
|
|
862
|
+
// The heading and the active variant body should sit on adjacent
|
|
863
|
+
// lines separated by exactly one blank line — no triple-newline
|
|
864
|
+
// artifacts from the section markers.
|
|
865
|
+
expect(result).toMatch(
|
|
866
|
+
/## External Service Access\n\nPriority: \(1\) sandbox `bash` -/,
|
|
867
|
+
);
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
test("bundled access-preference default renders when no workspace override", () => {
|
|
871
|
+
// Bundled `05-access-preference.md` carries both with-client and
|
|
872
|
+
// no-client variants inline behind mustache section conditionals,
|
|
873
|
+
// so the default body renders without any workspace file present.
|
|
874
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
875
|
+
const result = buildSystemPrompt();
|
|
876
|
+
expect(result).toContain("## External Service Access");
|
|
877
|
+
expect(result).toContain("`host_bash` with CLIs");
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
test("renders after the attachment section to preserve original order", () => {
|
|
881
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
882
|
+
writeFileSync(
|
|
883
|
+
join(SYSTEM_PROMPTS_DIR, "04-attachment.md"),
|
|
884
|
+
"## Sending Files to the User\n\nbody.\n",
|
|
885
|
+
);
|
|
886
|
+
writeFileSync(ACCESS_FILE, TEMPLATE_BODY);
|
|
887
|
+
const result = buildSystemPrompt();
|
|
888
|
+
const attachmentIdx = result.indexOf("## Sending Files to the User");
|
|
889
|
+
const accessIdx = result.indexOf("## External Service Access");
|
|
890
|
+
expect(attachmentIdx).toBeGreaterThan(-1);
|
|
891
|
+
expect(accessIdx).toBeGreaterThan(-1);
|
|
892
|
+
expect(attachmentIdx).toBeLessThan(accessIdx);
|
|
893
|
+
});
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
describe("mustache section interpolation", () => {
|
|
897
|
+
// Reuse slot 00 (prefix) — its default-on `enabled` predicate is
|
|
898
|
+
// already covered by other tests; here we only care about body
|
|
899
|
+
// interpolation shape.
|
|
900
|
+
const SECTION_FILE = join(SYSTEM_PROMPTS_DIR, "00-prefix.md");
|
|
901
|
+
const FRONTMATTER = '---\nenabled: "!excludeCustomPrefix"\n---\n';
|
|
902
|
+
|
|
903
|
+
test("{{#flag}}body{{/flag}} renders body when ctx[flag] is truthy", () => {
|
|
904
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
905
|
+
writeFileSync(
|
|
906
|
+
SECTION_FILE,
|
|
907
|
+
FRONTMATTER + "before {{#hasNoClient}}YES{{/hasNoClient}} after\n",
|
|
908
|
+
);
|
|
909
|
+
const result = buildSystemPrompt({ hasNoClient: true });
|
|
910
|
+
expect(result).toContain("before YES after");
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
test("{{#flag}}body{{/flag}} omits body when ctx[flag] is falsy", () => {
|
|
914
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
915
|
+
writeFileSync(
|
|
916
|
+
SECTION_FILE,
|
|
917
|
+
FRONTMATTER + "before {{#hasNoClient}}YES{{/hasNoClient}} after\n",
|
|
918
|
+
);
|
|
919
|
+
const result = buildSystemPrompt({ hasNoClient: false });
|
|
920
|
+
expect(result).toContain("before after");
|
|
921
|
+
expect(result).not.toContain("YES");
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
test("{{^flag}}body{{/flag}} renders body when ctx[flag] is falsy", () => {
|
|
925
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
926
|
+
writeFileSync(
|
|
927
|
+
SECTION_FILE,
|
|
928
|
+
FRONTMATTER + "before {{^hasNoClient}}NO{{/hasNoClient}} after\n",
|
|
929
|
+
);
|
|
930
|
+
const result = buildSystemPrompt({ hasNoClient: false });
|
|
931
|
+
expect(result).toContain("before NO after");
|
|
932
|
+
});
|
|
933
|
+
|
|
934
|
+
test("{{^flag}}body{{/flag}} omits body when ctx[flag] is truthy", () => {
|
|
935
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
936
|
+
writeFileSync(
|
|
937
|
+
SECTION_FILE,
|
|
938
|
+
FRONTMATTER + "before {{^hasNoClient}}NO{{/hasNoClient}} after\n",
|
|
939
|
+
);
|
|
940
|
+
const result = buildSystemPrompt({ hasNoClient: true });
|
|
941
|
+
expect(result).toContain("before after");
|
|
942
|
+
expect(result).not.toContain("NO");
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
test("paired {{#flag}} + {{^flag}} acts as if/else", () => {
|
|
946
|
+
// Use long unique markers — single letters collide with substrings
|
|
947
|
+
// in the rest of the system prompt (e.g. "B" lives inside
|
|
948
|
+
// SYSTEM_PROMPT_CACHE_BOUNDARY, "A" inside "API keys").
|
|
949
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
950
|
+
writeFileSync(
|
|
951
|
+
SECTION_FILE,
|
|
952
|
+
FRONTMATTER +
|
|
953
|
+
"{{#hasNoClient}}NO_CLIENT_BRANCH_MARKER{{/hasNoClient}}{{^hasNoClient}}WITH_CLIENT_BRANCH_MARKER{{/hasNoClient}}\n",
|
|
954
|
+
);
|
|
955
|
+
const onTrue = buildSystemPrompt({ hasNoClient: true });
|
|
956
|
+
expect(onTrue).toContain("NO_CLIENT_BRANCH_MARKER");
|
|
957
|
+
expect(onTrue).not.toContain("WITH_CLIENT_BRANCH_MARKER");
|
|
958
|
+
const onFalse = buildSystemPrompt({ hasNoClient: false });
|
|
959
|
+
expect(onFalse).toContain("WITH_CLIENT_BRANCH_MARKER");
|
|
960
|
+
expect(onFalse).not.toContain("NO_CLIENT_BRANCH_MARKER");
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
test("section body may contain a {{variable}} substitution", () => {
|
|
964
|
+
// Gate on `hasNoClient` (passed explicitly, so we don't depend on
|
|
965
|
+
// ambient test-env state for `isContainerized`). The section body
|
|
966
|
+
// includes a `{{workspaceDir}}` interpolation that should resolve
|
|
967
|
+
// to the test workspace path.
|
|
968
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
969
|
+
writeFileSync(
|
|
970
|
+
SECTION_FILE,
|
|
971
|
+
FRONTMATTER +
|
|
972
|
+
"{{#hasNoClient}}cwd={{workspaceDir}}{{/hasNoClient}}\n",
|
|
973
|
+
);
|
|
974
|
+
const result = buildSystemPrompt({ hasNoClient: true });
|
|
975
|
+
expect(result).toMatch(/cwd=\S+/);
|
|
976
|
+
expect(result).not.toContain("{{workspaceDir}}");
|
|
977
|
+
});
|
|
978
|
+
|
|
979
|
+
test("section keys missing from ctx gate the body off (treated as falsy)", () => {
|
|
980
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
981
|
+
writeFileSync(
|
|
982
|
+
SECTION_FILE,
|
|
983
|
+
FRONTMATTER + "{{#noSuchFlag}}hidden{{/noSuchFlag}}\n",
|
|
984
|
+
);
|
|
985
|
+
const result = buildSystemPrompt();
|
|
986
|
+
expect(result).not.toContain("{{#noSuchFlag}}");
|
|
987
|
+
expect(result).not.toContain("hidden");
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
test("inverted section keys missing from ctx render the body (undefined is falsy)", () => {
|
|
991
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
992
|
+
writeFileSync(
|
|
993
|
+
SECTION_FILE,
|
|
994
|
+
FRONTMATTER + "{{^noSuchFlag}}shown{{/noSuchFlag}}\n",
|
|
995
|
+
);
|
|
996
|
+
const result = buildSystemPrompt();
|
|
997
|
+
expect(result).toContain("shown");
|
|
998
|
+
});
|
|
999
|
+
});
|
|
1000
|
+
|
|
1001
|
+
describe("attachment section (slot 04)", () => {
|
|
1002
|
+
const ATTACHMENT_FILE = join(SYSTEM_PROMPTS_DIR, "04-attachment.md");
|
|
1003
|
+
|
|
1004
|
+
test("workspace attachment file is rendered into the static block", () => {
|
|
1005
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
1006
|
+
writeFileSync(
|
|
1007
|
+
ATTACHMENT_FILE,
|
|
1008
|
+
"## Sending Files to the User\n\nUse the `<vellum-attachment />` tag.\n",
|
|
1009
|
+
);
|
|
1010
|
+
const result = buildSystemPrompt();
|
|
1011
|
+
expect(result).toContain("## Sending Files to the User");
|
|
1012
|
+
expect(result).toContain("Use the `<vellum-attachment />` tag.");
|
|
1013
|
+
// Section lives in the static (cached) block.
|
|
1014
|
+
const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
|
1015
|
+
expect(boundaryIdx).toBeGreaterThan(-1);
|
|
1016
|
+
const staticBlock = result.slice(0, boundaryIdx);
|
|
1017
|
+
expect(staticBlock).toContain("## Sending Files to the User");
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
test("renders after the cli-reference section to preserve original order", () => {
|
|
1021
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
1022
|
+
writeFileSync(
|
|
1023
|
+
join(SYSTEM_PROMPTS_DIR, "03-cli-reference.md"),
|
|
1024
|
+
"## Assistant CLI\n\nUse `assistant --help`.\n",
|
|
1025
|
+
);
|
|
1026
|
+
writeFileSync(
|
|
1027
|
+
ATTACHMENT_FILE,
|
|
1028
|
+
"## Sending Files to the User\n\nbody.\n",
|
|
1029
|
+
);
|
|
1030
|
+
const result = buildSystemPrompt();
|
|
1031
|
+
const cliIdx = result.indexOf("## Assistant CLI");
|
|
1032
|
+
const attachmentIdx = result.indexOf("## Sending Files to the User");
|
|
1033
|
+
expect(cliIdx).toBeGreaterThan(-1);
|
|
1034
|
+
expect(attachmentIdx).toBeGreaterThan(-1);
|
|
1035
|
+
expect(cliIdx).toBeLessThan(attachmentIdx);
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
test("bundled attachment default renders when no workspace override", () => {
|
|
1039
|
+
// Bundled `04-attachment.md` is the source of default truth; no
|
|
1040
|
+
// workspace override → renderer falls through to bundled body.
|
|
1041
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
1042
|
+
const result = buildSystemPrompt();
|
|
1043
|
+
expect(result).toContain("## Sending Files to the User");
|
|
1044
|
+
expect(result).toContain("<vellum-attachment");
|
|
1045
|
+
});
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
describe("credential-security section (slot 06)", () => {
|
|
1049
|
+
const CREDENTIAL_FILE = join(
|
|
1050
|
+
SYSTEM_PROMPTS_DIR,
|
|
1051
|
+
"06-credential-security.md",
|
|
1052
|
+
);
|
|
1053
|
+
|
|
1054
|
+
test("workspace credential-security file is rendered into the static block", () => {
|
|
1055
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
1056
|
+
writeFileSync(
|
|
1057
|
+
CREDENTIAL_FILE,
|
|
1058
|
+
"## Credential Security\n\nWorkspace override marker BRAVO_TANGO_7.\n",
|
|
1059
|
+
);
|
|
1060
|
+
const result = buildSystemPrompt();
|
|
1061
|
+
expect(result).toContain("## Credential Security");
|
|
1062
|
+
expect(result).toContain("Workspace override marker BRAVO_TANGO_7.");
|
|
1063
|
+
// Section lives in the static (cached) block.
|
|
1064
|
+
const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
|
1065
|
+
expect(boundaryIdx).toBeGreaterThan(-1);
|
|
1066
|
+
const staticBlock = result.slice(0, boundaryIdx);
|
|
1067
|
+
expect(staticBlock).toContain("## Credential Security");
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
test("bundled credential-security default renders when no workspace override", () => {
|
|
1071
|
+
// Bundled `06-credential-security` registry entry is the source of
|
|
1072
|
+
// default truth; no workspace override → renderer falls through to
|
|
1073
|
+
// bundled body.
|
|
1074
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
1075
|
+
const result = buildSystemPrompt();
|
|
1076
|
+
expect(result).toContain("## Credential Security");
|
|
1077
|
+
expect(result).toContain("Never ask users to share secrets");
|
|
1078
|
+
expect(result).toContain("`credential_store` tool");
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
test("renders after the access-preference section to preserve original order", () => {
|
|
1082
|
+
// Static-block order from the pre-registry inline build was
|
|
1083
|
+
// access-preference → credential-security. The numeric prefix on
|
|
1084
|
+
// the registry id (`06-` > `05-`) preserves that order.
|
|
1085
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
1086
|
+
const result = buildSystemPrompt();
|
|
1087
|
+
const accessIdx = result.indexOf("## External Service Access");
|
|
1088
|
+
const credentialIdx = result.indexOf("## Credential Security");
|
|
1089
|
+
expect(accessIdx).toBeGreaterThan(-1);
|
|
1090
|
+
expect(credentialIdx).toBeGreaterThan(-1);
|
|
1091
|
+
expect(accessIdx).toBeLessThan(credentialIdx);
|
|
1092
|
+
});
|
|
1093
|
+
});
|
|
1094
|
+
|
|
1095
|
+
describe("external-content section (slot 07)", () => {
|
|
1096
|
+
const EXTERNAL_FILE = join(
|
|
1097
|
+
SYSTEM_PROMPTS_DIR,
|
|
1098
|
+
"07-external-content.md",
|
|
1099
|
+
);
|
|
1100
|
+
|
|
1101
|
+
test("workspace external-content file is rendered into the static block", () => {
|
|
1102
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
1103
|
+
writeFileSync(
|
|
1104
|
+
EXTERNAL_FILE,
|
|
1105
|
+
"## External Content\n\nWorkspace override marker NEBULA_9X.\n",
|
|
1106
|
+
);
|
|
1107
|
+
const result = buildSystemPrompt();
|
|
1108
|
+
expect(result).toContain("## External Content");
|
|
1109
|
+
expect(result).toContain("Workspace override marker NEBULA_9X.");
|
|
1110
|
+
// Section lives in the static (cached) block.
|
|
1111
|
+
const boundaryIdx = result.indexOf(SYSTEM_PROMPT_CACHE_BOUNDARY);
|
|
1112
|
+
expect(boundaryIdx).toBeGreaterThan(-1);
|
|
1113
|
+
const staticBlock = result.slice(0, boundaryIdx);
|
|
1114
|
+
expect(staticBlock).toContain("## External Content");
|
|
1115
|
+
});
|
|
1116
|
+
|
|
1117
|
+
test("bundled external-content default renders when no workspace override", () => {
|
|
1118
|
+
// Bundled `07-external-content` registry entry is the source of
|
|
1119
|
+
// default truth; no workspace override → renderer falls through to
|
|
1120
|
+
// bundled body.
|
|
1121
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
1122
|
+
const result = buildSystemPrompt();
|
|
1123
|
+
expect(result).toContain("## External Content");
|
|
1124
|
+
expect(result).toContain("third-party data");
|
|
1125
|
+
expect(result).toContain("`<external_content>`");
|
|
1126
|
+
});
|
|
1127
|
+
|
|
1128
|
+
test("renders after the credential-security section to preserve original order", () => {
|
|
1129
|
+
// Static-block order from the pre-registry inline build was
|
|
1130
|
+
// credential-security → external-content. The numeric prefix on
|
|
1131
|
+
// the registry id (`07-` > `06-`) preserves that order.
|
|
1132
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
1133
|
+
const result = buildSystemPrompt();
|
|
1134
|
+
const credentialIdx = result.indexOf("## Credential Security");
|
|
1135
|
+
const externalIdx = result.indexOf("## External Content");
|
|
1136
|
+
expect(credentialIdx).toBeGreaterThan(-1);
|
|
1137
|
+
expect(externalIdx).toBeGreaterThan(-1);
|
|
1138
|
+
expect(credentialIdx).toBeLessThan(externalIdx);
|
|
1139
|
+
});
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
describe("background-conversation section (slot 08)", () => {
|
|
1143
|
+
const BACKGROUND_FILE = join(
|
|
1144
|
+
SYSTEM_PROMPTS_DIR,
|
|
1145
|
+
"08-background-conversation.md",
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
test("bundled default renders when isBackgroundConversation is true", () => {
|
|
1149
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
1150
|
+
const result = buildSystemPrompt({ isBackgroundConversation: true });
|
|
1151
|
+
expect(result).toContain("## Background Conversation");
|
|
1152
|
+
expect(result).toContain("non-interactive background job");
|
|
1153
|
+
expect(result).toContain("`notifications` skill");
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
test("bundled default is gated out when isBackgroundConversation is false", () => {
|
|
1157
|
+
// Mustache `{{#isBackgroundConversation}}...{{/isBackgroundConversation}}`
|
|
1158
|
+
// wraps the entire heading + body so the slot interpolates to empty
|
|
1159
|
+
// in foreground conversations and the renderer drops it.
|
|
1160
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
1161
|
+
const result = buildSystemPrompt({ isBackgroundConversation: false });
|
|
1162
|
+
expect(result).not.toContain("## Background Conversation");
|
|
1163
|
+
});
|
|
1164
|
+
|
|
1165
|
+
test("bundled default is gated out when isBackgroundConversation is omitted", () => {
|
|
1166
|
+
// `ctx.isBackgroundConversation` is normalized to `false` when the
|
|
1167
|
+
// caller omits the flag, so the gated section drops out cleanly.
|
|
1168
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
1169
|
+
const result = buildSystemPrompt();
|
|
1170
|
+
expect(result).not.toContain("## Background Conversation");
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
test("workspace override is also gated by isBackgroundConversation", () => {
|
|
1174
|
+
// Workspace overrides flow through the same mustache interpolation,
|
|
1175
|
+
// so authors can rely on the same `{{#isBackgroundConversation}}`
|
|
1176
|
+
// gate in their custom body.
|
|
1177
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
1178
|
+
writeFileSync(
|
|
1179
|
+
BACKGROUND_FILE,
|
|
1180
|
+
"{{#isBackgroundConversation}}## Background Conversation\n\nWorkspace override marker COMET_3K.\n{{/isBackgroundConversation}}\n",
|
|
1181
|
+
);
|
|
1182
|
+
|
|
1183
|
+
const offResult = buildSystemPrompt({ isBackgroundConversation: false });
|
|
1184
|
+
expect(offResult).not.toContain("## Background Conversation");
|
|
1185
|
+
expect(offResult).not.toContain("Workspace override marker COMET_3K.");
|
|
1186
|
+
|
|
1187
|
+
const onResult = buildSystemPrompt({ isBackgroundConversation: true });
|
|
1188
|
+
expect(onResult).toContain("## Background Conversation");
|
|
1189
|
+
expect(onResult).toContain("Workspace override marker COMET_3K.");
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
test("renders after the external-content section when both render", () => {
|
|
1193
|
+
// Numeric prefix `08-` > `07-` so the background-conversation
|
|
1194
|
+
// section trails the external-content section in the static block.
|
|
1195
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
1196
|
+
const result = buildSystemPrompt({ isBackgroundConversation: true });
|
|
1197
|
+
const externalIdx = result.indexOf("## External Content");
|
|
1198
|
+
const backgroundIdx = result.indexOf("## Background Conversation");
|
|
1199
|
+
expect(externalIdx).toBeGreaterThan(-1);
|
|
1200
|
+
expect(backgroundIdx).toBeGreaterThan(-1);
|
|
1201
|
+
expect(externalIdx).toBeLessThan(backgroundIdx);
|
|
1202
|
+
});
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
test("unresolved {{variable}} is left as a literal in the body", () => {
|
|
1206
|
+
mkdirSync(SYSTEM_PROMPTS_DIR, { recursive: true });
|
|
1207
|
+
writeFileSync(
|
|
1208
|
+
PREFIX_FILE,
|
|
1209
|
+
PREFIX_FRONTMATTER + "Has {{somethingMissing}} in body.\n",
|
|
1210
|
+
);
|
|
1211
|
+
const result = buildSystemPrompt();
|
|
1212
|
+
expect(result).toContain("Has {{somethingMissing}} in body.");
|
|
1213
|
+
});
|
|
1214
|
+
|
|
554
1215
|
});
|
|
555
1216
|
});
|
|
556
1217
|
|
|
@@ -748,6 +1409,19 @@ describe("ensurePromptFiles", () => {
|
|
|
748
1409
|
expect(content.length).toBeGreaterThan(0);
|
|
749
1410
|
});
|
|
750
1411
|
|
|
1412
|
+
test("does not seed bundled system prompt sections into the workspace", () => {
|
|
1413
|
+
// Bundled `templates/system/*.md` files are the source of default truth.
|
|
1414
|
+
// The renderer reads them directly; the workspace dir is an optional
|
|
1415
|
+
// override layer. On first run we must not pre-populate the workspace
|
|
1416
|
+
// with bundled section copies — leaving the workspace empty keeps the
|
|
1417
|
+
// override layer purely opt-in and lets bundled defaults flow through
|
|
1418
|
+
// automatically as the daemon ships updates.
|
|
1419
|
+
ensurePromptFiles();
|
|
1420
|
+
|
|
1421
|
+
const sectionsDir = join(TEST_DIR, "prompts", "system");
|
|
1422
|
+
expect(existsSync(sectionsDir)).toBe(false);
|
|
1423
|
+
});
|
|
1424
|
+
|
|
751
1425
|
test("does not recreate BOOTSTRAP.md when other prompt files already exist", () => {
|
|
752
1426
|
// Simulate a workspace where onboarding completed: core files exist,
|
|
753
1427
|
// BOOTSTRAP.md was deleted by the user.
|