stagent 0.9.5 → 0.10.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 +5 -42
- package/dist/cli.js +42 -18
- package/docs/.coverage-gaps.json +13 -55
- package/docs/.last-generated +1 -1
- package/docs/features/provider-runtimes.md +4 -0
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/tables.md +9 -2
- package/docs/features/workflows.md +10 -4
- package/docs/journeys/developer.md +15 -1
- package/docs/journeys/personal-use.md +21 -4
- 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/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/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/diagnostics/chat-streams/route.ts +65 -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/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 +0 -21
- 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/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 +6 -9
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
- package/src/components/chat/chat-command-popover.tsx +2 -2
- package/src/components/chat/chat-input.tsx +2 -3
- package/src/components/chat/chat-session-provider.tsx +720 -0
- package/src/components/chat/chat-shell.tsx +92 -401
- 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/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/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/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/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +4 -5
- 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 +19 -0
- package/src/components/tasks/task-card.tsx +26 -3
- package/src/components/tasks/task-chip-bar.tsx +24 -0
- 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/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent.test.ts +199 -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/claude-agent.ts +155 -18
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +29 -60
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -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__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/codex-engine.ts +16 -17
- package/src/lib/chat/context-builder.ts +5 -3
- package/src/lib/chat/engine.ts +50 -3
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/stagent-tools.ts +1 -0
- package/src/lib/chat/stream-telemetry.ts +132 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +26 -1
- package/src/lib/chat/tool-catalog.ts +2 -1
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +39 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- 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/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +84 -20
- package/src/lib/chat/tools/workflow-tools.ts +234 -32
- package/src/lib/constants/settings.ts +8 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +20 -15
- 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 +45 -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 +68 -23
- package/src/lib/environment/workspace-context.ts +13 -1
- 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 +131 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +78 -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 +153 -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 +232 -13
- 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/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/types.ts +10 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +207 -4
- 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/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
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
# Spec C — Swarm Visibility
|
|
2
|
+
|
|
3
|
+
**Status:** Approved
|
|
4
|
+
**Created:** 2026-04-08
|
|
5
|
+
**Scope mode:** REDUCE
|
|
6
|
+
**Related:** [Schedule Orchestration (Spec A)](./2026-04-08-schedule-orchestration-design.md), [Chat SSE Resilience Hotfix (Spec B)](./2026-04-08-chat-sse-resilience-hotfix-design.md)
|
|
7
|
+
|
|
8
|
+
## Context
|
|
9
|
+
|
|
10
|
+
Spec A introduces a global concurrency cap on scheduled agents. Power users running many schedules will observe queueing delays that they previously did not. Without a visible signal for "how busy is the swarm right now," they'll experience unexplained schedule lateness and file tickets.
|
|
11
|
+
|
|
12
|
+
This spec adds minimal, always-visible swarm-load signal to the app chrome, a saturation-only pre-chat banner, and small enhancements to the schedule list. It is deliberately small — REDUCE mode — to avoid overbuilding visibility infrastructure before we know what users actually need.
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
1. Give users a passive, always-visible signal of swarm load state (quiet / working / saturated).
|
|
17
|
+
2. Warn users *before* they send a chat message if the swarm is at capacity and their chat will queue behind running agents.
|
|
18
|
+
3. Make the new concurrency-driven queueing visible on the schedule list.
|
|
19
|
+
4. Rename "turns" to "agent steps" everywhere user-facing, to close the semantic gap between prompt-level "MAX N turns" hints and runtime-counted turns.
|
|
20
|
+
|
|
21
|
+
## Non-goals (NOT in scope)
|
|
22
|
+
|
|
23
|
+
- **Activity feed route** (`/swarm/activity`) with event log, filters, pagination — future spec "Swarm Activity Feed"
|
|
24
|
+
- **`swarm_snapshots` time-series table** — future spec "Swarm Activity Feed"
|
|
25
|
+
- **Proactive push notifications for overload** — the indicator is always visible, no push needed
|
|
26
|
+
- **Bulk "pause all schedules" action** — users can pause individual schedules from existing pages
|
|
27
|
+
- **Efficiency scoring rings** / turn drift detection alerts — future spec "Schedule Observability"
|
|
28
|
+
- **New `busyness` StatusChip family** — rejected by design review; use custom primitive
|
|
29
|
+
- **Pre-chat banner in `working` state** — only render in `saturated` state to avoid anxiety copy
|
|
30
|
+
- **Traffic-light turn-budget badge** (`lastTurnCount / maxTurns` with color gradient) — leaks policy as warning on normal operation
|
|
31
|
+
- **Popover / Sheet for running schedules list** — hover tooltip + deep link to existing route is sufficient
|
|
32
|
+
|
|
33
|
+
## Design
|
|
34
|
+
|
|
35
|
+
### C.1 `GET /api/swarm-status` endpoint
|
|
36
|
+
|
|
37
|
+
New route at `src/app/api/swarm-status/route.ts`. Reads:
|
|
38
|
+
|
|
39
|
+
- `getAllExecutions()` from `src/lib/agents/execution-manager.ts:60` — filters to `sourceType === 'scheduled'` for running count and schedule metadata
|
|
40
|
+
- A count query on `tasks` table for `status='queued' AND source_type='scheduled'` — queued count
|
|
41
|
+
- `activeChatStreams.size` from Spec A's `src/lib/chat/active-streams.ts`
|
|
42
|
+
|
|
43
|
+
**Response shape:**
|
|
44
|
+
|
|
45
|
+
```json
|
|
46
|
+
{
|
|
47
|
+
"runningScheduled": [
|
|
48
|
+
{
|
|
49
|
+
"scheduleId": "abc",
|
|
50
|
+
"name": "Portfolio Coach",
|
|
51
|
+
"startedAt": "2026-04-08T21:00:03Z",
|
|
52
|
+
"elapsedSec": 42,
|
|
53
|
+
"maxTurns": 500,
|
|
54
|
+
"currentTurns": 127
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"queuedScheduled": [
|
|
58
|
+
{ "scheduleId": "def", "name": "News Sentinel", "queuedAt": "2026-04-08T21:00:14Z", "position": 1 }
|
|
59
|
+
],
|
|
60
|
+
"chatStreamsActive": 0,
|
|
61
|
+
"loadState": "working"
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`loadState` is computed server-side:
|
|
66
|
+
- `quiet` — `runningScheduled.length === 0`
|
|
67
|
+
- `working` — `runningScheduled.length >= 1 && queuedScheduled.length === 0`
|
|
68
|
+
- `saturated` — `queuedScheduled.length > 0` (at-or-above cap)
|
|
69
|
+
|
|
70
|
+
No new DB state — the endpoint reads from in-memory execution map + one SQL count.
|
|
71
|
+
|
|
72
|
+
### C.2 `<SwarmLoadIndicator />` component
|
|
73
|
+
|
|
74
|
+
**Placement:** top of `<SidebarContent>` in `src/components/shared/app-sidebar.tsx`, above the first NavGroup. Not the footer — per design review, the footer is already dense (UpgradeBadge, WorkspaceIndicator, AuthStatusDot, TrustTierBadge, theme toggle) and the sidebar-as-chrome pattern means aggregate system state belongs at the top, where nav groups live.
|
|
75
|
+
|
|
76
|
+
**Visual:** custom primitive (NOT a StatusChip family — semantic mismatch; StatusChip is "one entity, one state", swarm load is "aggregate cardinality"). Reuses badge tokens and the pulse animation pattern but renders as a thin one-line row.
|
|
77
|
+
|
|
78
|
+
**Three states** (not four — red is reserved for actual failures, not backpressure):
|
|
79
|
+
|
|
80
|
+
| State | Condition | Token | Label |
|
|
81
|
+
|---|---|---|---|
|
|
82
|
+
| Quiet | `loadState === 'quiet'` | `text-muted-foreground` | `Swarm quiet` |
|
|
83
|
+
| Working | `loadState === 'working'` | `text-status-running` (indigo, pulse) | `● 2 running` |
|
|
84
|
+
| Saturated | `loadState === 'saturated'` | `text-status-warning` (amber, pulse) | `● 3 running · 1 queued` |
|
|
85
|
+
|
|
86
|
+
**Hover tooltip:** lists up to 3 running schedules inline with elapsed time. Delivers ~80% of the "activity feed" value with zero new overlay state:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
Swarm · 2 running
|
|
90
|
+
─────────────────
|
|
91
|
+
• portfolio-coach 2m
|
|
92
|
+
• launch-copy-chief 41s
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
If there are more than 3 running, append `• +N more`.
|
|
96
|
+
|
|
97
|
+
**Click behavior:** the indicator is a `<Link>` to `/schedules?status=running` — deep-link to the existing schedules page with a filter. No popover, no sheet, no new overlay pattern.
|
|
98
|
+
|
|
99
|
+
**Polling:** every 8s via new `usePolling(url, intervalMs)` hook (C.5). Shared state is used by both the indicator and the pre-chat banner so there is no double-fetch.
|
|
100
|
+
|
|
101
|
+
**Accessibility:** `aria-live="polite"`, tooltip keyboard-focusable, text contrast meets Calm Ops baseline.
|
|
102
|
+
|
|
103
|
+
### C.3 `<ChatOverloadBanner />` component
|
|
104
|
+
|
|
105
|
+
**Placement:** above `<ChatInput />` in `src/components/chat/chat-shell.tsx`.
|
|
106
|
+
|
|
107
|
+
**Render condition:** ONLY when `loadState === 'saturated'` (queue depth > 0). Not `working`. Anxiety copy on normal operation violates Calm Ops tone — "responses may be slower" with zero agency tells users a bad thing might happen and gives them no action.
|
|
108
|
+
|
|
109
|
+
**Visual:** surface-2 bordered banner, `rounded-lg`, amber accent matching the indicator's saturated state.
|
|
110
|
+
|
|
111
|
+
**Copy:** `"Swarm at capacity — your chat will queue behind {N} running agents."` where N is `runningScheduled.length`. One action: `[View Activity]` links to `/schedules?status=running`.
|
|
112
|
+
|
|
113
|
+
**Dismissal:** per conversation, stored in `sessionStorage` keyed by conversation ID. Re-appears if load state flips back to `saturated` after being `working`.
|
|
114
|
+
|
|
115
|
+
### C.4 Schedule list row enhancements
|
|
116
|
+
|
|
117
|
+
Modify `src/components/schedules/schedule-list.tsx` (or equivalent):
|
|
118
|
+
|
|
119
|
+
1. **Queue-depth badge (PR2a):** if a schedule has queued firings waiting for a slot, render `+{N} queued` as an `outline` Badge next to the schedule name. Almost free — reuses existing badge component. Addresses the power-user scenario where 10 schedules fire in a 5-min window and #4-10 queue silently.
|
|
120
|
+
|
|
121
|
+
2. **"Near turn cap" outline badge:** rendered ONLY when `lastTurnCount / maxTurns >= 0.9`. No traffic-light gradient. Progressive disclosure — normal operation shows nothing. At ≥90%, shows a subtle outline badge: `Near step cap`.
|
|
122
|
+
|
|
123
|
+
### C.5 `usePolling(url, intervalMs)` shared hook
|
|
124
|
+
|
|
125
|
+
New file `src/hooks/use-polling.ts`. Extracted from the pattern at `src/components/notifications/inbox-list.tsx:40-43`. Signature:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
export function usePolling<T>(url: string, intervalMs: number): {
|
|
129
|
+
data: T | null;
|
|
130
|
+
error: Error | null;
|
|
131
|
+
loading: boolean;
|
|
132
|
+
};
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Fetches on mount, re-fetches every `intervalMs`. Handles unmount cleanup. Stable query key (URL) so multiple consumers of the same URL share state via module-level cache.
|
|
136
|
+
|
|
137
|
+
Used by: `<SwarmLoadIndicator />`, `<ChatOverloadBanner />`. Can be adopted by other components (inbox list, schedule detail sheet) in future cleanups.
|
|
138
|
+
|
|
139
|
+
### C.6 UI rename: "turns" → "agent steps"
|
|
140
|
+
|
|
141
|
+
User-facing strings only. Keep `maxTurns` as the code/API identifier.
|
|
142
|
+
|
|
143
|
+
- `schedule-form.tsx` field label: "Max turns per firing" → "Max agent steps per run"
|
|
144
|
+
- Tooltip on field: "One step = one agent action (message, tool call, or sub-response). Most schedules use 50–500 steps; heavy research runs 2,000+."
|
|
145
|
+
- Tooltip on prompt field: "Note: writing 'MAX N turns' in your prompt is a hint to the model, not a runtime limit. Use Max agent steps below to enforce a budget."
|
|
146
|
+
- Inline calibration hint after prompt entry: "Schedules like this average ~{N} steps" (derived from `avgTurnsPerFiring` on similar schedules).
|
|
147
|
+
- Schedule list "Near step cap" badge (C.4)
|
|
148
|
+
- Notifications: "Schedule X used 812 / 800 agent steps" (formerly "turns")
|
|
149
|
+
|
|
150
|
+
## Calm Ops compliance checklist
|
|
151
|
+
|
|
152
|
+
- [x] No backdrop-filter, rgba, glass, gradient
|
|
153
|
+
- [x] Running state uses `status-running` (indigo), NOT green (green is `status-completed`)
|
|
154
|
+
- [x] Saturated state uses `status-warning` (amber), NOT red (red is `status-failed`)
|
|
155
|
+
- [x] No new StatusChip family added (use custom `SwarmLoadIndicator`)
|
|
156
|
+
- [x] No popover/sheet overlay — tooltip + deep link only
|
|
157
|
+
- [x] Banner only renders when actionable (saturated state), not `working`
|
|
158
|
+
- [x] All radii ≤ `rounded-xl`
|
|
159
|
+
- [x] Polling pattern reuses existing template (`inbox-list.tsx`)
|
|
160
|
+
- [x] Any Sheet usage (N/A here) would need `px-6 pb-6` body padding
|
|
161
|
+
|
|
162
|
+
## Tests
|
|
163
|
+
|
|
164
|
+
### Unit / component
|
|
165
|
+
1. `<SwarmLoadIndicator />` renders correct state (Quiet / Working / Saturated) for each input
|
|
166
|
+
2. Tooltip shows running schedules; click navigates to `/schedules?status=running`
|
|
167
|
+
3. `<ChatOverloadBanner />` renders ONLY in `saturated` state
|
|
168
|
+
4. Dismissal persists in sessionStorage across re-mounts
|
|
169
|
+
5. Queue-depth badge renders when schedule has queued firings
|
|
170
|
+
6. "Near step cap" badge renders only at ≥90% ratio
|
|
171
|
+
7. `usePolling` hook fetches on mount, re-fetches on interval, cleans up on unmount
|
|
172
|
+
8. Multiple consumers of same URL share state (no duplicate fetches)
|
|
173
|
+
|
|
174
|
+
### API
|
|
175
|
+
9. `GET /api/swarm-status` returns correct shape with running/queued/chat counts
|
|
176
|
+
10. `loadState` computed correctly at boundary conditions (0 running, cap-1 running, cap running, queue>0)
|
|
177
|
+
|
|
178
|
+
### Accessibility
|
|
179
|
+
11. Indicator has `aria-live="polite"`
|
|
180
|
+
12. Tooltip is keyboard-focusable
|
|
181
|
+
13. Contrast meets Calm Ops baseline (manual check)
|
|
182
|
+
|
|
183
|
+
### Visual regression
|
|
184
|
+
14. Screenshot sidebar in all 3 states; compare to Calm Ops tokens
|
|
185
|
+
|
|
186
|
+
## Files touched
|
|
187
|
+
|
|
188
|
+
### New
|
|
189
|
+
- `src/app/api/swarm-status/route.ts`
|
|
190
|
+
- `src/hooks/use-polling.ts`
|
|
191
|
+
- `src/components/shared/swarm-load-indicator.tsx`
|
|
192
|
+
- `src/components/chat/chat-overload-banner.tsx`
|
|
193
|
+
|
|
194
|
+
### Modify
|
|
195
|
+
- `src/components/shared/app-sidebar.tsx` — mount `<SwarmLoadIndicator />` at top of SidebarContent
|
|
196
|
+
- `src/components/chat/chat-shell.tsx` — mount `<ChatOverloadBanner />` above ChatInput
|
|
197
|
+
- `src/components/schedules/schedule-list.tsx` (or equivalent) — queue-depth badge + near-cap badge
|
|
198
|
+
- `src/components/schedules/schedule-form.tsx` — rename + tooltips + calibration hint
|
|
199
|
+
|
|
200
|
+
### Not modified (avoiding pollution)
|
|
201
|
+
- `src/lib/constants/status-families.ts` — NO new `busyness` family per design review
|
|
202
|
+
|
|
203
|
+
## Dependencies on Spec A
|
|
204
|
+
|
|
205
|
+
- `<SwarmLoadIndicator />` reads `chatStreamsActive` from the in-memory `activeChatStreams` set created by Spec A (`src/lib/chat/active-streams.ts`). C can scaffold mid-A after A's interface is pinned.
|
|
206
|
+
- Queue-depth badge reads `tasks.status='queued' AND source_type='scheduled'` which exists today but is populated meaningfully only after Spec A's concurrency limiter lands.
|
|
207
|
+
- "Near step cap" badge reads `schedules.max_turns` column added by Spec A.
|
|
208
|
+
|
|
209
|
+
## Ship plan
|
|
210
|
+
|
|
211
|
+
- No feature flag — UI is additive and safe.
|
|
212
|
+
- Scaffolding (API endpoint, hook, indicator component) can begin mid-A.
|
|
213
|
+
- Full ship after Spec A stabilizes and A's data-model migrations have landed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "AI Business Operating System — run your business with AI agents. Local-first, multi-provider, governed.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -67,7 +67,6 @@
|
|
|
67
67
|
"@dnd-kit/sortable": "^10.0.0",
|
|
68
68
|
"@dnd-kit/utilities": "^3.2.2",
|
|
69
69
|
"@hookform/resolvers": "^5.2.2",
|
|
70
|
-
"@supabase/supabase-js": "^2.101.1",
|
|
71
70
|
"@tailwindcss/postcss": "^4",
|
|
72
71
|
"@tailwindcss/typography": "^0.5",
|
|
73
72
|
"@tanstack/react-table": "^8.21.3",
|
|
@@ -97,6 +96,7 @@
|
|
|
97
96
|
"react-markdown": "^10.1.0",
|
|
98
97
|
"recharts": "^3.8.1",
|
|
99
98
|
"remark-gfm": "^4.0.1",
|
|
99
|
+
"semver": "^7.7.4",
|
|
100
100
|
"sharp": "^0.34.5",
|
|
101
101
|
"smol-toml": "^1.6.1",
|
|
102
102
|
"sonner": "^2.0.7",
|
|
@@ -116,6 +116,7 @@
|
|
|
116
116
|
"@types/js-yaml": "^4.0.9",
|
|
117
117
|
"@types/react": "^19",
|
|
118
118
|
"@types/react-dom": "^19",
|
|
119
|
+
"@types/semver": "^7.7.1",
|
|
119
120
|
"@types/sharp": "^0.31.1",
|
|
120
121
|
"@vitejs/plugin-react": "^5.1.4",
|
|
121
122
|
"@vitest/coverage-v8": "^4.0.18",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
describe("instrumentation register()", () => {
|
|
4
|
+
afterEach(() => {
|
|
5
|
+
vi.unstubAllEnvs();
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
it("ensureInstance is importable from the bootstrap module and returns a skipped result in dev mode", async () => {
|
|
9
|
+
vi.stubEnv("STAGENT_DEV_MODE", "true");
|
|
10
|
+
const { ensureInstance } = await import("@/lib/instance/bootstrap");
|
|
11
|
+
const result = await ensureInstance();
|
|
12
|
+
expect(result.skipped).toBe("dev_mode_env");
|
|
13
|
+
expect(result.steps).toEqual([]);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { Suspense } from "react";
|
|
2
2
|
import { PageShell } from "@/components/shared/page-shell";
|
|
3
3
|
import { AnalyticsDashboard } from "@/components/analytics/analytics-dashboard";
|
|
4
|
-
import { AnalyticsGateCard } from "@/components/analytics/analytics-gate-card";
|
|
5
|
-
import { licenseManager } from "@/lib/license/manager";
|
|
6
4
|
import {
|
|
7
5
|
getOutcomeCounts,
|
|
8
6
|
getSuccessRateTrend,
|
|
@@ -14,16 +12,13 @@ import {
|
|
|
14
12
|
export const dynamic = "force-dynamic";
|
|
15
13
|
|
|
16
14
|
function AnalyticsContent() {
|
|
17
|
-
const tier = licenseManager.getTierFromDb();
|
|
18
|
-
const isAllowed = tier !== "community";
|
|
19
|
-
|
|
20
15
|
const outcomes = getOutcomeCounts(30);
|
|
21
16
|
const successTrend = getSuccessRateTrend(30);
|
|
22
17
|
const costTrend = getCostPerOutcomeTrend(30);
|
|
23
18
|
const leaderboard = getProfileLeaderboard(30);
|
|
24
19
|
const hoursSaved = getEstimatedHoursSaved(30);
|
|
25
20
|
|
|
26
|
-
|
|
21
|
+
return (
|
|
27
22
|
<AnalyticsDashboard
|
|
28
23
|
outcomes={outcomes}
|
|
29
24
|
successTrend={successTrend}
|
|
@@ -32,21 +27,6 @@ function AnalyticsContent() {
|
|
|
32
27
|
hoursSaved={hoursSaved}
|
|
33
28
|
/>
|
|
34
29
|
);
|
|
35
|
-
|
|
36
|
-
if (isAllowed) return dashboard;
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<div className="relative">
|
|
40
|
-
{/* Blurred dashboard preview */}
|
|
41
|
-
<div className="opacity-20 pointer-events-none select-none blur-[2px]" aria-hidden>
|
|
42
|
-
{dashboard}
|
|
43
|
-
</div>
|
|
44
|
-
{/* Upgrade CTA */}
|
|
45
|
-
<div className="absolute inset-0 flex items-start justify-center pt-16">
|
|
46
|
-
<AnalyticsGateCard />
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
);
|
|
50
30
|
}
|
|
51
31
|
|
|
52
32
|
export default function AnalyticsPage() {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { getConversation, getMessages } from "@/lib/data/chat";
|
|
3
3
|
import { sendMessage } from "@/lib/chat/engine";
|
|
4
|
+
import { recordTermination } from "@/lib/chat/stream-telemetry";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* GET /api/chat/conversations/[id]/messages?after=xxx&limit=100
|
|
@@ -58,6 +59,7 @@ export async function POST(
|
|
|
58
59
|
|
|
59
60
|
// Bridge the async generator to an SSE ReadableStream
|
|
60
61
|
const encoder = new TextEncoder();
|
|
62
|
+
const streamStartedAt = Date.now();
|
|
61
63
|
const stream = new ReadableStream({
|
|
62
64
|
async start(controller) {
|
|
63
65
|
const keepalive = setInterval(() => {
|
|
@@ -94,9 +96,28 @@ export async function POST(
|
|
|
94
96
|
);
|
|
95
97
|
} finally {
|
|
96
98
|
clearInterval(keepalive);
|
|
97
|
-
|
|
99
|
+
try {
|
|
100
|
+
controller.close();
|
|
101
|
+
} catch {
|
|
102
|
+
// Stream may already be closed by peer; safe to ignore
|
|
103
|
+
}
|
|
98
104
|
}
|
|
99
105
|
},
|
|
106
|
+
// Fires when the client disconnects mid-stream (browser tab closed,
|
|
107
|
+
// user navigated away, AbortController.abort() fired on the fetch).
|
|
108
|
+
// The engine's own `req.signal` abort already records
|
|
109
|
+
// `stream.aborted.signal` in its catch path — this cancel callback
|
|
110
|
+
// only fires when the ReadableStream is torn down independently,
|
|
111
|
+
// so record it as a distinct `stream.aborted.client` code.
|
|
112
|
+
cancel(reason) {
|
|
113
|
+
recordTermination({
|
|
114
|
+
reason: "stream.aborted.client",
|
|
115
|
+
conversationId: id,
|
|
116
|
+
messageId: null,
|
|
117
|
+
durationMs: Date.now() - streamStartedAt,
|
|
118
|
+
error: reason ? String(reason).slice(0, 200) : undefined,
|
|
119
|
+
});
|
|
120
|
+
},
|
|
100
121
|
});
|
|
101
122
|
|
|
102
123
|
return new Response(stream, {
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import {
|
|
3
|
+
readTerminations,
|
|
4
|
+
countTerminations,
|
|
5
|
+
} from "@/lib/chat/stream-telemetry";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /api/diagnostics/chat-streams
|
|
9
|
+
*
|
|
10
|
+
* Dev-only diagnostics endpoint that reports how chat SSE streams have
|
|
11
|
+
* terminated in the current server process. Returns counts by reason code
|
|
12
|
+
* plus the most recent N events.
|
|
13
|
+
*
|
|
14
|
+
* Query params:
|
|
15
|
+
* ?windowMinutes=N — restrict counts to the last N minutes (default: all)
|
|
16
|
+
* ?limit=N — cap on recent events returned (default: 50, max: 500)
|
|
17
|
+
*
|
|
18
|
+
* Response shape:
|
|
19
|
+
* {
|
|
20
|
+
* windowMinutes: number | null,
|
|
21
|
+
* totalEvents: number,
|
|
22
|
+
* counts: Record<TerminationReason, number>,
|
|
23
|
+
* recent: TerminationEvent[]
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* Gated behind NODE_ENV !== production to match the data/clear and
|
|
27
|
+
* data/seed routes. This is for maintainer inspection, not end users.
|
|
28
|
+
*
|
|
29
|
+
* See src/lib/chat/stream-telemetry.ts for the ring buffer + reason code
|
|
30
|
+
* definitions. See features/chat-stream-resilience-telemetry.md for the
|
|
31
|
+
* motivation (verify-before-building telemetry for a mid-stream refresh
|
|
32
|
+
* bug reported by a sibling repo that doesn't reproduce here).
|
|
33
|
+
*/
|
|
34
|
+
export async function GET(req: NextRequest) {
|
|
35
|
+
if (process.env.NODE_ENV === "production") {
|
|
36
|
+
return NextResponse.json(
|
|
37
|
+
{ error: "Diagnostics disabled in production" },
|
|
38
|
+
{ status: 403 }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { searchParams } = req.nextUrl;
|
|
43
|
+
const windowMinutesRaw = searchParams.get("windowMinutes");
|
|
44
|
+
const limitRaw = searchParams.get("limit");
|
|
45
|
+
|
|
46
|
+
const windowMinutes =
|
|
47
|
+
windowMinutesRaw !== null ? Math.max(0, parseInt(windowMinutesRaw, 10) || 0) : null;
|
|
48
|
+
const windowMs = windowMinutes !== null ? windowMinutes * 60 * 1000 : 0;
|
|
49
|
+
|
|
50
|
+
const limit = Math.min(
|
|
51
|
+
500,
|
|
52
|
+
limitRaw !== null ? Math.max(1, parseInt(limitRaw, 10) || 50) : 50
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const all = readTerminations();
|
|
56
|
+
const recent = all.slice(-limit).reverse(); // newest first
|
|
57
|
+
const counts = countTerminations(windowMs);
|
|
58
|
+
|
|
59
|
+
return NextResponse.json({
|
|
60
|
+
windowMinutes,
|
|
61
|
+
totalEvents: all.length,
|
|
62
|
+
counts,
|
|
63
|
+
recent,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import {
|
|
3
|
+
getInstanceConfig,
|
|
4
|
+
getGuardrails,
|
|
5
|
+
getUpgradeState,
|
|
6
|
+
} from "@/lib/instance/settings";
|
|
7
|
+
import { isDevMode } from "@/lib/instance/detect";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GET /api/instance/config
|
|
11
|
+
*
|
|
12
|
+
* Returns the full instance state: config, guardrails, and upgrade state
|
|
13
|
+
* in a single response. Used by the Settings → Instance section and by
|
|
14
|
+
* the upgrade pre-flight modal.
|
|
15
|
+
*
|
|
16
|
+
* When running on the canonical dev repo (STAGENT_DEV_MODE=true or the
|
|
17
|
+
* .git/stagent-dev-mode sentinel), returns `{ devMode: true }` with null
|
|
18
|
+
* payloads. This prevents stale instance rows written during prior testing
|
|
19
|
+
* from surfacing in the UI as if the dev repo were a real instance.
|
|
20
|
+
*/
|
|
21
|
+
export async function GET() {
|
|
22
|
+
try {
|
|
23
|
+
if (isDevMode()) {
|
|
24
|
+
return NextResponse.json({
|
|
25
|
+
devMode: true,
|
|
26
|
+
config: null,
|
|
27
|
+
guardrails: null,
|
|
28
|
+
upgrade: null,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return NextResponse.json({
|
|
32
|
+
devMode: false,
|
|
33
|
+
config: getInstanceConfig(),
|
|
34
|
+
guardrails: getGuardrails(),
|
|
35
|
+
upgrade: getUpgradeState(),
|
|
36
|
+
});
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { ensureInstance } from "@/lib/instance/bootstrap";
|
|
3
|
+
import {
|
|
4
|
+
getInstanceConfig,
|
|
5
|
+
getGuardrails,
|
|
6
|
+
getUpgradeState,
|
|
7
|
+
} from "@/lib/instance/settings";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POST /api/instance/init
|
|
11
|
+
*
|
|
12
|
+
* Idempotent manual re-run of the instance bootstrap. Useful when the
|
|
13
|
+
* initial boot-time run failed (permission error, git not installed),
|
|
14
|
+
* or when the user wants to re-apply guardrails after changing consent
|
|
15
|
+
* via the Settings → Instance UI.
|
|
16
|
+
*
|
|
17
|
+
* Returns the current instance config + guardrails + upgrade state after
|
|
18
|
+
* the re-run so the Settings → Instance section can refresh its display
|
|
19
|
+
* without a second request.
|
|
20
|
+
*/
|
|
21
|
+
export async function POST() {
|
|
22
|
+
try {
|
|
23
|
+
const result = await ensureInstance();
|
|
24
|
+
return NextResponse.json({
|
|
25
|
+
ensureResult: result,
|
|
26
|
+
config: getInstanceConfig(),
|
|
27
|
+
guardrails: getGuardrails(),
|
|
28
|
+
upgrade: getUpgradeState(),
|
|
29
|
+
});
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { tick } from "@/lib/instance/upgrade-poller";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* POST /api/instance/upgrade/check
|
|
6
|
+
*
|
|
7
|
+
* Force-run the upgrade availability poller. Rate-limited to one call per
|
|
8
|
+
* ~5 minutes via the same lock file the scheduled poller uses. Returns the
|
|
9
|
+
* new UpgradeState on success, or a skipped reason if the lock was held or
|
|
10
|
+
* dev-mode was active.
|
|
11
|
+
*/
|
|
12
|
+
export async function POST() {
|
|
13
|
+
try {
|
|
14
|
+
const result = await tick();
|
|
15
|
+
if (result.updated) {
|
|
16
|
+
return NextResponse.json({ ok: true, state: result.updated });
|
|
17
|
+
}
|
|
18
|
+
return NextResponse.json(
|
|
19
|
+
{ ok: false, skipped: result.skipped, error: result.error },
|
|
20
|
+
{ status: 202 }
|
|
21
|
+
);
|
|
22
|
+
} catch (err) {
|
|
23
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
24
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { db } from "@/lib/db";
|
|
4
|
+
import { tasks } from "@/lib/db/schema";
|
|
5
|
+
import {
|
|
6
|
+
getInstanceConfig,
|
|
7
|
+
getUpgradeState,
|
|
8
|
+
setUpgradeState,
|
|
9
|
+
} from "@/lib/instance/settings";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* POST /api/instance/upgrade
|
|
13
|
+
*
|
|
14
|
+
* Spawns an upgrade task with the `upgrade-assistant` agent profile. Returns
|
|
15
|
+
* 202 Accepted with the task id; the client then navigates to the upgrade
|
|
16
|
+
* session view to watch streaming progress and respond to conflict prompts.
|
|
17
|
+
*
|
|
18
|
+
* The task description includes the instance context (branch name, commits
|
|
19
|
+
* behind, data directory) as template variables that the profile's SKILL.md
|
|
20
|
+
* references. The claude-agent runtime interpolates them when building the
|
|
21
|
+
* system prompt.
|
|
22
|
+
*
|
|
23
|
+
* Fire-and-forget per TDR-001: the route returns immediately; task execution
|
|
24
|
+
* runs in the background through the existing execution-manager pipeline.
|
|
25
|
+
*/
|
|
26
|
+
export async function POST() {
|
|
27
|
+
try {
|
|
28
|
+
const config = getInstanceConfig();
|
|
29
|
+
if (!config) {
|
|
30
|
+
return NextResponse.json(
|
|
31
|
+
{ error: "Instance not yet initialized — run POST /api/instance/init first" },
|
|
32
|
+
{ status: 409 }
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const upgrade = getUpgradeState();
|
|
37
|
+
if (!upgrade.upgradeAvailable) {
|
|
38
|
+
return NextResponse.json(
|
|
39
|
+
{ error: "No upgrade available", upgradeState: upgrade },
|
|
40
|
+
{ status: 409 }
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const branchName = config.branchName;
|
|
45
|
+
const commitsBehind = upgrade.commitsBehind;
|
|
46
|
+
const dataDir = process.env.STAGENT_DATA_DIR ?? "~/.stagent";
|
|
47
|
+
|
|
48
|
+
const description = [
|
|
49
|
+
`Upgrade instance branch \`${branchName}\` with ${commitsBehind} upstream commit(s) from origin/main.`,
|
|
50
|
+
"",
|
|
51
|
+
"Context for the upgrade-assistant profile:",
|
|
52
|
+
`- INSTANCE_BRANCH=${branchName}`,
|
|
53
|
+
`- COMMITS_BEHIND=${commitsBehind}`,
|
|
54
|
+
`- DATA_DIR=${dataDir}`,
|
|
55
|
+
"",
|
|
56
|
+
"Follow the standard merge flow defined in SKILL.md. Stop and ask the user on any merge conflict. Abort and roll back on any failure. Do not push any branch.",
|
|
57
|
+
].join("\n");
|
|
58
|
+
|
|
59
|
+
const id = randomUUID();
|
|
60
|
+
const now = new Date();
|
|
61
|
+
|
|
62
|
+
db.insert(tasks)
|
|
63
|
+
.values({
|
|
64
|
+
id,
|
|
65
|
+
title: `Upgrade ${branchName} — ${commitsBehind} upstream commit${commitsBehind === 1 ? "" : "s"}`,
|
|
66
|
+
description,
|
|
67
|
+
projectId: null,
|
|
68
|
+
priority: 1,
|
|
69
|
+
assignedAgent: null,
|
|
70
|
+
agentProfile: "upgrade-assistant",
|
|
71
|
+
sourceType: "manual",
|
|
72
|
+
status: "planned",
|
|
73
|
+
createdAt: now,
|
|
74
|
+
updatedAt: now,
|
|
75
|
+
})
|
|
76
|
+
.run();
|
|
77
|
+
|
|
78
|
+
// Record which task id owns this upgrade so the UI can deep-link to it,
|
|
79
|
+
// and optimistically clear the pending-count so the sidebar badge and
|
|
80
|
+
// settings card reflect the user's intent immediately. If the merge task
|
|
81
|
+
// fails or is cancelled, the next scheduled poll (or a manual "Check for
|
|
82
|
+
// upgrades") will restore the real count by re-running git rev-list.
|
|
83
|
+
await setUpgradeState({
|
|
84
|
+
...upgrade,
|
|
85
|
+
lastUpgradeTaskId: id,
|
|
86
|
+
commitsBehind: 0,
|
|
87
|
+
upgradeAvailable: false,
|
|
88
|
+
lastSuccessfulUpgradeAt: Math.floor(Date.now() / 1000),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return NextResponse.json({ taskId: id }, { status: 202 });
|
|
92
|
+
} catch (err) {
|
|
93
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
94
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { getUpgradeState } from "@/lib/instance/settings";
|
|
3
|
+
import { isDevMode } from "@/lib/instance/detect";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GET /api/instance/upgrade/status
|
|
7
|
+
*
|
|
8
|
+
* Returns the current UpgradeState for client components that need to poll
|
|
9
|
+
* (e.g. the upgrade modal pre-flight). Server Components should read directly
|
|
10
|
+
* from settings per TDR-004 rather than calling this route.
|
|
11
|
+
*
|
|
12
|
+
* When running on the canonical dev repo, returns a synthetic state with
|
|
13
|
+
* `devMode: true` and `upgradeAvailable: false` so the sidebar upgrade
|
|
14
|
+
* button never renders on main.
|
|
15
|
+
*/
|
|
16
|
+
export async function GET() {
|
|
17
|
+
try {
|
|
18
|
+
if (isDevMode()) {
|
|
19
|
+
return NextResponse.json({
|
|
20
|
+
devMode: true,
|
|
21
|
+
lastPolledAt: null,
|
|
22
|
+
upgradeAvailable: false,
|
|
23
|
+
commitsBehind: 0,
|
|
24
|
+
lastSuccessfulUpgradeAt: null,
|
|
25
|
+
pollFailureCount: 0,
|
|
26
|
+
lastPollError: null,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
const state = getUpgradeState();
|
|
30
|
+
return NextResponse.json({ devMode: false, ...state });
|
|
31
|
+
} catch (err) {
|
|
32
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
33
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
34
|
+
}
|
|
35
|
+
}
|