stagent 0.9.6 → 0.11.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 +20 -44
- package/dist/cli.js +66 -18
- package/docs/.coverage-gaps.json +144 -56
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +12 -2
- package/docs/features/chat.md +40 -5
- package/docs/features/cost-usage.md +1 -1
- package/docs/features/documents.md +5 -2
- package/docs/features/inbox-notifications.md +10 -2
- package/docs/features/keyboard-navigation.md +12 -3
- package/docs/features/provider-runtimes.md +20 -2
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/shared-components.md +7 -3
- package/docs/features/tables.md +11 -2
- package/docs/features/tool-permissions.md +6 -2
- package/docs/features/workflows.md +14 -4
- package/docs/index.md +1 -1
- package/docs/journeys/developer.md +39 -2
- package/docs/journeys/personal-use.md +32 -8
- package/docs/journeys/power-user.md +45 -14
- package/docs/journeys/work-use.md +17 -8
- package/docs/manifest.json +15 -15
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
- package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
- package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
- package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
- package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
- package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
- package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
- package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
- package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
- package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
- package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
- package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
- package/next.config.mjs +1 -0
- package/package.json +3 -2
- package/src/__tests__/instrumentation-smoke.test.ts +15 -0
- package/src/app/analytics/page.tsx +1 -21
- package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
- package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
- package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
- package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
- package/src/app/api/chat/export/route.ts +52 -0
- package/src/app/api/chat/files/search/route.ts +50 -0
- package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
- package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
- package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
- package/src/app/api/environment/skills/route.ts +13 -0
- package/src/app/api/instance/config/route.ts +41 -0
- package/src/app/api/instance/init/route.ts +34 -0
- package/src/app/api/instance/upgrade/check/route.ts +26 -0
- package/src/app/api/instance/upgrade/route.ts +96 -0
- package/src/app/api/instance/upgrade/status/route.ts +35 -0
- package/src/app/api/memory/route.ts +0 -11
- package/src/app/api/notifications/route.ts +4 -2
- package/src/app/api/projects/[id]/route.ts +5 -155
- package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
- package/src/app/api/schedules/[id]/execute/route.ts +111 -0
- package/src/app/api/schedules/[id]/route.ts +9 -1
- package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
- package/src/app/api/schedules/route.ts +3 -12
- package/src/app/api/settings/chat/pins/route.ts +94 -0
- package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
- package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
- package/src/app/api/settings/environment/route.ts +26 -0
- package/src/app/api/settings/openai/login/route.ts +22 -0
- package/src/app/api/settings/openai/logout/route.ts +7 -0
- package/src/app/api/settings/openai/route.ts +21 -1
- package/src/app/api/settings/providers/route.ts +35 -8
- package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
- package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
- package/src/app/api/tables/[id]/enrich/route.ts +147 -0
- package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
- package/src/app/api/tasks/[id]/execute/route.ts +52 -33
- package/src/app/api/tasks/[id]/respond/route.ts +31 -15
- package/src/app/api/tasks/[id]/resume/route.ts +24 -3
- package/src/app/api/workflows/[id]/resume/route.ts +59 -0
- package/src/app/api/workflows/[id]/status/route.ts +22 -8
- package/src/app/api/workspace/context/route.ts +2 -0
- package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
- package/src/app/chat/page.tsx +11 -0
- package/src/app/documents/page.tsx +4 -1
- package/src/app/inbox/page.tsx +12 -5
- package/src/app/layout.tsx +42 -21
- package/src/app/page.tsx +0 -2
- package/src/app/settings/page.tsx +8 -9
- package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +573 -0
- package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
- package/src/components/chat/capability-banner.tsx +68 -0
- package/src/components/chat/chat-command-popover.tsx +670 -49
- package/src/components/chat/chat-input.tsx +104 -10
- package/src/components/chat/chat-message.tsx +12 -3
- package/src/components/chat/chat-session-provider.tsx +790 -0
- package/src/components/chat/chat-shell.tsx +151 -401
- package/src/components/chat/command-tab-bar.tsx +68 -0
- package/src/components/chat/conversation-template-picker.tsx +421 -0
- package/src/components/chat/help-dialog.tsx +39 -0
- package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
- package/src/components/chat/skill-row.tsx +147 -0
- package/src/components/documents/document-browser.tsx +37 -19
- package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
- package/src/components/instance/instance-section.tsx +382 -0
- package/src/components/instance/upgrade-badge.tsx +219 -0
- package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
- package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
- package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
- package/src/components/notifications/batch-proposal-review.tsx +20 -5
- package/src/components/notifications/inbox-list.tsx +11 -2
- package/src/components/notifications/notification-item.tsx +56 -2
- package/src/components/notifications/pending-approval-host.tsx +56 -37
- package/src/components/notifications/permission-response-actions.tsx +155 -1
- package/src/components/schedules/schedule-create-sheet.tsx +19 -1
- package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
- package/src/components/schedules/schedule-form.tsx +31 -0
- package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
- package/src/components/settings/auth-method-selector.tsx +19 -4
- package/src/components/settings/auth-status-badge.tsx +28 -3
- package/src/components/settings/environment-section.tsx +102 -0
- package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
- package/src/components/settings/openai-runtime-section.tsx +7 -1
- package/src/components/settings/providers-runtimes-section.tsx +138 -19
- package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
- package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
- package/src/components/shared/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +266 -7
- package/src/components/shared/filter-hint.tsx +70 -0
- package/src/components/shared/filter-input.tsx +59 -0
- package/src/components/shared/saved-searches-manager.tsx +199 -0
- package/src/components/shared/theme-toggle.tsx +5 -24
- package/src/components/shared/workspace-indicator.tsx +61 -2
- package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
- package/src/components/tables/table-create-sheet.tsx +4 -0
- package/src/components/tables/table-enrichment-runs.tsx +103 -0
- package/src/components/tables/table-enrichment-sheet.tsx +538 -0
- package/src/components/tables/table-spreadsheet.tsx +29 -5
- package/src/components/tables/table-toolbar.tsx +10 -1
- package/src/components/tasks/kanban-board.tsx +1 -0
- package/src/components/tasks/kanban-column.tsx +53 -14
- package/src/components/tasks/task-bento-grid.tsx +31 -2
- package/src/components/tasks/task-card.tsx +29 -3
- package/src/components/tasks/task-chip-bar.tsx +54 -1
- package/src/components/tasks/task-result-renderer.tsx +1 -1
- package/src/components/workflows/delay-step-body.tsx +109 -0
- package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
- package/src/components/workflows/loop-status-view.tsx +1 -1
- package/src/components/workflows/shared/step-result.tsx +78 -0
- package/src/components/workflows/shared/workflow-header.tsx +141 -0
- package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
- package/src/components/workflows/swarm-dashboard.tsx +2 -15
- package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
- package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
- package/src/components/workflows/workflow-form-view.tsx +133 -16
- package/src/components/workflows/workflow-status-view.tsx +30 -740
- package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
- package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
- package/src/hooks/use-active-skills.ts +110 -0
- package/src/hooks/use-chat-autocomplete.ts +120 -7
- package/src/hooks/use-enriched-skills.ts +19 -0
- package/src/hooks/use-pinned-entries.ts +104 -0
- package/src/hooks/use-recent-user-messages.ts +19 -0
- package/src/hooks/use-saved-searches.ts +142 -0
- package/src/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +212 -0
- package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
- package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
- package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
- package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
- package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
- package/src/lib/agents/claude-agent.ts +217 -21
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/handoff/bus.ts +2 -2
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +53 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +97 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +36 -0
- package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
- package/src/lib/agents/profiles/registry.ts +18 -0
- package/src/lib/agents/profiles/types.ts +7 -1
- package/src/lib/agents/router.ts +3 -6
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
- package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
- package/src/lib/agents/runtime/catalog.ts +121 -0
- package/src/lib/agents/runtime/claude-sdk.ts +32 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/execution-target.ts +456 -0
- package/src/lib/agents/runtime/index.ts +4 -0
- package/src/lib/agents/runtime/launch-failure.ts +101 -0
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +64 -60
- package/src/lib/agents/runtime/openai-direct.ts +8 -0
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/agents/task-dispatch.ts +220 -0
- package/src/lib/agents/tool-permissions.ts +16 -1
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
- package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
- package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
- package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
- package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
- package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
- package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
- package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
- package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
- package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/__tests__/types.test.ts +28 -0
- package/src/lib/chat/active-skills.ts +31 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/clean-filter-input.ts +30 -0
- package/src/lib/chat/codex-engine.ts +46 -24
- package/src/lib/chat/command-tabs.ts +61 -0
- package/src/lib/chat/context-builder.ts +146 -4
- package/src/lib/chat/dismissals.ts +73 -0
- package/src/lib/chat/engine.ts +159 -18
- package/src/lib/chat/files/__tests__/search.test.ts +135 -0
- package/src/lib/chat/files/expand-mention.ts +76 -0
- package/src/lib/chat/files/search.ts +99 -0
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/skill-composition.ts +210 -0
- package/src/lib/chat/skill-conflict.ts +105 -0
- package/src/lib/chat/stagent-tools.ts +7 -19
- package/src/lib/chat/stream-telemetry.ts +137 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +48 -1
- package/src/lib/chat/tool-catalog.ts +35 -4
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
- package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +399 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +351 -0
- package/src/lib/chat/tools/blueprint-tools.ts +190 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +41 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- package/src/lib/chat/tools/profile-tools.ts +120 -23
- package/src/lib/chat/tools/project-tools.ts +33 -0
- package/src/lib/chat/tools/schedule-tools.ts +44 -11
- package/src/lib/chat/tools/skill-tools.ts +183 -0
- package/src/lib/chat/tools/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +89 -21
- package/src/lib/chat/tools/workflow-tools.ts +275 -32
- package/src/lib/chat/types.ts +15 -0
- package/src/lib/constants/settings.ts +10 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +17 -16
- package/src/lib/data/delete-project.ts +171 -0
- package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
- package/src/lib/db/bootstrap.ts +62 -16
- package/src/lib/db/index.ts +5 -0
- package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
- package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
- package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
- package/src/lib/db/migrations/0026_drop_license.sql +3 -0
- package/src/lib/db/migrations/meta/_journal.json +21 -0
- package/src/lib/db/schema.ts +94 -23
- package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
- package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
- package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
- package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
- package/src/lib/environment/data.ts +9 -0
- package/src/lib/environment/list-skills.ts +176 -0
- package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
- package/src/lib/environment/parsers/skill.ts +26 -5
- package/src/lib/environment/profile-generator.ts +54 -0
- package/src/lib/environment/skill-enrichment.ts +106 -0
- package/src/lib/environment/skill-recommendations.ts +66 -0
- package/src/lib/environment/workspace-context.ts +13 -1
- package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
- package/src/lib/filters/__tests__/parse.test.ts +135 -0
- package/src/lib/filters/parse.ts +86 -0
- package/src/lib/import/dedup.ts +4 -54
- package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
- package/src/lib/instance/__tests__/detect.test.ts +115 -0
- package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
- package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
- package/src/lib/instance/__tests__/settings.test.ts +83 -0
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +181 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +76 -0
- package/src/lib/instance/git-ops.ts +95 -0
- package/src/lib/instance/settings.ts +61 -0
- package/src/lib/instance/types.ts +77 -0
- package/src/lib/instance/upgrade-poller.ts +205 -0
- package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
- package/src/lib/notifications/visibility.ts +33 -0
- package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
- package/src/lib/schedules/__tests__/config.test.ts +62 -0
- package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
- package/src/lib/schedules/__tests__/integration.test.ts +82 -0
- package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
- package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
- package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
- package/src/lib/schedules/collision-check.ts +105 -0
- package/src/lib/schedules/config.ts +53 -0
- package/src/lib/schedules/scheduler.ts +236 -17
- package/src/lib/schedules/slot-claim.ts +105 -0
- package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
- package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
- package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
- package/src/lib/settings/openai-auth.ts +105 -10
- package/src/lib/settings/openai-login-manager.ts +260 -0
- package/src/lib/settings/runtime-setup.ts +14 -4
- package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
- package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
- package/src/lib/tables/enrichment-planner.ts +454 -0
- package/src/lib/tables/enrichment.ts +328 -0
- package/src/lib/tables/query-builder.ts +5 -2
- package/src/lib/tables/trigger-evaluator.ts +3 -2
- package/src/lib/theme.ts +71 -0
- package/src/lib/usage/ledger.ts +2 -18
- package/src/lib/util/__tests__/similarity.test.ts +106 -0
- package/src/lib/util/similarity.ts +77 -0
- package/src/lib/utils/format-timestamp.ts +24 -0
- package/src/lib/utils/stagent-paths.ts +12 -0
- package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
- package/src/lib/validators/__tests__/settings.test.ts +10 -0
- package/src/lib/validators/blueprint.ts +70 -9
- package/src/lib/validators/profile.ts +2 -2
- package/src/lib/validators/settings.ts +3 -1
- package/src/lib/workflows/__tests__/delay.test.ts +196 -0
- package/src/lib/workflows/__tests__/engine.test.ts +8 -0
- package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
- package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
- package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
- package/src/lib/workflows/blueprints/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
- package/src/lib/workflows/blueprints/types.ts +16 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +212 -7
- package/src/lib/workflows/loop-executor.ts +349 -24
- package/src/lib/workflows/post-action.ts +91 -0
- package/src/lib/workflows/types.ts +166 -1
- package/src/test/setup.ts +10 -0
- package/src/app/api/license/checkout/route.ts +0 -28
- package/src/app/api/license/portal/route.ts +0 -26
- package/src/app/api/license/route.ts +0 -89
- package/src/app/api/license/usage/route.ts +0 -63
- package/src/app/api/marketplace/browse/route.ts +0 -15
- package/src/app/api/marketplace/import/route.ts +0 -28
- package/src/app/api/marketplace/publish/route.ts +0 -40
- package/src/app/api/onboarding/email/route.ts +0 -53
- package/src/app/api/settings/telemetry/route.ts +0 -14
- package/src/app/api/sync/export/route.ts +0 -54
- package/src/app/api/sync/restore/route.ts +0 -37
- package/src/app/api/sync/sessions/route.ts +0 -24
- package/src/app/auth/callback/route.ts +0 -73
- package/src/app/marketplace/page.tsx +0 -19
- package/src/components/analytics/analytics-gate-card.tsx +0 -101
- package/src/components/marketplace/blueprint-card.tsx +0 -61
- package/src/components/marketplace/marketplace-browser.tsx +0 -131
- package/src/components/onboarding/email-capture-card.tsx +0 -104
- package/src/components/settings/activation-form.tsx +0 -95
- package/src/components/settings/cloud-account-section.tsx +0 -147
- package/src/components/settings/cloud-sync-section.tsx +0 -155
- package/src/components/settings/subscription-section.tsx +0 -410
- package/src/components/settings/telemetry-section.tsx +0 -80
- package/src/components/shared/premium-gate-overlay.tsx +0 -50
- package/src/components/shared/schedule-gate-dialog.tsx +0 -64
- package/src/components/shared/upgrade-banner.tsx +0 -112
- package/src/hooks/use-supabase-auth.ts +0 -79
- package/src/lib/billing/email.ts +0 -54
- package/src/lib/billing/products.ts +0 -80
- package/src/lib/billing/stripe.ts +0 -101
- package/src/lib/cloud/supabase-browser.ts +0 -32
- package/src/lib/cloud/supabase-client.ts +0 -56
- package/src/lib/license/__tests__/features.test.ts +0 -56
- package/src/lib/license/__tests__/key-format.test.ts +0 -88
- package/src/lib/license/__tests__/manager.test.ts +0 -64
- package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
- package/src/lib/license/cloud-validation.ts +0 -60
- package/src/lib/license/features.ts +0 -44
- package/src/lib/license/key-format.ts +0 -101
- package/src/lib/license/limit-check.ts +0 -111
- package/src/lib/license/limit-queries.ts +0 -51
- package/src/lib/license/manager.ts +0 -345
- package/src/lib/license/notifications.ts +0 -59
- package/src/lib/license/tier-limits.ts +0 -71
- package/src/lib/marketplace/marketplace-client.ts +0 -107
- package/src/lib/sync/cloud-sync.ts +0 -235
- package/src/lib/telemetry/conversion-events.ts +0 -71
- package/src/lib/telemetry/queue.ts +0 -122
- package/src/lib/validators/license.ts +0 -33
package/src/lib/db/schema.ts
CHANGED
|
@@ -29,6 +29,12 @@ export const tasks = sqliteTable(
|
|
|
29
29
|
.notNull(),
|
|
30
30
|
assignedAgent: text("assigned_agent"),
|
|
31
31
|
agentProfile: text("agent_profile"),
|
|
32
|
+
/** Runtime actually used for the most recent execution attempt. */
|
|
33
|
+
effectiveRuntimeId: text("effective_runtime_id"),
|
|
34
|
+
/** Model actually used for the most recent execution attempt. */
|
|
35
|
+
effectiveModelId: text("effective_model_id"),
|
|
36
|
+
/** Human-readable reason when execution fell back from the requested runtime/model. */
|
|
37
|
+
runtimeFallbackReason: text("runtime_fallback_reason"),
|
|
32
38
|
priority: integer("priority").default(2).notNull(),
|
|
33
39
|
result: text("result"),
|
|
34
40
|
sessionId: text("session_id"),
|
|
@@ -40,6 +46,21 @@ export const tasks = sqliteTable(
|
|
|
40
46
|
workflowRunNumber: integer("workflow_run_number"),
|
|
41
47
|
/** Resolved per-task budget cap in USD — set by workflow engine for child tasks */
|
|
42
48
|
maxBudgetUsd: real("max_budget_usd"),
|
|
49
|
+
/** When the slot for this task was atomically claimed */
|
|
50
|
+
slotClaimedAt: integer("slot_claimed_at", { mode: "timestamp" }),
|
|
51
|
+
/** Wall-clock expiry; reaper aborts tasks whose lease has passed */
|
|
52
|
+
leaseExpiresAt: integer("lease_expires_at", { mode: "timestamp" }),
|
|
53
|
+
/**
|
|
54
|
+
* Explicit terminal-state reason written by the runtime adapter at
|
|
55
|
+
* failure/abort transitions (e.g. 'turn_limit_exceeded', 'lease_expired',
|
|
56
|
+
* 'aborted', 'sdk_error'). Distinct from `result` — `result` holds the
|
|
57
|
+
* agent's final output text, while `failureReason` holds a machine-readable
|
|
58
|
+
* classifier that drives scheduler failure-streak logic without re-parsing
|
|
59
|
+
* error prose.
|
|
60
|
+
*/
|
|
61
|
+
failureReason: text("failure_reason"),
|
|
62
|
+
/** Per-task turn budget copied from schedules.maxTurns at firing time */
|
|
63
|
+
maxTurns: integer("max_turns"),
|
|
43
64
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
44
65
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
45
66
|
},
|
|
@@ -49,6 +70,7 @@ export const tasks = sqliteTable(
|
|
|
49
70
|
index("idx_tasks_workflow_id").on(table.workflowId),
|
|
50
71
|
index("idx_tasks_schedule_id").on(table.scheduleId),
|
|
51
72
|
index("idx_tasks_agent_profile").on(table.agentProfile),
|
|
73
|
+
index("idx_tasks_running_scheduled").on(table.status, table.sourceType, table.leaseExpiresAt),
|
|
52
74
|
]
|
|
53
75
|
);
|
|
54
76
|
|
|
@@ -65,6 +87,13 @@ export const workflows = sqliteTable("workflows", {
|
|
|
65
87
|
runNumber: integer("run_number").default(0).notNull(),
|
|
66
88
|
/** Runtime to use for all steps (nullable — falls back to system default) */
|
|
67
89
|
runtimeId: text("runtime_id"),
|
|
90
|
+
/**
|
|
91
|
+
* Epoch millisecond timestamp at which a paused (delayed) workflow is due to resume.
|
|
92
|
+
* Null for workflows that are not waiting on a delay step. Indexed via
|
|
93
|
+
* idx_workflows_resume_at (partial index on non-null values) so the scheduler tick
|
|
94
|
+
* can efficiently find due workflows. See features/workflow-step-delays.md.
|
|
95
|
+
*/
|
|
96
|
+
resumeAt: integer("resume_at"),
|
|
68
97
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
69
98
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
70
99
|
});
|
|
@@ -210,6 +239,20 @@ export const schedules = sqliteTable(
|
|
|
210
239
|
failureStreak: integer("failure_streak").default(0).notNull(),
|
|
211
240
|
/** Detected reason for the most recent failure (turn_limit_exceeded, timeout, etc.) */
|
|
212
241
|
lastFailureReason: text("last_failure_reason"),
|
|
242
|
+
/** Hard cap on turns per firing; NULL inherits the global MAX_TURNS setting */
|
|
243
|
+
maxTurns: integer("max_turns"),
|
|
244
|
+
/** Timestamp when maxTurns was last edited — drives first-breach grace */
|
|
245
|
+
maxTurnsSetAt: integer("max_turns_set_at", { mode: "timestamp" }),
|
|
246
|
+
/** Wall-clock lease override in seconds; NULL inherits global default (1200s) */
|
|
247
|
+
maxRunDurationSec: integer("max_run_duration_sec"),
|
|
248
|
+
/**
|
|
249
|
+
* Counter separate from failureStreak — increments only on maxTurns breach.
|
|
250
|
+
* Reset to 0 on any non-breach outcome (successful run, generic failure, or
|
|
251
|
+
* first-breach grace window after maxTurnsSetAt). Auto-pause at 5. This
|
|
252
|
+
* higher threshold + grace window protects users from tripping auto-pause
|
|
253
|
+
* via a misconfigured maxTurns edit.
|
|
254
|
+
*/
|
|
255
|
+
turnBudgetBreachStreak: integer("turn_budget_breach_streak").default(0).notNull(),
|
|
213
256
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
214
257
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
215
258
|
},
|
|
@@ -307,6 +350,7 @@ export const usageLedger = sqliteTable(
|
|
|
307
350
|
"context_summarization",
|
|
308
351
|
"chat_turn",
|
|
309
352
|
"profile_assist",
|
|
353
|
+
"manual_force_bypass",
|
|
310
354
|
],
|
|
311
355
|
}).notNull(),
|
|
312
356
|
runtimeId: text("runtime_id").notNull(),
|
|
@@ -504,6 +548,26 @@ export const conversations = sqliteTable(
|
|
|
504
548
|
.notNull(),
|
|
505
549
|
sessionId: text("session_id"),
|
|
506
550
|
contextScope: text("context_scope"), // JSON: context config overrides
|
|
551
|
+
/**
|
|
552
|
+
* Opaque skill ID of the Stagent-activated skill for this conversation.
|
|
553
|
+
* When set, the context builder injects that skill's SKILL.md into the
|
|
554
|
+
* Tier 0 system prompt every turn. Primary use case is Ollama (no
|
|
555
|
+
* SDK-native skill support); Claude and Codex can also use it as a
|
|
556
|
+
* programmatic skill-activation path alongside their native Skill tools.
|
|
557
|
+
*
|
|
558
|
+
* See `features/chat-ollama-native-skills.md`.
|
|
559
|
+
*/
|
|
560
|
+
activeSkillId: text("active_skill_id"),
|
|
561
|
+
/**
|
|
562
|
+
* Composition v1 — array of additionally-activated skill IDs (beyond
|
|
563
|
+
* the legacy `activeSkillId`). Default `[]`. Read paths merge legacy
|
|
564
|
+
* + new and dedupe via `mergeActiveSkillIds`. Stored as JSON text.
|
|
565
|
+
*
|
|
566
|
+
* See `features/chat-skill-composition.md`.
|
|
567
|
+
*/
|
|
568
|
+
activeSkillIds: text("active_skill_ids", { mode: "json" })
|
|
569
|
+
.$type<string[]>()
|
|
570
|
+
.default([] as unknown as string[]),
|
|
507
571
|
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
508
572
|
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
509
573
|
},
|
|
@@ -1162,29 +1226,6 @@ export const snapshots = sqliteTable(
|
|
|
1162
1226
|
|
|
1163
1227
|
export type SnapshotRow = InferSelectModel<typeof snapshots>;
|
|
1164
1228
|
|
|
1165
|
-
// ── License ──────────────────────────────────────────────────────────
|
|
1166
|
-
|
|
1167
|
-
export const license = sqliteTable("license", {
|
|
1168
|
-
id: text("id").primaryKey(),
|
|
1169
|
-
supabaseUserId: text("supabase_user_id"),
|
|
1170
|
-
tier: text("tier", { enum: ["community", "solo", "operator", "scale"] })
|
|
1171
|
-
.default("community")
|
|
1172
|
-
.notNull(),
|
|
1173
|
-
status: text("status", { enum: ["active", "inactive", "grace"] })
|
|
1174
|
-
.default("inactive")
|
|
1175
|
-
.notNull(),
|
|
1176
|
-
email: text("email"),
|
|
1177
|
-
activatedAt: integer("activated_at", { mode: "timestamp" }),
|
|
1178
|
-
expiresAt: integer("expires_at", { mode: "timestamp" }),
|
|
1179
|
-
lastValidatedAt: integer("last_validated_at", { mode: "timestamp" }),
|
|
1180
|
-
gracePeriodExpiresAt: integer("grace_period_expires_at", { mode: "timestamp" }),
|
|
1181
|
-
encryptedToken: text("encrypted_token"),
|
|
1182
|
-
createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
|
|
1183
|
-
updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
|
|
1184
|
-
});
|
|
1185
|
-
|
|
1186
|
-
export type LicenseRow = InferSelectModel<typeof license>;
|
|
1187
|
-
|
|
1188
1229
|
// ── Workflow Execution Stats ─────────────────────────────────────────
|
|
1189
1230
|
|
|
1190
1231
|
export const workflowExecutionStats = sqliteTable("workflow_execution_stats", {
|
|
@@ -1212,3 +1253,33 @@ export const workflowExecutionStats = sqliteTable("workflow_execution_stats", {
|
|
|
1212
1253
|
});
|
|
1213
1254
|
|
|
1214
1255
|
export type WorkflowExecutionStatsRow = InferSelectModel<typeof workflowExecutionStats>;
|
|
1256
|
+
|
|
1257
|
+
// ── Schedule Firing Metrics ───────────────────────────────────────────
|
|
1258
|
+
|
|
1259
|
+
export const scheduleFiringMetrics = sqliteTable(
|
|
1260
|
+
"schedule_firing_metrics",
|
|
1261
|
+
{
|
|
1262
|
+
id: text("id").primaryKey(),
|
|
1263
|
+
scheduleId: text("schedule_id")
|
|
1264
|
+
.references(() => schedules.id)
|
|
1265
|
+
.notNull(),
|
|
1266
|
+
taskId: text("task_id").references(() => tasks.id),
|
|
1267
|
+
firedAt: integer("fired_at", { mode: "timestamp" }).notNull(),
|
|
1268
|
+
slotClaimedAt: integer("slot_claimed_at", { mode: "timestamp" }),
|
|
1269
|
+
completedAt: integer("completed_at", { mode: "timestamp" }),
|
|
1270
|
+
slotWaitMs: integer("slot_wait_ms"),
|
|
1271
|
+
durationMs: integer("duration_ms"),
|
|
1272
|
+
turnCount: integer("turn_count"),
|
|
1273
|
+
maxTurnsAtFiring: integer("max_turns_at_firing"),
|
|
1274
|
+
eventLoopLagMs: real("event_loop_lag_ms"),
|
|
1275
|
+
peakRssMb: integer("peak_rss_mb"),
|
|
1276
|
+
chatStreamsActive: integer("chat_streams_active"),
|
|
1277
|
+
concurrentSchedules: integer("concurrent_schedules"),
|
|
1278
|
+
failureReason: text("failure_reason"),
|
|
1279
|
+
},
|
|
1280
|
+
(table) => [
|
|
1281
|
+
index("idx_sfm_schedule_time").on(table.scheduleId, table.firedAt),
|
|
1282
|
+
]
|
|
1283
|
+
);
|
|
1284
|
+
|
|
1285
|
+
export type ScheduleFiringMetricRow = InferSelectModel<typeof scheduleFiringMetrics>;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("@/lib/settings/helpers", () => ({
|
|
4
|
+
getSettingSync: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("../profile-linker", () => ({
|
|
8
|
+
linkArtifactsToProfiles: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
// The module under test depends on suggestProfilesTiered + createProfileFromSuggestion,
|
|
12
|
+
// which live in the same file. We test via the real function but stub its
|
|
13
|
+
// collaborators (getArtifacts + listProfiles) through their imported modules.
|
|
14
|
+
vi.mock("../data", () => ({
|
|
15
|
+
getArtifacts: vi.fn(() => []),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock("@/lib/agents/profiles/registry", () => ({
|
|
19
|
+
listProfiles: vi.fn(() => []),
|
|
20
|
+
createProfile: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
import { autoPromoteUnlinkedSkills } from "../profile-generator";
|
|
24
|
+
import { getSettingSync } from "@/lib/settings/helpers";
|
|
25
|
+
import { linkArtifactsToProfiles } from "../profile-linker";
|
|
26
|
+
import { getArtifacts } from "../data";
|
|
27
|
+
import { createProfile } from "@/lib/agents/profiles/registry";
|
|
28
|
+
|
|
29
|
+
const mockGetSettingSync = getSettingSync as ReturnType<typeof vi.fn>;
|
|
30
|
+
const mockLinker = linkArtifactsToProfiles as ReturnType<typeof vi.fn>;
|
|
31
|
+
const mockGetArtifacts = getArtifacts as ReturnType<typeof vi.fn>;
|
|
32
|
+
const mockCreateProfile = createProfile as ReturnType<typeof vi.fn>;
|
|
33
|
+
|
|
34
|
+
function unlinkedSkill(name: string) {
|
|
35
|
+
return {
|
|
36
|
+
id: `art-${name}`,
|
|
37
|
+
scanId: "scan-1",
|
|
38
|
+
tool: "claude-code",
|
|
39
|
+
category: "skill",
|
|
40
|
+
scope: "user",
|
|
41
|
+
name,
|
|
42
|
+
relPath: `${name}/SKILL.md`,
|
|
43
|
+
absPath: `/home/u/.claude/skills/${name}/SKILL.md`,
|
|
44
|
+
contentHash: "abc",
|
|
45
|
+
preview: `---\nname: ${name}\ndescription: A ${name} skill\n---\n`,
|
|
46
|
+
metadata: null,
|
|
47
|
+
sizeBytes: 100,
|
|
48
|
+
modifiedAt: new Date(),
|
|
49
|
+
createdAt: new Date(),
|
|
50
|
+
linkedProfileId: null,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe("autoPromoteUnlinkedSkills", () => {
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
vi.clearAllMocks();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns empty result when setting is disabled", () => {
|
|
60
|
+
mockGetSettingSync.mockReturnValue("false");
|
|
61
|
+
mockGetArtifacts.mockReturnValue([unlinkedSkill("alpha")]);
|
|
62
|
+
|
|
63
|
+
const result = autoPromoteUnlinkedSkills("scan-1");
|
|
64
|
+
|
|
65
|
+
expect(result.created).toEqual([]);
|
|
66
|
+
expect(mockCreateProfile).not.toHaveBeenCalled();
|
|
67
|
+
expect(mockLinker).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("returns empty result when setting is missing (default off)", () => {
|
|
71
|
+
mockGetSettingSync.mockReturnValue(null);
|
|
72
|
+
mockGetArtifacts.mockReturnValue([unlinkedSkill("alpha")]);
|
|
73
|
+
|
|
74
|
+
const result = autoPromoteUnlinkedSkills("scan-1");
|
|
75
|
+
|
|
76
|
+
expect(result.created).toEqual([]);
|
|
77
|
+
expect(mockCreateProfile).not.toHaveBeenCalled();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("creates profiles for every Tier 2 suggestion and re-links when enabled", () => {
|
|
81
|
+
mockGetSettingSync.mockReturnValue("true");
|
|
82
|
+
mockGetArtifacts.mockReturnValue([
|
|
83
|
+
unlinkedSkill("alpha"),
|
|
84
|
+
unlinkedSkill("beta"),
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
const result = autoPromoteUnlinkedSkills("scan-1");
|
|
88
|
+
|
|
89
|
+
expect(mockCreateProfile).toHaveBeenCalledTimes(2);
|
|
90
|
+
expect(result.created).toHaveLength(2);
|
|
91
|
+
expect(result.errors).toHaveLength(0);
|
|
92
|
+
expect(mockLinker).toHaveBeenCalledWith("scan-1");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("counts 'already exists' failures as skipped, not errors", () => {
|
|
96
|
+
mockGetSettingSync.mockReturnValue("true");
|
|
97
|
+
mockGetArtifacts.mockReturnValue([unlinkedSkill("gamma")]);
|
|
98
|
+
mockCreateProfile.mockImplementationOnce(() => {
|
|
99
|
+
throw new Error("profile already exists");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const result = autoPromoteUnlinkedSkills("scan-1");
|
|
103
|
+
|
|
104
|
+
expect(result.created).toEqual([]);
|
|
105
|
+
expect(result.skipped).toHaveLength(1);
|
|
106
|
+
expect(result.errors).toEqual([]);
|
|
107
|
+
// No re-link when nothing was created
|
|
108
|
+
expect(mockLinker).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("records non-duplicate errors and still re-links when some profiles succeeded", () => {
|
|
112
|
+
mockGetSettingSync.mockReturnValue("true");
|
|
113
|
+
mockGetArtifacts.mockReturnValue([
|
|
114
|
+
unlinkedSkill("delta"),
|
|
115
|
+
unlinkedSkill("epsilon"),
|
|
116
|
+
]);
|
|
117
|
+
mockCreateProfile
|
|
118
|
+
.mockImplementationOnce(() => {
|
|
119
|
+
/* succeeds */
|
|
120
|
+
})
|
|
121
|
+
.mockImplementationOnce(() => {
|
|
122
|
+
throw new Error("disk full");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const result = autoPromoteUnlinkedSkills("scan-1");
|
|
126
|
+
|
|
127
|
+
expect(result.created).toHaveLength(1);
|
|
128
|
+
expect(result.errors).toHaveLength(1);
|
|
129
|
+
expect(result.errors[0].message).toBe("disk full");
|
|
130
|
+
expect(mockLinker).toHaveBeenCalledWith("scan-1");
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("../data", () => ({
|
|
4
|
+
getLatestScan: () => ({ id: "scan-1" }),
|
|
5
|
+
getArtifacts: () => [
|
|
6
|
+
{
|
|
7
|
+
id: "art-1",
|
|
8
|
+
scanId: "scan-1",
|
|
9
|
+
category: "skill",
|
|
10
|
+
tool: "claude-code",
|
|
11
|
+
scope: "user",
|
|
12
|
+
name: "code-reviewer",
|
|
13
|
+
relPath: ".claude/skills/code-reviewer",
|
|
14
|
+
absPath: "/u/.claude/skills/code-reviewer",
|
|
15
|
+
preview: "Review PRs",
|
|
16
|
+
sizeBytes: 100,
|
|
17
|
+
modifiedAt: new Date("2026-01-01T00:00:00Z").getTime(),
|
|
18
|
+
linkedProfileId: "code-reviewer-profile",
|
|
19
|
+
contentHash: "h",
|
|
20
|
+
metadata: null,
|
|
21
|
+
createdAt: new Date(),
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: "art-2",
|
|
25
|
+
scanId: "scan-1",
|
|
26
|
+
category: "skill",
|
|
27
|
+
tool: "codex",
|
|
28
|
+
scope: "user",
|
|
29
|
+
name: "code-reviewer",
|
|
30
|
+
relPath: ".agents/skills/code-reviewer",
|
|
31
|
+
absPath: "/u/.agents/skills/code-reviewer",
|
|
32
|
+
preview: "Review PRs",
|
|
33
|
+
sizeBytes: 100,
|
|
34
|
+
modifiedAt: new Date("2026-01-01T00:00:00Z").getTime(),
|
|
35
|
+
linkedProfileId: null,
|
|
36
|
+
contentHash: "h",
|
|
37
|
+
metadata: null,
|
|
38
|
+
createdAt: new Date(),
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
import { listSkillsEnriched } from "../list-skills";
|
|
44
|
+
|
|
45
|
+
describe("listSkillsEnriched", () => {
|
|
46
|
+
it("returns enriched skills with syncStatus and linkedProfileId populated", () => {
|
|
47
|
+
const nowMs = new Date("2026-04-14T00:00:00Z").getTime();
|
|
48
|
+
const enriched = listSkillsEnriched({ nowMs });
|
|
49
|
+
expect(enriched).toHaveLength(1);
|
|
50
|
+
expect(enriched[0].name).toBe("code-reviewer");
|
|
51
|
+
expect(enriched[0].syncStatus).toBe("synced");
|
|
52
|
+
expect(enriched[0].linkedProfileId).toBe("code-reviewer-profile");
|
|
53
|
+
expect(enriched[0].healthScore).toBe("healthy");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
computeHealthScore,
|
|
4
|
+
computeSyncStatus,
|
|
5
|
+
type HealthScore,
|
|
6
|
+
type SyncStatus,
|
|
7
|
+
} from "../skill-enrichment";
|
|
8
|
+
|
|
9
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
describe("computeHealthScore", () => {
|
|
12
|
+
const NOW = new Date("2026-04-14T00:00:00Z").getTime();
|
|
13
|
+
|
|
14
|
+
it("returns 'healthy' for artifacts modified in the last 6 months", () => {
|
|
15
|
+
expect(computeHealthScore(NOW - 30 * MS_PER_DAY, NOW)).toBe("healthy");
|
|
16
|
+
expect(computeHealthScore(NOW - 179 * MS_PER_DAY, NOW)).toBe("healthy");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("returns 'stale' for artifacts between 6 and 12 months old", () => {
|
|
20
|
+
expect(computeHealthScore(NOW - 200 * MS_PER_DAY, NOW)).toBe("stale");
|
|
21
|
+
expect(computeHealthScore(NOW - 364 * MS_PER_DAY, NOW)).toBe("stale");
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("returns 'aging' for artifacts over 12 months old", () => {
|
|
25
|
+
expect(computeHealthScore(NOW - 400 * MS_PER_DAY, NOW)).toBe("aging");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns 'unknown' when modifiedAt is null", () => {
|
|
29
|
+
expect(computeHealthScore(null, NOW)).toBe("unknown");
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("computeSyncStatus", () => {
|
|
34
|
+
it("returns 'synced' when both tools have the skill", () => {
|
|
35
|
+
expect(computeSyncStatus(["claude-code", "codex"])).toBe("synced");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns 'claude-only' when only claude-code has it", () => {
|
|
39
|
+
expect(computeSyncStatus(["claude-code"])).toBe("claude-only");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("returns 'codex-only' when only codex has it", () => {
|
|
43
|
+
expect(computeSyncStatus(["codex"])).toBe("codex-only");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("returns 'shared' when only shared tool is present", () => {
|
|
47
|
+
expect(computeSyncStatus(["shared"])).toBe("shared");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns 'synced' when claude + shared", () => {
|
|
51
|
+
expect(computeSyncStatus(["claude-code", "shared"])).toBe("synced");
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
import { enrichSkills, type EnrichedSkill } from "../skill-enrichment";
|
|
56
|
+
import type { SkillSummary } from "../list-skills";
|
|
57
|
+
|
|
58
|
+
const NOW = new Date("2026-04-14T00:00:00Z").getTime();
|
|
59
|
+
const DAY = 24 * 60 * 60 * 1000;
|
|
60
|
+
|
|
61
|
+
function skill(
|
|
62
|
+
id: string,
|
|
63
|
+
name: string,
|
|
64
|
+
tool: string,
|
|
65
|
+
overrides: Partial<SkillSummary> = {}
|
|
66
|
+
): SkillSummary {
|
|
67
|
+
return {
|
|
68
|
+
id,
|
|
69
|
+
name,
|
|
70
|
+
tool,
|
|
71
|
+
scope: "user",
|
|
72
|
+
preview: "",
|
|
73
|
+
sizeBytes: 0,
|
|
74
|
+
absPath: `/tmp/${id}`,
|
|
75
|
+
...overrides,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
describe("enrichSkills", () => {
|
|
80
|
+
it("groups by name and computes syncStatus across tools", () => {
|
|
81
|
+
const out = enrichSkills(
|
|
82
|
+
[
|
|
83
|
+
skill("a", "research", "claude-code"),
|
|
84
|
+
skill("b", "research", "codex"),
|
|
85
|
+
skill("c", "standalone", "claude-code"),
|
|
86
|
+
],
|
|
87
|
+
{ modifiedAtMsByPath: {}, linkedProfilesByPath: {}, nowMs: NOW }
|
|
88
|
+
);
|
|
89
|
+
const bySkill: Record<string, EnrichedSkill> = {};
|
|
90
|
+
for (const s of out) bySkill[s.name] = s;
|
|
91
|
+
expect(bySkill.research.syncStatus).toBe("synced");
|
|
92
|
+
expect(bySkill.standalone.syncStatus).toBe("claude-only");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("attaches linkedProfileId per artifact absPath", () => {
|
|
96
|
+
const out = enrichSkills(
|
|
97
|
+
[skill("x", "coder", "claude-code", { absPath: "/p/a" })],
|
|
98
|
+
{
|
|
99
|
+
modifiedAtMsByPath: {},
|
|
100
|
+
linkedProfilesByPath: { "/p/a": "code-reviewer" },
|
|
101
|
+
nowMs: NOW,
|
|
102
|
+
}
|
|
103
|
+
);
|
|
104
|
+
expect(out[0].linkedProfileId).toBe("code-reviewer");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("assigns health from modifiedAtMsByPath", () => {
|
|
108
|
+
const out = enrichSkills(
|
|
109
|
+
[skill("x", "aging", "claude-code", { absPath: "/p/a" })],
|
|
110
|
+
{
|
|
111
|
+
modifiedAtMsByPath: { "/p/a": NOW - 400 * DAY },
|
|
112
|
+
linkedProfilesByPath: {},
|
|
113
|
+
nowMs: NOW,
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
expect(out[0].healthScore).toBe("aging");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("merges duplicate absPaths (symlink case) to a single entry", () => {
|
|
120
|
+
const out = enrichSkills(
|
|
121
|
+
[
|
|
122
|
+
skill("a", "shared", "claude-code", { absPath: "/same" }),
|
|
123
|
+
skill("b", "shared", "codex", { absPath: "/same" }),
|
|
124
|
+
],
|
|
125
|
+
{ modifiedAtMsByPath: {}, linkedProfilesByPath: {}, nowMs: NOW }
|
|
126
|
+
);
|
|
127
|
+
expect(out).toHaveLength(1);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { computeRecommendation } from "../skill-recommendations";
|
|
3
|
+
import type { EnrichedSkill } from "../skill-enrichment";
|
|
4
|
+
|
|
5
|
+
const mkSkill = (
|
|
6
|
+
name: string,
|
|
7
|
+
preview: string,
|
|
8
|
+
overrides: Partial<EnrichedSkill> = {}
|
|
9
|
+
): EnrichedSkill => ({
|
|
10
|
+
id: name,
|
|
11
|
+
name,
|
|
12
|
+
tool: "claude-code",
|
|
13
|
+
scope: "user",
|
|
14
|
+
preview,
|
|
15
|
+
sizeBytes: 0,
|
|
16
|
+
absPath: `/p/${name}`,
|
|
17
|
+
healthScore: "healthy",
|
|
18
|
+
syncStatus: "claude-only",
|
|
19
|
+
linkedProfileId: null,
|
|
20
|
+
absPaths: [`/p/${name}`],
|
|
21
|
+
...overrides,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("computeRecommendation", () => {
|
|
25
|
+
it("recommends a healthy skill whose keywords match 2+ in recent messages", () => {
|
|
26
|
+
const skills = [
|
|
27
|
+
mkSkill("code-reviewer", "Review pull requests for security"),
|
|
28
|
+
mkSkill("researcher", "Search the web for up-to-date information"),
|
|
29
|
+
];
|
|
30
|
+
const rec = computeRecommendation(skills, [
|
|
31
|
+
"can you review this pull request for security issues?",
|
|
32
|
+
]);
|
|
33
|
+
expect(rec?.name).toBe("code-reviewer");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns null when no strong keyword match exists", () => {
|
|
37
|
+
const skills = [mkSkill("code-reviewer", "Review PRs for security")];
|
|
38
|
+
const rec = computeRecommendation(skills, ["hi there"]);
|
|
39
|
+
expect(rec).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("excludes already-active skill", () => {
|
|
43
|
+
const skills = [mkSkill("code-reviewer", "Review pull requests security")];
|
|
44
|
+
const rec = computeRecommendation(
|
|
45
|
+
skills,
|
|
46
|
+
["review this pull request for security"],
|
|
47
|
+
{ activeSkillId: "code-reviewer" }
|
|
48
|
+
);
|
|
49
|
+
expect(rec).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("excludes dismissed skills", () => {
|
|
53
|
+
const skills = [mkSkill("code-reviewer", "Review pull requests security")];
|
|
54
|
+
const rec = computeRecommendation(
|
|
55
|
+
skills,
|
|
56
|
+
["review pull request security issues"],
|
|
57
|
+
{ dismissedIds: new Set(["code-reviewer"]) }
|
|
58
|
+
);
|
|
59
|
+
expect(rec).toBeNull();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("excludes broken/aging skills", () => {
|
|
63
|
+
const skills = [
|
|
64
|
+
mkSkill("code-reviewer", "Review pull requests security", {
|
|
65
|
+
healthScore: "aging",
|
|
66
|
+
}),
|
|
67
|
+
];
|
|
68
|
+
const rec = computeRecommendation(skills, [
|
|
69
|
+
"review pull request security issues",
|
|
70
|
+
]);
|
|
71
|
+
expect(rec).toBeNull();
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("ignores stopwords and requires ≥2 distinct meaningful hits", () => {
|
|
75
|
+
const skills = [mkSkill("researcher", "the and for a of in on")];
|
|
76
|
+
const rec = computeRecommendation(skills, ["the and for a of in on"]);
|
|
77
|
+
expect(rec).toBeNull();
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("returns null on empty message list", () => {
|
|
81
|
+
const rec = computeRecommendation(
|
|
82
|
+
[mkSkill("code-reviewer", "review pull request security")],
|
|
83
|
+
[]
|
|
84
|
+
);
|
|
85
|
+
expect(rec).toBeNull();
|
|
86
|
+
});
|
|
87
|
+
});
|
|
@@ -78,6 +78,15 @@ export function createScan(
|
|
|
78
78
|
console.warn("[environment] Profile linking failed (non-blocking):", err);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
// Auto-promote unlinked skills to profiles if the user opted in.
|
|
82
|
+
// Imported lazily to avoid a top-level circular import with profile-generator.ts.
|
|
83
|
+
// Fire-and-forget: auto-promote runs asynchronously and failures are logged only.
|
|
84
|
+
import("./profile-generator")
|
|
85
|
+
.then((m) => m.autoPromoteUnlinkedSkills(scanId))
|
|
86
|
+
.catch((err) =>
|
|
87
|
+
console.warn("[environment] Auto-promote failed (non-blocking):", err)
|
|
88
|
+
);
|
|
89
|
+
|
|
81
90
|
return db
|
|
82
91
|
.select()
|
|
83
92
|
.from(environmentScans)
|