stagent 0.5.0 → 0.6.1

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 (256) 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 +104 -0
  38. package/src/app/api/channels/[id]/test/route.ts +52 -0
  39. package/src/app/api/channels/inbound/slack/route.ts +116 -0
  40. package/src/app/api/channels/inbound/telegram/poll/route.ts +140 -0
  41. package/src/app/api/channels/inbound/telegram/route.ts +87 -0
  42. package/src/app/api/channels/route.ts +72 -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/data/clear/route.ts +4 -0
  46. package/src/app/api/data/seed/route.ts +4 -0
  47. package/src/app/api/documents/route.ts +36 -6
  48. package/src/app/api/environment/profiles/suggest/route.ts +19 -3
  49. package/src/app/api/environment/scan/route.ts +8 -1
  50. package/src/app/api/handoffs/[id]/route.ts +76 -0
  51. package/src/app/api/handoffs/route.ts +89 -0
  52. package/src/app/api/memory/route.ts +181 -0
  53. package/src/app/api/profiles/[id]/route.ts +16 -1
  54. package/src/app/api/profiles/[id]/test/route.ts +4 -0
  55. package/src/app/api/profiles/[id]/test-results/route.ts +22 -0
  56. package/src/app/api/profiles/[id]/test-single/route.ts +64 -0
  57. package/src/app/api/profiles/assist/route.ts +35 -0
  58. package/src/app/api/profiles/import-repo/apply-updates/route.ts +123 -0
  59. package/src/app/api/profiles/import-repo/check-updates/route.ts +163 -0
  60. package/src/app/api/profiles/import-repo/confirm/route.ts +118 -0
  61. package/src/app/api/profiles/import-repo/preview/route.ts +107 -0
  62. package/src/app/api/profiles/import-repo/route.ts +29 -0
  63. package/src/app/api/profiles/import-repo/scan/route.ts +25 -0
  64. package/src/app/api/profiles/route.ts +73 -22
  65. package/src/app/api/runtimes/ollama/route.ts +86 -0
  66. package/src/app/api/runtimes/suggest/route.ts +29 -0
  67. package/src/app/api/schedules/[id]/heartbeat-history/route.ts +77 -0
  68. package/src/app/api/schedules/[id]/route.ts +41 -3
  69. package/src/app/api/schedules/parse/route.ts +66 -0
  70. package/src/app/api/schedules/route.ts +71 -12
  71. package/src/app/api/settings/author-default/route.ts +7 -0
  72. package/src/app/api/settings/learning/route.ts +41 -0
  73. package/src/app/api/settings/ollama/route.ts +34 -0
  74. package/src/app/api/settings/providers/route.ts +57 -0
  75. package/src/app/api/settings/routing/route.ts +24 -0
  76. package/src/app/api/settings/web-search/route.ts +28 -0
  77. package/src/app/api/tasks/[id]/execute/route.ts +13 -1
  78. package/src/app/api/tasks/[id]/respond/route.ts +23 -1
  79. package/src/app/documents/page.tsx +3 -0
  80. package/src/app/environment/page.tsx +8 -1
  81. package/src/app/settings/page.tsx +10 -4
  82. package/src/app/workflows/[id]/edit/page.tsx +2 -0
  83. package/src/app/workflows/new/page.tsx +2 -0
  84. package/src/components/chat/chat-command-popover.tsx +22 -19
  85. package/src/components/chat/chat-input.tsx +5 -0
  86. package/src/components/chat/chat-model-selector.tsx +42 -1
  87. package/src/components/chat/chat-shell.tsx +2 -0
  88. package/src/components/dashboard/welcome-landing.tsx +9 -9
  89. package/src/components/environment/artifact-card.tsx +27 -1
  90. package/src/components/environment/environment-dashboard.tsx +50 -2
  91. package/src/components/environment/environment-summary-card.tsx +5 -2
  92. package/src/components/environment/suggested-profiles.tsx +117 -52
  93. package/src/components/handoffs/handoff-approval-card.tsx +159 -0
  94. package/src/components/memory/memory-browser.tsx +315 -0
  95. package/src/components/profiles/learned-context-panel.tsx +4 -4
  96. package/src/components/profiles/profile-assist-panel.tsx +512 -0
  97. package/src/components/profiles/profile-browser.tsx +109 -8
  98. package/src/components/profiles/profile-card.tsx +29 -1
  99. package/src/components/profiles/profile-detail-view.tsx +200 -28
  100. package/src/components/profiles/profile-form-view.tsx +220 -82
  101. package/src/components/profiles/repo-import-wizard.tsx +648 -0
  102. package/src/components/profiles/smoke-test-editor.tsx +106 -0
  103. package/src/components/schedules/schedule-create-sheet.tsx +9 -1
  104. package/src/components/schedules/schedule-form.tsx +348 -9
  105. package/src/components/schedules/schedule-list.tsx +15 -2
  106. package/src/components/settings/auth-method-selector.tsx +7 -1
  107. package/src/components/settings/budget-guardrails-section.tsx +111 -48
  108. package/src/components/settings/channels-section.tsx +526 -0
  109. package/src/components/settings/chat-settings-section.tsx +27 -1
  110. package/src/components/settings/data-management-section.tsx +8 -6
  111. package/src/components/settings/learning-context-section.tsx +124 -0
  112. package/src/components/settings/ollama-section.tsx +270 -0
  113. package/src/components/settings/providers-runtimes-section.tsx +499 -0
  114. package/src/components/settings/web-search-section.tsx +101 -0
  115. package/src/components/shared/tag-input.tsx +156 -0
  116. package/src/components/tasks/kanban-board.tsx +32 -0
  117. package/src/components/tasks/kanban-column.tsx +4 -2
  118. package/src/components/tasks/task-card.tsx +1 -0
  119. package/src/components/tasks/task-chip-bar.tsx +6 -1
  120. package/src/components/tasks/task-create-panel.tsx +55 -5
  121. package/src/components/workflows/workflow-form-view.tsx +38 -3
  122. package/src/hooks/use-chat-autocomplete.ts +24 -26
  123. package/src/hooks/use-project-skills.ts +66 -0
  124. package/src/hooks/use-tag-suggestions.ts +31 -0
  125. package/src/instrumentation.ts +4 -1
  126. package/src/lib/agents/__tests__/claude-agent.test.ts +3 -0
  127. package/src/lib/agents/__tests__/learned-context.test.ts +10 -0
  128. package/src/lib/agents/agentic-loop.ts +235 -0
  129. package/src/lib/agents/browser-mcp.ts +59 -4
  130. package/src/lib/agents/claude-agent.ts +27 -200
  131. package/src/lib/agents/handoff/bus.ts +164 -0
  132. package/src/lib/agents/handoff/governance.ts +47 -0
  133. package/src/lib/agents/handoff/types.ts +16 -0
  134. package/src/lib/agents/learned-context.ts +27 -7
  135. package/src/lib/agents/memory/decay.ts +61 -0
  136. package/src/lib/agents/memory/extractor.ts +181 -0
  137. package/src/lib/agents/memory/retrieval.ts +96 -0
  138. package/src/lib/agents/memory/types.ts +6 -0
  139. package/src/lib/agents/profiles/__tests__/project-profiles.test.ts +119 -0
  140. package/src/lib/agents/profiles/__tests__/registry.test.ts +11 -3
  141. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +2 -2
  142. package/src/lib/agents/profiles/builtins/content-creator/SKILL.md +19 -0
  143. package/src/lib/agents/profiles/builtins/content-creator/profile.yaml +27 -0
  144. package/src/lib/agents/profiles/builtins/customer-support-agent/SKILL.md +19 -0
  145. package/src/lib/agents/profiles/builtins/customer-support-agent/profile.yaml +26 -0
  146. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +2 -2
  147. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +2 -2
  148. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +2 -2
  149. package/src/lib/agents/profiles/builtins/financial-analyst/SKILL.md +19 -0
  150. package/src/lib/agents/profiles/builtins/financial-analyst/profile.yaml +24 -0
  151. package/src/lib/agents/profiles/builtins/general/profile.yaml +2 -2
  152. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +2 -2
  153. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +2 -2
  154. package/src/lib/agents/profiles/builtins/marketing-strategist/SKILL.md +19 -0
  155. package/src/lib/agents/profiles/builtins/marketing-strategist/profile.yaml +27 -0
  156. package/src/lib/agents/profiles/builtins/operations-coordinator/SKILL.md +19 -0
  157. package/src/lib/agents/profiles/builtins/operations-coordinator/profile.yaml +26 -0
  158. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +2 -2
  159. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +1 -0
  160. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +2 -2
  161. package/src/lib/agents/profiles/builtins/sales-researcher/SKILL.md +19 -0
  162. package/src/lib/agents/profiles/builtins/sales-researcher/profile.yaml +26 -0
  163. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +1 -0
  164. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +2 -2
  165. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +1 -1
  166. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +2 -2
  167. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +2 -0
  168. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +2 -2
  169. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +2 -0
  170. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +2 -2
  171. package/src/lib/agents/profiles/project-profiles.ts +193 -0
  172. package/src/lib/agents/profiles/registry.ts +130 -6
  173. package/src/lib/agents/profiles/types.ts +28 -0
  174. package/src/lib/agents/router.ts +174 -2
  175. package/src/lib/agents/runtime/__tests__/catalog.test.ts +15 -4
  176. package/src/lib/agents/runtime/anthropic-direct.ts +644 -0
  177. package/src/lib/agents/runtime/catalog.ts +57 -2
  178. package/src/lib/agents/runtime/claude.ts +205 -1
  179. package/src/lib/agents/runtime/index.ts +22 -0
  180. package/src/lib/agents/runtime/ollama-adapter.ts +409 -0
  181. package/src/lib/agents/runtime/openai-direct.ts +514 -0
  182. package/src/lib/agents/runtime/profile-assist-types.ts +30 -0
  183. package/src/lib/agents/runtime/types.ts +2 -0
  184. package/src/lib/agents/tool-permissions.ts +203 -0
  185. package/src/lib/channels/gateway.ts +321 -0
  186. package/src/lib/channels/poller.ts +268 -0
  187. package/src/lib/channels/registry.ts +90 -0
  188. package/src/lib/channels/slack-adapter.ts +188 -0
  189. package/src/lib/channels/telegram-adapter.ts +218 -0
  190. package/src/lib/channels/types.ts +75 -0
  191. package/src/lib/channels/webhook-adapter.ts +74 -0
  192. package/src/lib/chat/context-builder.ts +22 -2
  193. package/src/lib/chat/engine.ts +95 -13
  194. package/src/lib/chat/ollama-engine.ts +198 -0
  195. package/src/lib/chat/stagent-tools.ts +106 -20
  196. package/src/lib/chat/tool-catalog.ts +24 -0
  197. package/src/lib/chat/tool-registry.ts +90 -0
  198. package/src/lib/chat/tools/chat-history-tools.ts +4 -4
  199. package/src/lib/chat/tools/document-tools.ts +7 -7
  200. package/src/lib/chat/tools/handoff-tools.ts +70 -0
  201. package/src/lib/chat/tools/notification-tools.ts +4 -4
  202. package/src/lib/chat/tools/profile-tools.ts +3 -3
  203. package/src/lib/chat/tools/project-tools.ts +3 -3
  204. package/src/lib/chat/tools/schedule-tools.ts +29 -13
  205. package/src/lib/chat/tools/settings-tools.ts +2 -2
  206. package/src/lib/chat/tools/task-tools.ts +66 -11
  207. package/src/lib/chat/tools/usage-tools.ts +2 -2
  208. package/src/lib/chat/tools/workflow-tools.ts +8 -8
  209. package/src/lib/chat/types.ts +11 -5
  210. package/src/lib/constants/known-tools.ts +19 -0
  211. package/src/lib/constants/prose-styles.ts +1 -1
  212. package/src/lib/constants/settings.ts +7 -0
  213. package/src/lib/data/channel-bindings.ts +85 -0
  214. package/src/lib/data/clear.ts +22 -0
  215. package/src/lib/data/profile-test-results.ts +48 -0
  216. package/src/lib/data/seed-data/conversations.ts +196 -0
  217. package/src/lib/data/seed-data/learned-context.ts +99 -0
  218. package/src/lib/data/seed-data/notifications.ts +54 -1
  219. package/src/lib/data/seed-data/profile-test-results.ts +96 -0
  220. package/src/lib/data/seed-data/repo-imports.ts +51 -0
  221. package/src/lib/data/seed-data/views.ts +60 -0
  222. package/src/lib/data/seed.ts +51 -0
  223. package/src/lib/db/bootstrap.ts +162 -0
  224. package/src/lib/db/migrations/0013_add_repo_imports.sql +15 -0
  225. package/src/lib/db/migrations/0014_add_linked_profile_id.sql +3 -0
  226. package/src/lib/db/migrations/0015_add_channel_bindings.sql +23 -0
  227. package/src/lib/db/schema.ts +190 -1
  228. package/src/lib/environment/__tests__/auto-scan.test.ts +86 -0
  229. package/src/lib/environment/__tests__/profile-linker.test.ts +187 -0
  230. package/src/lib/environment/auto-scan.ts +48 -0
  231. package/src/lib/environment/data.ts +25 -0
  232. package/src/lib/environment/profile-generator.ts +40 -10
  233. package/src/lib/environment/profile-linker.ts +143 -0
  234. package/src/lib/environment/profile-rules.ts +96 -0
  235. package/src/lib/import/dedup.ts +149 -0
  236. package/src/lib/import/format-adapter.ts +631 -0
  237. package/src/lib/import/github-api.ts +219 -0
  238. package/src/lib/import/repo-scanner.ts +251 -0
  239. package/src/lib/schedules/__tests__/nlp-parser.test.ts +330 -0
  240. package/src/lib/schedules/active-hours.ts +120 -0
  241. package/src/lib/schedules/heartbeat-parser.ts +224 -0
  242. package/src/lib/schedules/heartbeat-prompt.ts +153 -0
  243. package/src/lib/schedules/nlp-parser.ts +357 -0
  244. package/src/lib/schedules/scheduler.ts +218 -3
  245. package/src/lib/settings/__tests__/budget-guardrails.test.ts +39 -1
  246. package/src/lib/settings/helpers.ts +6 -0
  247. package/src/lib/settings/routing.ts +24 -0
  248. package/src/lib/settings/runtime-setup.ts +28 -1
  249. package/src/lib/usage/ledger.ts +2 -1
  250. package/src/lib/validators/__tests__/settings.test.ts +9 -0
  251. package/src/lib/validators/profile.ts +39 -0
  252. package/src/lib/workflows/blueprints/builtins/business-daily-briefing.yaml +102 -0
  253. package/src/lib/workflows/blueprints/builtins/content-marketing-pipeline.yaml +90 -0
  254. package/src/lib/workflows/blueprints/builtins/customer-support-triage.yaml +107 -0
  255. package/src/lib/workflows/blueprints/builtins/financial-reporting.yaml +104 -0
  256. package/src/lib/workflows/blueprints/builtins/lead-research-pipeline.yaml +82 -0
@@ -21,7 +21,7 @@ import {
21
21
  } from "lucide-react";
22
22
  import type { LucideIcon } from "lucide-react";
23
23
  import {
24
- getToolCatalog,
24
+ getToolCatalogWithSkills,
25
25
  groupToolCatalog,
26
26
  TOOL_GROUP_ICONS,
27
27
  TOOL_GROUP_ORDER,
@@ -36,6 +36,7 @@ interface ChatCommandPopoverProps {
36
36
  anchorRect: { top: number; left: number; height: number } | null;
37
37
  entityResults: EntitySearchResult[];
38
38
  entityLoading: boolean;
39
+ projectProfiles?: Array<{ id: string; name: string; description: string }>;
39
40
  onSelect: (item: {
40
41
  type: "slash" | "mention";
41
42
  id: string;
@@ -81,6 +82,7 @@ export function ChatCommandPopover({
81
82
  anchorRect,
82
83
  entityResults,
83
84
  entityLoading,
85
+ projectProfiles,
84
86
  onSelect,
85
87
  onClose,
86
88
  }: ChatCommandPopoverProps) {
@@ -116,7 +118,7 @@ export function ChatCommandPopover({
116
118
  data-chat-autocomplete=""
117
119
  className="rounded-lg border bg-popover text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95 slide-in-from-bottom-2"
118
120
  >
119
- <Command shouldFilter={mode === "slash"} loop>
121
+ <Command shouldFilter loop>
120
122
  {/* Hidden input for cmdk filtering — synced to query */}
121
123
  <div className="sr-only">
122
124
  <CommandInput value={query} />
@@ -127,13 +129,17 @@ export function ChatCommandPopover({
127
129
  {mode === "slash" ? "No matching tools" : "No matching entities"}
128
130
  </CommandEmpty>
129
131
 
130
- {mode === "slash" && <ToolCatalogItems onSelect={onSelect} />}
132
+ {mode === "slash" && (
133
+ <ToolCatalogItems
134
+ onSelect={onSelect}
135
+ projectProfiles={projectProfiles}
136
+ />
137
+ )}
131
138
 
132
139
  {mode === "mention" && (
133
140
  <MentionItems
134
141
  results={entityResults}
135
142
  loading={entityLoading}
136
- query={query}
137
143
  onSelect={onSelect}
138
144
  />
139
145
  )}
@@ -147,10 +153,15 @@ export function ChatCommandPopover({
147
153
 
148
154
  function ToolCatalogItems({
149
155
  onSelect,
156
+ projectProfiles,
150
157
  }: {
151
158
  onSelect: ChatCommandPopoverProps["onSelect"];
159
+ projectProfiles?: ChatCommandPopoverProps["projectProfiles"];
152
160
  }) {
153
- const catalog = getToolCatalog({ includeBrowser: true });
161
+ const catalog = getToolCatalogWithSkills({
162
+ includeBrowser: true,
163
+ projectProfiles,
164
+ });
154
165
  const groups = groupToolCatalog(catalog);
155
166
 
156
167
  return (
@@ -172,7 +183,9 @@ function ToolCatalogItems({
172
183
  label: entry.name,
173
184
  text: entry.behavior === "execute_immediately"
174
185
  ? entry.name
175
- : `Use ${entry.name} to `,
186
+ : entry.group === "Skills"
187
+ ? `Use the ${entry.name} profile: `
188
+ : `Use ${entry.name} to `,
176
189
  })
177
190
  }
178
191
  >
@@ -200,27 +213,17 @@ function ToolCatalogItems({
200
213
  function MentionItems({
201
214
  results,
202
215
  loading,
203
- query,
204
216
  onSelect,
205
217
  }: {
206
218
  results: EntitySearchResult[];
207
219
  loading: boolean;
208
- query: string;
209
220
  onSelect: ChatCommandPopoverProps["onSelect"];
210
221
  }) {
211
222
  if (loading && results.length === 0) {
212
223
  return (
213
224
  <div className="flex items-center gap-2 px-3 py-4 text-sm text-muted-foreground">
214
225
  <Loader2 className="h-4 w-4 animate-spin" />
215
- Searching...
216
- </div>
217
- );
218
- }
219
-
220
- if (!query) {
221
- return (
222
- <div className="px-3 py-4 text-sm text-muted-foreground text-center">
223
- Type to search entities...
226
+ Loading...
224
227
  </div>
225
228
  );
226
229
  }
@@ -228,7 +231,7 @@ function MentionItems({
228
231
  const grouped = groupByType(results);
229
232
  const entityTypes = Object.keys(grouped);
230
233
 
231
- if (entityTypes.length === 0 && !loading) {
234
+ if (entityTypes.length === 0) {
232
235
  return null; // CommandEmpty will show
233
236
  }
234
237
 
@@ -242,7 +245,7 @@ function MentionItems({
242
245
  {grouped[type].map((entity) => (
243
246
  <CommandItem
244
247
  key={`${entity.entityType}-${entity.entityId}`}
245
- value={`${entity.entityType} ${entity.label}`}
248
+ value={`${entity.entityType} ${entity.label} ${entity.description ?? ""} ${entity.status ?? ""}`}
246
249
  onSelect={() =>
247
250
  onSelect({
248
251
  type: "mention",
@@ -8,6 +8,7 @@ import { ChatModelSelector } from "./chat-model-selector";
8
8
  import { ChatCommandPopover } from "./chat-command-popover";
9
9
  import { useChatAutocomplete, type MentionReference } from "@/hooks/use-chat-autocomplete";
10
10
  import { getToolCatalog } from "@/lib/chat/tool-catalog";
11
+ import { useProjectSkills } from "@/hooks/use-project-skills";
11
12
  import type { ChatModelOption } from "@/lib/chat/types";
12
13
 
13
14
  interface ChatInputProps {
@@ -19,6 +20,7 @@ interface ChatInputProps {
19
20
  modelId?: string;
20
21
  onModelChange?: (modelId: string) => void;
21
22
  availableModels?: ChatModelOption[];
23
+ projectId?: string | null;
22
24
  }
23
25
 
24
26
  export function ChatInput({
@@ -30,10 +32,12 @@ export function ChatInput({
30
32
  modelId,
31
33
  onModelChange,
32
34
  availableModels,
35
+ projectId,
33
36
  }: ChatInputProps) {
34
37
  const [value, setValue] = useState("");
35
38
  const textareaRef = useRef<HTMLTextAreaElement>(null);
36
39
  const autocomplete = useChatAutocomplete();
40
+ const { skills: projectSkills } = useProjectSkills(projectId);
37
41
 
38
42
  // Sync textarea ref with autocomplete hook
39
43
  useEffect(() => {
@@ -206,6 +210,7 @@ export function ChatInput({
206
210
  anchorRect={autocomplete.state.anchorRect}
207
211
  entityResults={autocomplete.entityResults}
208
212
  entityLoading={autocomplete.entityLoading}
213
+ projectProfiles={projectSkills.length > 0 ? projectSkills : undefined}
209
214
  onSelect={handlePopoverSelect}
210
215
  onClose={autocomplete.close}
211
216
  />
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
 
3
+ import { useEffect, useState } from "react";
3
4
  import { CHAT_MODELS, DEFAULT_CHAT_MODEL, type ChatModelOption } from "@/lib/chat/types";
4
5
  import { Button } from "@/components/ui/button";
5
6
  import {
@@ -24,6 +25,7 @@ const tierEmoji: Record<string, string> = {
24
25
  Fast: "\u26A1",
25
26
  Balanced: "\u2728",
26
27
  Best: "\uD83D\uDC8E",
28
+ Local: "\uD83C\uDFE0",
27
29
  };
28
30
 
29
31
  export function ChatModelSelector({
@@ -31,7 +33,27 @@ export function ChatModelSelector({
31
33
  onModelChange,
32
34
  models = CHAT_MODELS,
33
35
  }: ChatModelSelectorProps) {
34
- const current = models.find((m) => m.id === modelId) ?? models[0] ?? { id: modelId, label: modelId, provider: "anthropic" as const, tier: "Balanced", costLabel: "$$" };
36
+ const [ollamaModels, setOllamaModels] = useState<ChatModelOption[]>([]);
37
+
38
+ // Fetch available Ollama models on mount
39
+ useEffect(() => {
40
+ fetch("/api/runtimes/ollama")
41
+ .then((r) => (r.ok ? r.json() : { models: [] }))
42
+ .then((data: { models?: Array<{ name: string; details?: { parameter_size?: string } }> }) => {
43
+ const models = (data.models ?? []).map((m) => ({
44
+ id: `ollama:${m.name}`,
45
+ label: m.name.replace(/:latest$/, ""),
46
+ provider: "ollama" as const,
47
+ tier: "Local",
48
+ costLabel: "Free",
49
+ }));
50
+ setOllamaModels(models);
51
+ })
52
+ .catch(() => {});
53
+ }, []);
54
+
55
+ const allModels = [...models, ...ollamaModels];
56
+ const current = allModels.find((m) => m.id === modelId) ?? allModels[0] ?? { id: modelId, label: modelId, provider: "anthropic" as const, tier: "Balanced", costLabel: "$$" };
35
57
 
36
58
  const anthropicModels = models.filter(
37
59
  (m) => m.provider === "anthropic"
@@ -83,6 +105,25 @@ export function ChatModelSelector({
83
105
  </DropdownMenuGroup>
84
106
  </>
85
107
  )}
108
+
109
+ {ollamaModels.length > 0 && (
110
+ <>
111
+ <DropdownMenuSeparator />
112
+ <DropdownMenuLabel className="text-xs text-muted-foreground">
113
+ Ollama (Local)
114
+ </DropdownMenuLabel>
115
+ <DropdownMenuGroup>
116
+ {ollamaModels.map((m) => (
117
+ <ModelMenuItem
118
+ key={m.id}
119
+ model={m}
120
+ isSelected={m.id === modelId}
121
+ onSelect={onModelChange}
122
+ />
123
+ ))}
124
+ </DropdownMenuGroup>
125
+ </>
126
+ )}
86
127
  </DropdownMenuContent>
87
128
  </DropdownMenu>
88
129
  );
@@ -515,6 +515,7 @@ export function ChatShell({
515
515
  modelId={modelId}
516
516
  onModelChange={handleModelChange}
517
517
  availableModels={availableModels}
518
+ projectId={activeConversation?.projectId}
518
519
  />
519
520
  </ChatEmptyState>
520
521
  </div>
@@ -539,6 +540,7 @@ export function ChatShell({
539
540
  modelId={modelId}
540
541
  onModelChange={handleModelChange}
541
542
  availableModels={availableModels}
543
+ projectId={activeConversation?.projectId}
542
544
  />
543
545
  </>
544
546
  )}
@@ -1,22 +1,22 @@
1
1
  import Link from "next/link";
2
2
  import { Button } from "@/components/ui/button";
3
- import { Shield, GitBranch, Wallet } from "lucide-react";
3
+ import { Shield, Zap, Wallet } from "lucide-react";
4
4
 
5
5
  const pillars = [
6
6
  {
7
7
  icon: Shield,
8
- title: "Governed Execution",
9
- description: "Every agent action requires explicit approval. Full audit trail for every decision.",
8
+ title: "Your Rules, Enforced",
9
+ description: "Every agent action respects your policies. Full audit trail for every decision.",
10
10
  },
11
11
  {
12
- icon: GitBranch,
13
- title: "Reusable Automation",
14
- description: "Build workflow templates once, run them many times. Profile-aware agent routing.",
12
+ icon: Zap,
13
+ title: "Business on Autopilot",
14
+ description: "Build workflow templates once, run them many times. 21 specialist profiles ready to deploy.",
15
15
  },
16
16
  {
17
17
  icon: Wallet,
18
- title: "Cost & Visibility",
19
- description: "Track spend per task, per runtime. Budget guardrails prevent surprise bills.",
18
+ title: "Know What You Spend",
19
+ description: "Track spend per task, per provider. Budget guardrails prevent surprise bills.",
20
20
  },
21
21
  ];
22
22
 
@@ -31,7 +31,7 @@ export function WelcomeLanding() {
31
31
  Welcome to Stagent
32
32
  </h1>
33
33
  <p className="text-base text-muted-foreground mb-8 max-w-lg">
34
- The governed AI agent operations workspace. Execute tasks, manage workflows, and maintain full oversight of your AI agents.
34
+ Your AI Business Operating System. Deploy AI agents, automate business processes, and maintain full control of spend and execution.
35
35
  </p>
36
36
 
37
37
  <div className="grid grid-cols-1 sm:grid-cols-3 gap-4 w-full mb-8">
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { Card, CardContent } from "@/components/ui/card";
4
4
  import { Badge } from "@/components/ui/badge";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Bot, Link as LinkIcon } from "lucide-react";
5
7
  import type { EnvironmentArtifactRow } from "@/lib/db/schema";
6
8
  import { CATEGORY_META } from "./summary-cards-row";
7
9
  import { PersonaIndicator } from "./persona-indicator";
@@ -9,6 +11,7 @@ import { PersonaIndicator } from "./persona-indicator";
9
11
  interface ArtifactCardProps {
10
12
  artifact: EnvironmentArtifactRow;
11
13
  onClick: () => void;
14
+ onCreateProfile?: () => void;
12
15
  }
13
16
 
14
17
  function formatSize(bytes: number): string {
@@ -17,7 +20,7 @@ function formatSize(bytes: number): string {
17
20
  return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
18
21
  }
19
22
 
20
- export function ArtifactCard({ artifact, onClick }: ArtifactCardProps) {
23
+ export function ArtifactCard({ artifact, onClick, onCreateProfile }: ArtifactCardProps) {
21
24
  const meta = CATEGORY_META[artifact.category];
22
25
  const Icon = meta?.icon;
23
26
 
@@ -57,10 +60,33 @@ export function ArtifactCard({ artifact, onClick }: ArtifactCardProps) {
57
60
  <Badge variant="secondary" className="text-[10px] px-1.5 py-0">
58
61
  {artifact.scope}
59
62
  </Badge>
63
+ {/* Profile linkage indicator for skill artifacts */}
64
+ {artifact.category === "skill" && artifact.linkedProfileId && (
65
+ <Badge variant="default" className="text-[10px] px-1.5 py-0 gap-0.5">
66
+ <LinkIcon className="h-2.5 w-2.5" />
67
+ Profile
68
+ </Badge>
69
+ )}
60
70
  <span className="text-[10px] text-muted-foreground ml-auto">
61
71
  {formatSize(artifact.sizeBytes)}
62
72
  </span>
63
73
  </div>
74
+
75
+ {/* Create Profile button for unlinked skill artifacts */}
76
+ {artifact.category === "skill" && !artifact.linkedProfileId && onCreateProfile && (
77
+ <Button
78
+ variant="outline"
79
+ size="sm"
80
+ className="w-full text-xs gap-1.5"
81
+ onClick={(e) => {
82
+ e.stopPropagation();
83
+ onCreateProfile();
84
+ }}
85
+ >
86
+ <Bot className="h-3 w-3" />
87
+ Create Profile
88
+ </Button>
89
+ )}
64
90
  </CardContent>
65
91
  </Card>
66
92
  );
@@ -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}