stagent 0.5.0 → 0.6.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.
Files changed (252) hide show
  1. package/README.md +8 -8
  2. package/dist/cli.js +146 -2
  3. package/docs/.coverage-gaps.json +21 -0
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +36 -14
  6. package/docs/features/chat.md +33 -56
  7. package/docs/features/cost-usage.md +14 -10
  8. package/docs/features/dashboard-kanban.md +30 -13
  9. package/docs/features/delivery-channels.md +198 -0
  10. package/docs/features/design-system.md +10 -10
  11. package/docs/features/documents.md +8 -8
  12. package/docs/features/home-workspace.md +20 -15
  13. package/docs/features/inbox-notifications.md +22 -10
  14. package/docs/features/keyboard-navigation.md +11 -11
  15. package/docs/features/monitoring.md +1 -1
  16. package/docs/features/playbook.md +30 -32
  17. package/docs/features/profiles.md +33 -11
  18. package/docs/features/projects.md +2 -2
  19. package/docs/features/provider-runtimes.md +58 -14
  20. package/docs/features/schedules.md +70 -40
  21. package/docs/features/settings.md +74 -46
  22. package/docs/features/shared-components.md +7 -15
  23. package/docs/features/tool-permissions.md +9 -9
  24. package/docs/features/workflows.md +32 -21
  25. package/docs/getting-started.md +33 -9
  26. package/docs/index.md +25 -16
  27. package/docs/journeys/developer.md +124 -207
  28. package/docs/journeys/personal-use.md +70 -79
  29. package/docs/journeys/power-user.md +107 -151
  30. package/docs/journeys/work-use.md +81 -113
  31. package/docs/manifest.json +77 -45
  32. package/docs/superpowers/plans/2026-03-30-finish-in-progress-features.md +547 -0
  33. package/docs/use-cases/agency-operator.md +84 -0
  34. package/docs/use-cases/solo-founder.md +75 -0
  35. package/docs/why-stagent.md +59 -0
  36. package/package.json +10 -3
  37. package/src/app/api/channels/[id]/route.ts +103 -0
  38. package/src/app/api/channels/[id]/test/route.ts +52 -0
  39. package/src/app/api/channels/inbound/slack/route.ts +109 -0
  40. package/src/app/api/channels/inbound/telegram/poll/route.ts +128 -0
  41. package/src/app/api/channels/inbound/telegram/route.ts +76 -0
  42. package/src/app/api/channels/route.ts +71 -0
  43. package/src/app/api/chat/conversations/route.ts +15 -0
  44. package/src/app/api/chat/entities/search/route.ts +46 -31
  45. package/src/app/api/environment/profiles/suggest/route.ts +19 -3
  46. package/src/app/api/environment/scan/route.ts +8 -1
  47. package/src/app/api/handoffs/[id]/route.ts +76 -0
  48. package/src/app/api/handoffs/route.ts +89 -0
  49. package/src/app/api/memory/route.ts +181 -0
  50. package/src/app/api/profiles/[id]/route.ts +16 -1
  51. package/src/app/api/profiles/[id]/test/route.ts +4 -0
  52. package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
  53. package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
  54. package/src/app/api/profiles/assist/route.ts +35 -0
  55. package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
  56. package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
  57. package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
  58. package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
  59. package/src/app/api/profiles/import-repo/route.ts +29 -0
  60. package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
  61. package/src/app/api/profiles/route.ts +73 -22
  62. package/src/app/api/runtimes/ollama/route.ts +86 -0
  63. package/src/app/api/runtimes/suggest/route.ts +29 -0
  64. package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
  65. package/src/app/api/schedules/[id]/route.ts +41 -3
  66. package/src/app/api/schedules/parse/route.ts +66 -0
  67. package/src/app/api/schedules/route.ts +71 -12
  68. package/src/app/api/settings/author-default/route.ts +7 -0
  69. package/src/app/api/settings/learning/route.ts +41 -0
  70. package/src/app/api/settings/ollama/route.ts +34 -0
  71. package/src/app/api/settings/providers/route.ts +57 -0
  72. package/src/app/api/settings/routing/route.ts +24 -0
  73. package/src/app/api/settings/web-search/route.ts +28 -0
  74. package/src/app/api/tasks/[id]/execute/route.ts +13 -1
  75. package/src/app/documents/page.tsx +3 -0
  76. package/src/app/environment/page.tsx +8 -1
  77. package/src/app/settings/page.tsx +10 -4
  78. package/src/app/workflows/[id]/edit/page.tsx +2 -0
  79. package/src/app/workflows/new/page.tsx +2 -0
  80. package/src/components/chat/chat-command-popover.tsx +22 -19
  81. package/src/components/chat/chat-input.tsx +5 -0
  82. package/src/components/chat/chat-model-selector.tsx +42 -1
  83. package/src/components/chat/chat-shell.tsx +2 -0
  84. package/src/components/dashboard/welcome-landing.tsx +9 -9
  85. package/src/components/environment/artifact-card.tsx +27 -1
  86. package/src/components/environment/environment-dashboard.tsx +50 -2
  87. package/src/components/environment/environment-summary-card.tsx +5 -2
  88. package/src/components/environment/suggested-profiles.tsx +117 -52
  89. package/src/components/handoffs/handoff-approval-card.tsx +159 -0
  90. package/src/components/memory/memory-browser.tsx +315 -0
  91. package/src/components/profiles/learned-context-panel.tsx +4 -4
  92. package/src/components/profiles/profile-assist-panel.tsx +512 -0
  93. package/src/components/profiles/profile-browser.tsx +109 -8
  94. package/src/components/profiles/profile-card.tsx +29 -1
  95. package/src/components/profiles/profile-detail-view.tsx +200 -28
  96. package/src/components/profiles/profile-form-view.tsx +220 -82
  97. package/src/components/profiles/repo-import-wizard.tsx +648 -0
  98. package/src/components/profiles/smoke-test-editor.tsx +106 -0
  99. package/src/components/schedules/schedule-create-sheet.tsx +9 -1
  100. package/src/components/schedules/schedule-form.tsx +348 -9
  101. package/src/components/schedules/schedule-list.tsx +15 -2
  102. package/src/components/settings/auth-method-selector.tsx +7 -1
  103. package/src/components/settings/budget-guardrails-section.tsx +111 -48
  104. package/src/components/settings/channels-section.tsx +526 -0
  105. package/src/components/settings/chat-settings-section.tsx +27 -1
  106. package/src/components/settings/data-management-section.tsx +8 -6
  107. package/src/components/settings/learning-context-section.tsx +124 -0
  108. package/src/components/settings/ollama-section.tsx +270 -0
  109. package/src/components/settings/providers-runtimes-section.tsx +499 -0
  110. package/src/components/settings/web-search-section.tsx +101 -0
  111. package/src/components/shared/tag-input.tsx +156 -0
  112. package/src/components/tasks/kanban-board.tsx +32 -0
  113. package/src/components/tasks/kanban-column.tsx +4 -2
  114. package/src/components/tasks/task-card.tsx +1 -0
  115. package/src/components/tasks/task-chip-bar.tsx +6 -1
  116. package/src/components/tasks/task-create-panel.tsx +55 -5
  117. package/src/components/workflows/workflow-form-view.tsx +38 -3
  118. package/src/hooks/use-chat-autocomplete.ts +24 -26
  119. package/src/hooks/use-project-skills.ts +66 -0
  120. package/src/hooks/use-tag-suggestions.ts +31 -0
  121. package/src/instrumentation.ts +4 -1
  122. package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
  123. package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
  124. package/src/lib/agents/agentic-loop.ts +235 -0
  125. package/src/lib/agents/browser-mcp.ts +59 -4
  126. package/src/lib/agents/claude-agent.ts +26 -199
  127. package/src/lib/agents/handoff/bus.ts +164 -0
  128. package/src/lib/agents/handoff/governance.ts +47 -0
  129. package/src/lib/agents/handoff/types.ts +16 -0
  130. package/src/lib/agents/learned-context.ts +27 -7
  131. package/src/lib/agents/memory/decay.ts +61 -0
  132. package/src/lib/agents/memory/extractor.ts +181 -0
  133. package/src/lib/agents/memory/retrieval.ts +96 -0
  134. package/src/lib/agents/memory/types.ts +6 -0
  135. package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
  136. package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
  137. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
  138. package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
  139. package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
  140. package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
  141. package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
  142. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
  143. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
  144. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
  145. package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
  146. package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
  147. package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
  148. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
  149. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
  150. package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
  151. package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
  152. package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
  153. package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
  154. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
  155. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
  156. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
  157. package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
  158. package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
  159. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
  160. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
  161. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
  162. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
  163. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
  164. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
  165. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
  166. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
  167. package/src/lib/agents/profiles/project-profiles.ts +193 -0
  168. package/src/lib/agents/profiles/registry.ts +130 -6
  169. package/src/lib/agents/profiles/types.ts +28 -0
  170. package/src/lib/agents/router.ts +174 -2
  171. package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
  172. package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
  173. package/src/lib/agents/runtime/catalog.ts +57 -2
  174. package/src/lib/agents/runtime/claude.ts +205 -1
  175. package/src/lib/agents/runtime/index.ts +22 -0
  176. package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
  177. package/src/lib/agents/runtime/openai-direct.ts +514 -0
  178. package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
  179. package/src/lib/agents/runtime/types.ts +2 -0
  180. package/src/lib/agents/tool-permissions.ts +203 -0
  181. package/src/lib/channels/gateway.ts +321 -0
  182. package/src/lib/channels/poller.ts +268 -0
  183. package/src/lib/channels/registry.ts +90 -0
  184. package/src/lib/channels/slack-adapter.ts +188 -0
  185. package/src/lib/channels/telegram-adapter.ts +218 -0
  186. package/src/lib/channels/types.ts +43 -0
  187. package/src/lib/channels/webhook-adapter.ts +74 -0
  188. package/src/lib/chat/context-builder.ts +22 -2
  189. package/src/lib/chat/engine.ts +95 -13
  190. package/src/lib/chat/ollama-engine.ts +198 -0
  191. package/src/lib/chat/stagent-tools.ts +106 -20
  192. package/src/lib/chat/tool-catalog.ts +24 -0
  193. package/src/lib/chat/tool-registry.ts +90 -0
  194. package/src/lib/chat/tools/chat-history-tools.ts +4 -4
  195. package/src/lib/chat/tools/document-tools.ts +7 -7
  196. package/src/lib/chat/tools/handoff-tools.ts +70 -0
  197. package/src/lib/chat/tools/notification-tools.ts +4 -4
  198. package/src/lib/chat/tools/profile-tools.ts +3 -3
  199. package/src/lib/chat/tools/project-tools.ts +3 -3
  200. package/src/lib/chat/tools/schedule-tools.ts +29 -13
  201. package/src/lib/chat/tools/settings-tools.ts +2 -2
  202. package/src/lib/chat/tools/task-tools.ts +66 -11
  203. package/src/lib/chat/tools/usage-tools.ts +2 -2
  204. package/src/lib/chat/tools/workflow-tools.ts +8 -8
  205. package/src/lib/chat/types.ts +11 -5
  206. package/src/lib/constants/known-tools.ts +19 -0
  207. package/src/lib/constants/prose-styles.ts +1 -1
  208. package/src/lib/constants/settings.ts +7 -0
  209. package/src/lib/data/channel-bindings.ts +85 -0
  210. package/src/lib/data/clear.ts +22 -0
  211. package/src/lib/data/profile-test-results.ts +48 -0
  212. package/src/lib/data/seed-data/conversations.ts +196 -0
  213. package/src/lib/data/seed-data/learned-context.ts +99 -0
  214. package/src/lib/data/seed-data/notifications.ts +54 -1
  215. package/src/lib/data/seed-data/profile-test-results.ts +96 -0
  216. package/src/lib/data/seed-data/repo-imports.ts +51 -0
  217. package/src/lib/data/seed-data/views.ts +60 -0
  218. package/src/lib/data/seed.ts +51 -0
  219. package/src/lib/db/bootstrap.ts +162 -0
  220. package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
  221. package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
  222. package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
  223. package/src/lib/db/schema.ts +187 -1
  224. package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
  225. package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
  226. package/src/lib/environment/auto-scan.ts +48 -0
  227. package/src/lib/environment/data.ts +25 -0
  228. package/src/lib/environment/profile-generator.ts +40 -10
  229. package/src/lib/environment/profile-linker.ts +143 -0
  230. package/src/lib/environment/profile-rules.ts +96 -0
  231. package/src/lib/import/dedup.ts +149 -0
  232. package/src/lib/import/format-adapter.ts +631 -0
  233. package/src/lib/import/github-api.ts +219 -0
  234. package/src/lib/import/repo-scanner.ts +251 -0
  235. package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
  236. package/src/lib/schedules/active-hours.ts +120 -0
  237. package/src/lib/schedules/heartbeat-parser.ts +224 -0
  238. package/src/lib/schedules/heartbeat-prompt.ts +153 -0
  239. package/src/lib/schedules/nlp-parser.ts +357 -0
  240. package/src/lib/schedules/scheduler.ts +218 -3
  241. package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
  242. package/src/lib/settings/helpers.ts +6 -0
  243. package/src/lib/settings/routing.ts +24 -0
  244. package/src/lib/settings/runtime-setup.ts +28 -1
  245. package/src/lib/usage/ledger.ts +2 -1
  246. package/src/lib/validators/__tests__/settings.test.ts +9 -0
  247. package/src/lib/validators/profile.ts +39 -0
  248. package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
  249. package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
  250. package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
  251. package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
  252. package/src/lib/workflows/blueprints/builtins/lead-research-pipeline.yaml +82 -0
@@ -17,7 +17,9 @@ import { CheckpointList } from "./checkpoint-list";
17
17
  import { TemplateList } from "./template-list";
18
18
  import { HealthScoreCard } from "./health-score-card";
19
19
  import { SuggestedProfiles } from "./suggested-profiles";
20
+ import { ProfileCreateDialog } from "./profile-create-dialog";
20
21
  import type { HealthScore } from "@/lib/environment/health-scoring";
22
+ import type { ProfileSuggestion } from "@/lib/environment/profile-rules";
21
23
  import { DiscoverWorkspaceDialog } from "@/components/workspace/discover-workspace-dialog";
22
24
 
23
25
  interface EnvironmentDashboardProps {
@@ -28,6 +30,7 @@ interface EnvironmentDashboardProps {
28
30
  checkpoints?: EnvironmentCheckpointRow[];
29
31
  templates?: EnvironmentTemplateRow[];
30
32
  healthScore?: HealthScore | null;
33
+ scanPath?: string;
31
34
  }
32
35
 
33
36
  export function EnvironmentDashboard({
@@ -38,6 +41,7 @@ export function EnvironmentDashboard({
38
41
  checkpoints = [],
39
42
  templates = [],
40
43
  healthScore,
44
+ scanPath,
41
45
  }: EnvironmentDashboardProps) {
42
46
  const router = useRouter();
43
47
  const [scanning, setScanning] = useState(false);
@@ -47,16 +51,46 @@ export function EnvironmentDashboard({
47
51
  const [scopeFilter, setScopeFilter] = useState<string | null>(null);
48
52
  const [searchQuery, setSearchQuery] = useState("");
49
53
  const [discoverOpen, setDiscoverOpen] = useState(false);
54
+ const [createProfileSuggestion, setCreateProfileSuggestion] = useState<ProfileSuggestion | null>(null);
55
+
56
+ /** Convert an unlinked skill artifact into a quick suggestion for profile creation. */
57
+ function artifactToSuggestion(artifact: EnvironmentArtifactRow): ProfileSuggestion {
58
+ const name = artifact.name
59
+ .replace(/-/g, " ")
60
+ .replace(/\b\w/g, (c) => c.toUpperCase());
61
+ let description = `Discovered skill: ${artifact.name}`;
62
+ if (artifact.metadata) {
63
+ try {
64
+ const meta = JSON.parse(artifact.metadata);
65
+ if (meta.description) description = meta.description;
66
+ } catch { /* ignore */ }
67
+ }
68
+ return {
69
+ ruleId: `discovered-${artifact.name}`,
70
+ name,
71
+ description,
72
+ confidence: 0.5,
73
+ tier: "discovered",
74
+ matchedArtifacts: [{ id: artifact.id, name: artifact.name, category: artifact.category }],
75
+ suggestedTools: ["Read", "Grep", "Glob", "Bash"],
76
+ systemPrompt: description,
77
+ tags: artifact.name.split("-").filter((t) => t.length > 2),
78
+ };
79
+ }
50
80
 
51
81
  const handleScan = useCallback(async () => {
52
82
  setScanning(true);
53
83
  try {
54
- await fetch("/api/environment/scan", { method: "POST" });
84
+ await fetch("/api/environment/scan", {
85
+ method: "POST",
86
+ headers: { "Content-Type": "application/json" },
87
+ body: JSON.stringify({ projectDir: scanPath }),
88
+ });
55
89
  router.refresh();
56
90
  } finally {
57
91
  setScanning(false);
58
92
  }
59
- }, [router]);
93
+ }, [router, scanPath]);
60
94
 
61
95
  // Filter artifacts client-side
62
96
  const filtered = artifacts.filter((a) => {
@@ -164,6 +198,11 @@ export function EnvironmentDashboard({
164
198
  key={artifact.id}
165
199
  artifact={artifact}
166
200
  onClick={() => setSelectedArtifact(artifact)}
201
+ onCreateProfile={
202
+ artifact.category === "skill" && !artifact.linkedProfileId
203
+ ? () => setCreateProfileSuggestion(artifactToSuggestion(artifact))
204
+ : undefined
205
+ }
167
206
  />
168
207
  ))}
169
208
  </div>
@@ -195,6 +234,15 @@ export function EnvironmentDashboard({
195
234
  onOpenChange={setDiscoverOpen}
196
235
  onComplete={() => router.refresh()}
197
236
  />
237
+
238
+ {/* Profile creation from artifact card's "Create Profile" button */}
239
+ <ProfileCreateDialog
240
+ suggestion={createProfileSuggestion}
241
+ open={!!createProfileSuggestion}
242
+ onOpenChange={(open) => {
243
+ if (!open) setCreateProfileSuggestion(null);
244
+ }}
245
+ />
198
246
  </div>
199
247
  );
200
248
  }
@@ -36,12 +36,15 @@ export function EnvironmentSummaryCard({
36
36
  const [scanning, setScanning] = useState(false);
37
37
 
38
38
  useEffect(() => {
39
- fetch(`/api/environment/scan?projectId=${projectId}`)
39
+ // Auto-scan on mount: pass workingDirectory so the server can rescan if stale
40
+ const params = new URLSearchParams({ projectId });
41
+ if (workingDirectory) params.set("projectDir", workingDirectory);
42
+ fetch(`/api/environment/scan?${params}`)
40
43
  .then((res) => res.json())
41
44
  .then((json) => setData(json))
42
45
  .catch(() => setData(null))
43
46
  .finally(() => setLoading(false));
44
- }, [projectId]);
47
+ }, [projectId, workingDirectory]);
45
48
 
46
49
  const handleScan = async () => {
47
50
  if (!workingDirectory) return;
@@ -1,8 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { useState, useEffect } from "react";
4
- import { useRouter } from "next/navigation";
5
- import { Bot, Sparkles } from "lucide-react";
4
+ import { Bot, Sparkles, ChevronDown, ChevronRight } from "lucide-react";
6
5
  import { Card, CardContent } from "@/components/ui/card";
7
6
  import { Badge } from "@/components/ui/badge";
8
7
  import { Button } from "@/components/ui/button";
@@ -13,22 +12,91 @@ interface SuggestedProfilesProps {
13
12
  scanId?: string;
14
13
  }
15
14
 
15
+ function SuggestionCard({
16
+ suggestion,
17
+ onSelect,
18
+ }: {
19
+ suggestion: ProfileSuggestion;
20
+ onSelect: () => void;
21
+ }) {
22
+ return (
23
+ <Card className="elevation-1 hover:border-primary/40 transition-colors">
24
+ <CardContent className="p-4 space-y-2.5">
25
+ <div className="flex items-start justify-between">
26
+ <div className="flex items-center gap-2">
27
+ <Bot className="h-4 w-4 text-primary" />
28
+ <span className="text-sm font-medium">{suggestion.name}</span>
29
+ </div>
30
+ <div className="flex items-center gap-1.5 shrink-0">
31
+ {suggestion.tier === "discovered" && (
32
+ <Badge variant="secondary" className="text-[10px]">
33
+ Discovered
34
+ </Badge>
35
+ )}
36
+ <Badge variant="outline" className="text-[10px]">
37
+ {Math.round(suggestion.confidence * 100)}%
38
+ </Badge>
39
+ </div>
40
+ </div>
41
+
42
+ <p className="text-xs text-muted-foreground line-clamp-2">
43
+ {suggestion.description}
44
+ </p>
45
+
46
+ <div className="flex flex-wrap gap-1">
47
+ {suggestion.matchedArtifacts.map((a, i) => (
48
+ <Badge
49
+ key={`${a.id}-${i}`}
50
+ variant="secondary"
51
+ className="text-[10px] px-1.5 py-0"
52
+ >
53
+ {a.name}
54
+ </Badge>
55
+ ))}
56
+ </div>
57
+
58
+ <Button
59
+ variant="outline"
60
+ size="sm"
61
+ className="w-full"
62
+ onClick={onSelect}
63
+ >
64
+ Create Profile
65
+ </Button>
66
+ </CardContent>
67
+ </Card>
68
+ );
69
+ }
70
+
16
71
  export function SuggestedProfiles({ scanId }: SuggestedProfilesProps) {
17
- const [suggestions, setSuggestions] = useState<ProfileSuggestion[]>([]);
72
+ const [curated, setCurated] = useState<ProfileSuggestion[]>([]);
73
+ const [discovered, setDiscovered] = useState<ProfileSuggestion[]>([]);
18
74
  const [loading, setLoading] = useState(true);
19
- const [selectedSuggestion, setSelectedSuggestion] = useState<ProfileSuggestion | null>(null);
75
+ const [selectedSuggestion, setSelectedSuggestion] =
76
+ useState<ProfileSuggestion | null>(null);
77
+ const [discoveredExpanded, setDiscoveredExpanded] = useState(false);
20
78
 
21
79
  useEffect(() => {
22
- if (!scanId) { setLoading(false); return; }
80
+ if (!scanId) {
81
+ setLoading(false);
82
+ return;
83
+ }
23
84
 
24
- fetch(`/api/environment/profiles/suggest?scanId=${scanId}`)
85
+ fetch(`/api/environment/profiles/suggest?scanId=${scanId}&tiered=true`)
25
86
  .then((res) => res.json())
26
- .then((data) => setSuggestions(data.suggestions || []))
27
- .catch(() => setSuggestions([]))
87
+ .then((data) => {
88
+ setCurated(data.curated || []);
89
+ setDiscovered(data.discovered || []);
90
+ })
91
+ .catch(() => {
92
+ setCurated([]);
93
+ setDiscovered([]);
94
+ })
28
95
  .finally(() => setLoading(false));
29
96
  }, [scanId]);
30
97
 
31
- if (loading || suggestions.length === 0) return null;
98
+ const totalCount = curated.length + discovered.length;
99
+ if (loading || totalCount === 0) return null;
32
100
 
33
101
  return (
34
102
  <div className="space-y-3">
@@ -36,54 +104,51 @@ export function SuggestedProfiles({ scanId }: SuggestedProfilesProps) {
36
104
  <Sparkles className="h-4 w-4 text-primary" />
37
105
  <h3 className="text-sm font-medium">Suggested Profiles</h3>
38
106
  <Badge variant="secondary" className="text-[10px]">
39
- {suggestions.length} suggestion{suggestions.length !== 1 ? "s" : ""}
107
+ {totalCount} suggestion{totalCount !== 1 ? "s" : ""}
40
108
  </Badge>
41
109
  </div>
42
110
 
43
- <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
44
- {suggestions.map((suggestion) => (
45
- <Card
46
- key={suggestion.ruleId}
47
- className="elevation-1 hover:border-primary/40 transition-colors"
48
- >
49
- <CardContent className="p-4 space-y-2.5">
50
- <div className="flex items-start justify-between">
51
- <div className="flex items-center gap-2">
52
- <Bot className="h-4 w-4 text-primary" />
53
- <span className="text-sm font-medium">{suggestion.name}</span>
54
- </div>
55
- <Badge
56
- variant="outline"
57
- className="text-[10px] shrink-0"
58
- >
59
- {Math.round(suggestion.confidence * 100)}%
60
- </Badge>
61
- </div>
62
-
63
- <p className="text-xs text-muted-foreground line-clamp-2">
64
- {suggestion.description}
65
- </p>
111
+ {/* Tier 1: Curated suggestions */}
112
+ {curated.length > 0 && (
113
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
114
+ {curated.map((suggestion) => (
115
+ <SuggestionCard
116
+ key={suggestion.ruleId}
117
+ suggestion={suggestion}
118
+ onSelect={() => setSelectedSuggestion(suggestion)}
119
+ />
120
+ ))}
121
+ </div>
122
+ )}
66
123
 
67
- <div className="flex flex-wrap gap-1">
68
- {suggestion.matchedArtifacts.map((a, i) => (
69
- <Badge key={`${a.id}-${i}`} variant="secondary" className="text-[10px] px-1.5 py-0">
70
- {a.name}
71
- </Badge>
72
- ))}
73
- </div>
124
+ {/* Tier 2: Discovered suggestions (collapsible) */}
125
+ {discovered.length > 0 && (
126
+ <div className="space-y-2">
127
+ <button
128
+ onClick={() => setDiscoveredExpanded(!discoveredExpanded)}
129
+ className="flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
130
+ >
131
+ {discoveredExpanded ? (
132
+ <ChevronDown className="h-3 w-3" />
133
+ ) : (
134
+ <ChevronRight className="h-3 w-3" />
135
+ )}
136
+ {discovered.length} discoverable skill{discovered.length !== 1 ? "s" : ""}
137
+ </button>
74
138
 
75
- <Button
76
- variant="outline"
77
- size="sm"
78
- className="w-full"
79
- onClick={() => setSelectedSuggestion(suggestion)}
80
- >
81
- Create Profile
82
- </Button>
83
- </CardContent>
84
- </Card>
85
- ))}
86
- </div>
139
+ {discoveredExpanded && (
140
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
141
+ {discovered.map((suggestion) => (
142
+ <SuggestionCard
143
+ key={suggestion.ruleId}
144
+ suggestion={suggestion}
145
+ onSelect={() => setSelectedSuggestion(suggestion)}
146
+ />
147
+ ))}
148
+ </div>
149
+ )}
150
+ </div>
151
+ )}
87
152
 
88
153
  <ProfileCreateDialog
89
154
  suggestion={selectedSuggestion}
@@ -0,0 +1,159 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import {
5
+ ArrowRight,
6
+ CheckCircle2,
7
+ XCircle,
8
+ Clock,
9
+ AlertTriangle,
10
+ } from "lucide-react";
11
+ import { toast } from "sonner";
12
+ import { Button } from "@/components/ui/button";
13
+ import { Badge } from "@/components/ui/badge";
14
+
15
+ interface HandoffApprovalCardProps {
16
+ id: string;
17
+ fromProfileId: string;
18
+ toProfileId: string;
19
+ subject: string;
20
+ body: string;
21
+ priority: number;
22
+ chainDepth: number;
23
+ status: string;
24
+ requiresApproval: boolean;
25
+ onActionComplete?: () => void;
26
+ }
27
+
28
+ const PRIORITY_LABELS: Record<number, string> = {
29
+ 0: "Critical",
30
+ 1: "High",
31
+ 2: "Medium",
32
+ 3: "Low",
33
+ };
34
+
35
+ const PRIORITY_VARIANTS: Record<number, "destructive" | "default" | "secondary" | "outline"> = {
36
+ 0: "destructive",
37
+ 1: "default",
38
+ 2: "secondary",
39
+ 3: "outline",
40
+ };
41
+
42
+ const STATUS_ICONS: Record<string, typeof Clock> = {
43
+ pending: Clock,
44
+ accepted: CheckCircle2,
45
+ in_progress: Clock,
46
+ completed: CheckCircle2,
47
+ rejected: XCircle,
48
+ expired: AlertTriangle,
49
+ };
50
+
51
+ const STATUS_VARIANTS: Record<string, "default" | "secondary" | "destructive" | "outline"> = {
52
+ pending: "outline",
53
+ accepted: "default",
54
+ in_progress: "default",
55
+ completed: "secondary",
56
+ rejected: "destructive",
57
+ expired: "outline",
58
+ };
59
+
60
+ export function HandoffApprovalCard({
61
+ id,
62
+ fromProfileId,
63
+ toProfileId,
64
+ subject,
65
+ body,
66
+ priority,
67
+ chainDepth,
68
+ status,
69
+ requiresApproval,
70
+ onActionComplete,
71
+ }: HandoffApprovalCardProps) {
72
+ const [acting, setActing] = useState(false);
73
+
74
+ const StatusIcon = STATUS_ICONS[status] ?? Clock;
75
+
76
+ const handleAction = async (action: "approve" | "reject") => {
77
+ setActing(true);
78
+ try {
79
+ const res = await fetch(`/api/handoffs/${id}`, {
80
+ method: "PATCH",
81
+ headers: { "Content-Type": "application/json" },
82
+ body: JSON.stringify({ action, approvedBy: "user" }),
83
+ });
84
+
85
+ if (res.ok) {
86
+ toast.success(`Handoff ${action === "approve" ? "approved" : "rejected"}`);
87
+ onActionComplete?.();
88
+ } else {
89
+ const data = await res.json();
90
+ toast.error(data.error ?? `Failed to ${action} handoff`);
91
+ }
92
+ } catch {
93
+ toast.error(`Failed to ${action} handoff`);
94
+ } finally {
95
+ setActing(false);
96
+ }
97
+ };
98
+
99
+ return (
100
+ <div className="rounded-lg border p-4 space-y-3">
101
+ <div className="flex items-start justify-between">
102
+ <div className="space-y-1">
103
+ <h4 className="text-sm font-medium">{subject}</h4>
104
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
105
+ <span className="font-mono">{fromProfileId}</span>
106
+ <ArrowRight className="h-3 w-3" />
107
+ <span className="font-mono">{toProfileId}</span>
108
+ </div>
109
+ </div>
110
+ <div className="flex items-center gap-2">
111
+ <Badge variant={PRIORITY_VARIANTS[priority] ?? "secondary"}>
112
+ {PRIORITY_LABELS[priority] ?? "Medium"}
113
+ </Badge>
114
+ <Badge variant={STATUS_VARIANTS[status] ?? "outline"}>
115
+ <StatusIcon className="mr-1 h-3 w-3" />
116
+ {status}
117
+ </Badge>
118
+ </div>
119
+ </div>
120
+
121
+ <p className="text-sm text-muted-foreground line-clamp-3">{body}</p>
122
+
123
+ <div className="flex items-center justify-between">
124
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
125
+ {chainDepth > 0 && (
126
+ <span>Chain depth: {chainDepth}</span>
127
+ )}
128
+ {requiresApproval && status === "pending" && (
129
+ <Badge variant="outline" className="text-xs">
130
+ Awaiting approval
131
+ </Badge>
132
+ )}
133
+ </div>
134
+
135
+ {status === "pending" && requiresApproval && (
136
+ <div className="flex items-center gap-2">
137
+ <Button
138
+ variant="outline"
139
+ size="sm"
140
+ onClick={() => handleAction("reject")}
141
+ disabled={acting}
142
+ >
143
+ <XCircle className="mr-1 h-3.5 w-3.5" />
144
+ Reject
145
+ </Button>
146
+ <Button
147
+ size="sm"
148
+ onClick={() => handleAction("approve")}
149
+ disabled={acting}
150
+ >
151
+ <CheckCircle2 className="mr-1 h-3.5 w-3.5" />
152
+ Approve
153
+ </Button>
154
+ </div>
155
+ )}
156
+ </div>
157
+ </div>
158
+ );
159
+ }