stagent 0.1.10 → 0.1.12

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 (112) hide show
  1. package/README.md +58 -27
  2. package/package.json +3 -3
  3. package/src/__tests__/e2e/blueprint.test.ts +63 -0
  4. package/src/__tests__/e2e/cross-runtime.test.ts +77 -0
  5. package/src/__tests__/e2e/helpers.ts +286 -0
  6. package/src/__tests__/e2e/parallel-workflow.test.ts +120 -0
  7. package/src/__tests__/e2e/sequence-workflow.test.ts +109 -0
  8. package/src/__tests__/e2e/setup.ts +156 -0
  9. package/src/__tests__/e2e/single-task.test.ts +170 -0
  10. package/src/app/api/command-palette/recent/route.ts +41 -18
  11. package/src/app/api/context/batch/route.ts +44 -0
  12. package/src/app/api/permissions/presets/route.ts +80 -0
  13. package/src/app/api/playbook/status/route.ts +15 -0
  14. package/src/app/api/profiles/route.ts +23 -21
  15. package/src/app/api/settings/pricing/route.ts +15 -0
  16. package/src/app/costs/page.tsx +53 -43
  17. package/src/app/globals.css +0 -5
  18. package/src/app/playbook/[slug]/page.tsx +76 -0
  19. package/src/app/playbook/page.tsx +54 -0
  20. package/src/app/profiles/page.tsx +7 -4
  21. package/src/app/settings/page.tsx +2 -2
  22. package/src/app/tasks/page.tsx +5 -0
  23. package/src/components/costs/cost-dashboard.tsx +226 -320
  24. package/src/components/dashboard/activity-feed.tsx +6 -2
  25. package/src/components/notifications/batch-proposal-review.tsx +150 -0
  26. package/src/components/notifications/notification-item.tsx +6 -3
  27. package/src/components/notifications/pending-approval-host.tsx +57 -11
  28. package/src/components/playbook/adoption-heatmap.tsx +69 -0
  29. package/src/components/playbook/journey-card.tsx +110 -0
  30. package/src/components/playbook/playbook-action-button.tsx +22 -0
  31. package/src/components/playbook/playbook-browser.tsx +143 -0
  32. package/src/components/playbook/playbook-card.tsx +102 -0
  33. package/src/components/playbook/playbook-detail-view.tsx +223 -0
  34. package/src/components/playbook/playbook-homepage.tsx +142 -0
  35. package/src/components/playbook/playbook-toc.tsx +90 -0
  36. package/src/components/playbook/playbook-updated-badge.tsx +23 -0
  37. package/src/components/playbook/related-docs.tsx +30 -0
  38. package/src/components/profiles/__tests__/learned-context-panel.test.tsx +175 -0
  39. package/src/components/profiles/context-proposal-review.tsx +7 -3
  40. package/src/components/profiles/learned-context-panel.tsx +116 -8
  41. package/src/components/profiles/profile-detail-view.tsx +7 -19
  42. package/src/components/profiles/profile-form-view.tsx +0 -22
  43. package/src/components/settings/__tests__/auth-config-section.test.tsx +147 -0
  44. package/src/components/settings/api-key-form.tsx +5 -43
  45. package/src/components/settings/auth-config-section.tsx +10 -6
  46. package/src/components/settings/auth-status-badge.tsx +8 -0
  47. package/src/components/settings/budget-guardrails-section.tsx +403 -620
  48. package/src/components/settings/connection-test-control.tsx +63 -0
  49. package/src/components/settings/permissions-section.tsx +85 -75
  50. package/src/components/settings/permissions-sections.tsx +24 -0
  51. package/src/components/settings/presets-section.tsx +159 -0
  52. package/src/components/settings/pricing-registry-panel.tsx +164 -0
  53. package/src/components/shared/app-sidebar.tsx +2 -0
  54. package/src/components/shared/command-palette.tsx +30 -0
  55. package/src/components/shared/light-markdown.tsx +134 -0
  56. package/src/components/workflows/loop-status-view.tsx +8 -4
  57. package/src/components/workflows/workflow-status-view.tsx +16 -9
  58. package/src/lib/agents/__tests__/claude-agent.test.ts +7 -2
  59. package/src/lib/agents/__tests__/learned-context.test.ts +500 -0
  60. package/src/lib/agents/__tests__/pattern-extractor.test.ts +243 -0
  61. package/src/lib/agents/__tests__/sweep.test.ts +202 -0
  62. package/src/lib/agents/claude-agent.ts +104 -78
  63. package/src/lib/agents/learned-context.ts +32 -28
  64. package/src/lib/agents/learning-session.ts +234 -0
  65. package/src/lib/agents/pattern-extractor.ts +34 -64
  66. package/src/lib/agents/profiles/__tests__/sort.test.ts +42 -0
  67. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +0 -1
  68. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +0 -1
  69. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +0 -1
  70. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +0 -1
  71. package/src/lib/agents/profiles/builtins/general/profile.yaml +0 -1
  72. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +0 -1
  73. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +0 -1
  74. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +0 -1
  75. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +0 -1
  76. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +0 -1
  77. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +0 -1
  78. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +0 -1
  79. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +0 -1
  80. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +0 -1
  81. package/src/lib/agents/profiles/registry.ts +0 -1
  82. package/src/lib/agents/profiles/sort.ts +7 -0
  83. package/src/lib/agents/profiles/types.ts +0 -1
  84. package/src/lib/agents/runtime/catalog.ts +1 -1
  85. package/src/lib/agents/runtime/claude.ts +66 -0
  86. package/src/lib/constants/settings.ts +1 -0
  87. package/src/lib/constants/task-status.ts +6 -0
  88. package/src/lib/data/seed-data/profiles.ts +0 -3
  89. package/src/lib/db/schema.ts +3 -0
  90. package/src/lib/docs/adoption.ts +105 -0
  91. package/src/lib/docs/journey-tracker.ts +21 -0
  92. package/src/lib/docs/reader.ts +102 -0
  93. package/src/lib/docs/types.ts +54 -0
  94. package/src/lib/docs/usage-stage.ts +60 -0
  95. package/src/lib/notifications/actionable.ts +18 -10
  96. package/src/lib/settings/__tests__/budget-guardrails.test.ts +86 -24
  97. package/src/lib/settings/budget-guardrails.ts +213 -85
  98. package/src/lib/settings/permission-presets.ts +150 -0
  99. package/src/lib/settings/runtime-setup.ts +71 -0
  100. package/src/lib/usage/__tests__/ledger.test.ts +29 -5
  101. package/src/lib/usage/__tests__/pricing-registry.test.ts +78 -0
  102. package/src/lib/usage/ledger.ts +4 -2
  103. package/src/lib/usage/pricing-registry.ts +570 -0
  104. package/src/lib/usage/pricing.ts +15 -41
  105. package/src/lib/utils/__tests__/learned-context-history.test.ts +171 -0
  106. package/src/lib/utils/learned-context-history.ts +150 -0
  107. package/src/lib/validators/__tests__/profile.test.ts +0 -15
  108. package/src/lib/validators/__tests__/settings.test.ts +23 -16
  109. package/src/lib/validators/profile.ts +0 -1
  110. package/src/lib/validators/settings.ts +3 -9
  111. package/src/lib/workflows/__tests__/engine.test.ts +2 -0
  112. package/src/lib/workflows/engine.ts +20 -1
@@ -252,6 +252,71 @@ async function runClaudeProfileTests(profileId: string): Promise<ProfileTestRepo
252
252
  };
253
253
  }
254
254
 
255
+ // ---------------------------------------------------------------------------
256
+ // Lightweight meta-completion (pattern extraction, context summarization, etc.)
257
+ // ---------------------------------------------------------------------------
258
+
259
+ export async function runMetaCompletion(input: {
260
+ prompt: string;
261
+ activityType: string;
262
+ }): Promise<{ text: string; usage: UsageSnapshot }> {
263
+ const authEnv = await getAuthEnv();
264
+ const startedAt = new Date();
265
+ let usage: UsageSnapshot = {};
266
+ const abortController = new AbortController();
267
+ const timeout = setTimeout(() => abortController.abort(), 60_000);
268
+
269
+ try {
270
+ const response = query({
271
+ prompt: input.prompt,
272
+ options: {
273
+ abortController,
274
+ includePartialMessages: true,
275
+ cwd: process.cwd(),
276
+ env: buildClaudeSdkEnv(authEnv),
277
+ allowedTools: [],
278
+ maxTurns: 1,
279
+ },
280
+ });
281
+
282
+ const collected = await collectResultText(
283
+ response as AsyncIterable<Record<string, unknown>>
284
+ );
285
+ usage = collected.usage;
286
+
287
+ await recordUsageLedgerEntry({
288
+ activityType: input.activityType as import("@/lib/usage/ledger").UsageActivityType,
289
+ runtimeId: "claude-code",
290
+ providerId: "anthropic",
291
+ modelId: usage.modelId ?? null,
292
+ inputTokens: usage.inputTokens ?? null,
293
+ outputTokens: usage.outputTokens ?? null,
294
+ totalTokens: usage.totalTokens ?? null,
295
+ status: "completed",
296
+ startedAt,
297
+ finishedAt: new Date(),
298
+ });
299
+
300
+ return { text: collected.resultText, usage };
301
+ } catch (error) {
302
+ await recordUsageLedgerEntry({
303
+ activityType: input.activityType as import("@/lib/usage/ledger").UsageActivityType,
304
+ runtimeId: "claude-code",
305
+ providerId: "anthropic",
306
+ modelId: usage.modelId ?? null,
307
+ inputTokens: usage.inputTokens ?? null,
308
+ outputTokens: usage.outputTokens ?? null,
309
+ totalTokens: usage.totalTokens ?? null,
310
+ status: "failed",
311
+ startedAt,
312
+ finishedAt: new Date(),
313
+ });
314
+ throw error;
315
+ } finally {
316
+ clearTimeout(timeout);
317
+ }
318
+ }
319
+
255
320
  async function runClaudeTaskAssist(
256
321
  input: TaskAssistInput
257
322
  ): Promise<TaskAssistResponse> {
@@ -345,6 +410,7 @@ async function testClaudeConnection(): Promise<RuntimeConnectionResult> {
345
410
  options: {
346
411
  abortController,
347
412
  maxTurns: 1,
413
+ includePartialMessages: false,
348
414
  cwd: process.cwd(),
349
415
  env: buildClaudeSdkEnv(authEnv),
350
416
  },
@@ -7,6 +7,7 @@ export const SETTINGS_KEYS = {
7
7
  PERMISSIONS_ALLOW: "permissions.allow",
8
8
  BUDGET_POLICY: "usage.budgetPolicy",
9
9
  BUDGET_WARNING_STATE: "usage.budgetWarningState",
10
+ PRICING_REGISTRY: "usage.pricingRegistry",
10
11
  } as const;
11
12
 
12
13
  export type AuthMethod = "api_key" | "oauth";
@@ -47,3 +47,9 @@ export function isValidDragTransition(from: TaskStatus, to: TaskStatus): boolean
47
47
 
48
48
  /** Maximum number of times a task can be resumed before requiring a fresh start */
49
49
  export const MAX_RESUME_COUNT = 3;
50
+
51
+ /** Default max turns for agent task execution (safety net) */
52
+ export const DEFAULT_MAX_TURNS = 50;
53
+
54
+ /** Default per-execution budget cap in USD */
55
+ export const DEFAULT_MAX_BUDGET_USD = 2.0;
@@ -34,7 +34,6 @@ export function getSampleProfiles(): SampleProfileSeed[] {
34
34
  canUseToolPolicy: {
35
35
  autoApprove: ["Read", "Grep"],
36
36
  },
37
- temperature: 0.3,
38
37
  maxTurns: 18,
39
38
  outputFormat: "Weekly operating note with metrics, risks, and next actions.",
40
39
  author: SAMPLE_PROFILE_AUTHOR,
@@ -73,7 +72,6 @@ You review pipeline movement, funnel risk, and rep follow-ups with a bias toward
73
72
  canUseToolPolicy: {
74
73
  autoApprove: ["Read"],
75
74
  },
76
- temperature: 0.6,
77
75
  maxTurns: 16,
78
76
  outputFormat: "Experiment summary with winning message angles and next tests.",
79
77
  author: SAMPLE_PROFILE_AUTHOR,
@@ -109,7 +107,6 @@ You turn campaign performance and research inputs into sharper launch messaging.
109
107
  domain: "personal",
110
108
  tags: ["investing", "portfolio", "risk", "habits"],
111
109
  allowedTools: ["Read", "Write"],
112
- temperature: 0.25,
113
110
  maxTurns: 14,
114
111
  outputFormat: "Short investor brief with posture, risk notes, and watchlist changes.",
115
112
  author: SAMPLE_PROFILE_AUTHOR,
@@ -88,6 +88,7 @@ export const notifications = sqliteTable(
88
88
  "agent_message",
89
89
  "budget_alert",
90
90
  "context_proposal",
91
+ "context_proposal_batch",
91
92
  ],
92
93
  }).notNull(),
93
94
  title: text("title").notNull(),
@@ -223,6 +224,8 @@ export const usageLedger = sqliteTable(
223
224
  "scheduled_firing",
224
225
  "task_assist",
225
226
  "profile_test",
227
+ "pattern_extraction",
228
+ "context_summarization",
226
229
  ],
227
230
  }).notNull(),
228
231
  runtimeId: text("runtime_id").notNull(),
@@ -0,0 +1,105 @@
1
+ import { db } from "@/lib/db";
2
+ import {
3
+ tasks,
4
+ workflows,
5
+ documents,
6
+ schedules,
7
+ usageLedger,
8
+ learnedContext,
9
+ settings,
10
+ } from "@/lib/db/schema";
11
+ import { count, like, sql } from "drizzle-orm";
12
+ import type { AdoptionEntry } from "./types";
13
+
14
+ function toDepth(n: number): AdoptionEntry["depth"] {
15
+ if (n === 0) return "none";
16
+ if (n <= 3) return "light";
17
+ return "deep";
18
+ }
19
+
20
+ function entry(n: number): AdoptionEntry {
21
+ return { adopted: n > 0, depth: toDepth(n) };
22
+ }
23
+
24
+ /** Map each manifest section slug to DB evidence of adoption */
25
+ export async function getAdoptionMap(): Promise<Map<string, AdoptionEntry>> {
26
+ const [
27
+ taskCount,
28
+ workflowCount,
29
+ documentCount,
30
+ scheduleCount,
31
+ profileUsed,
32
+ usageCount,
33
+ learnedCount,
34
+ permissionCount,
35
+ providerCount,
36
+ ] = await Promise.all([
37
+ db
38
+ .select({ n: count() })
39
+ .from(tasks)
40
+ .then((r) => r[0]?.n ?? 0),
41
+ db
42
+ .select({ n: count() })
43
+ .from(workflows)
44
+ .then((r) => r[0]?.n ?? 0),
45
+ db
46
+ .select({ n: count() })
47
+ .from(documents)
48
+ .then((r) => r[0]?.n ?? 0),
49
+ db
50
+ .select({ n: count() })
51
+ .from(schedules)
52
+ .then((r) => r[0]?.n ?? 0),
53
+ db
54
+ .select({ n: count() })
55
+ .from(tasks)
56
+ .where(sql`${tasks.agentProfile} IS NOT NULL`)
57
+ .then((r) => r[0]?.n ?? 0),
58
+ db
59
+ .select({ n: count() })
60
+ .from(usageLedger)
61
+ .then((r) => r[0]?.n ?? 0),
62
+ db
63
+ .select({ n: count() })
64
+ .from(learnedContext)
65
+ .then((r) => r[0]?.n ?? 0),
66
+ db
67
+ .select({ n: count() })
68
+ .from(settings)
69
+ .where(like(settings.key, "permission:%"))
70
+ .then((r) => r[0]?.n ?? 0),
71
+ db
72
+ .select({ n: sql<number>`COUNT(DISTINCT ${usageLedger.providerId})` })
73
+ .from(usageLedger)
74
+ .then((r) => Number(r[0]?.n ?? 0)),
75
+ ]);
76
+
77
+ const map = new Map<string, AdoptionEntry>();
78
+
79
+ // Always adopted
80
+ map.set("home-workspace", { adopted: true, depth: "deep" });
81
+ map.set("settings", { adopted: true, depth: "deep" });
82
+
83
+ // Feature sections
84
+ map.set("dashboard-kanban", entry(taskCount));
85
+ map.set("inbox-notifications", entry(taskCount)); // notifications come with tasks
86
+ map.set("monitoring", entry(taskCount));
87
+ map.set("projects", entry(taskCount)); // projects enable tasks
88
+ map.set("workflows", entry(workflowCount));
89
+ map.set("documents", entry(documentCount));
90
+ map.set("profiles", entry(profileUsed));
91
+ map.set("schedules", entry(scheduleCount));
92
+ map.set("cost-usage", entry(usageCount));
93
+
94
+ // Cross-cutting
95
+ map.set(
96
+ "provider-runtimes",
97
+ providerCount > 1
98
+ ? { adopted: true, depth: "deep" }
99
+ : entry(usageCount > 0 ? 1 : 0)
100
+ );
101
+ map.set("agent-intelligence", entry(learnedCount));
102
+ map.set("tool-permissions", entry(permissionCount));
103
+
104
+ return map;
105
+ }
@@ -0,0 +1,21 @@
1
+ import type { AdoptionEntry, DocJourney, JourneyCompletion } from "./types";
2
+
3
+ /** Compute journey completion from adoption data */
4
+ export function getJourneyCompletions(
5
+ journeys: DocJourney[],
6
+ adoptionMap: Map<string, AdoptionEntry>
7
+ ): Map<string, JourneyCompletion> {
8
+ const completions = new Map<string, JourneyCompletion>();
9
+
10
+ for (const journey of journeys) {
11
+ const total = journey.sections.length;
12
+ const completed = journey.sections.filter(
13
+ (slug) => adoptionMap.get(slug)?.adopted === true
14
+ ).length;
15
+ const percentage = total > 0 ? Math.round((completed / total) * 100) : 0;
16
+
17
+ completions.set(journey.slug, { completed, total, percentage });
18
+ }
19
+
20
+ return completions;
21
+ }
@@ -0,0 +1,102 @@
1
+ import { readFileSync, readdirSync, existsSync } from "fs";
2
+ import { join, basename } from "path";
3
+ import type { DocManifest, ParsedDoc } from "./types";
4
+
5
+ /** Resolve the docs directory relative to project root */
6
+ function docsDir(): string {
7
+ return join(process.cwd(), "docs");
8
+ }
9
+
10
+ /** Read and parse docs/manifest.json */
11
+ export function getManifest(): DocManifest {
12
+ const raw = readFileSync(join(docsDir(), "manifest.json"), "utf-8");
13
+ return JSON.parse(raw) as DocManifest;
14
+ }
15
+
16
+ /** Parse a markdown file into frontmatter + body */
17
+ function parseMarkdown(content: string, slug: string): ParsedDoc {
18
+ const fmRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
19
+ const match = content.match(fmRegex);
20
+
21
+ if (!match) {
22
+ return { frontmatter: {}, body: content, slug };
23
+ }
24
+
25
+ const frontmatter: Record<string, unknown> = {};
26
+ const fmLines = match[1].split("\n");
27
+ for (const line of fmLines) {
28
+ const colonIdx = line.indexOf(":");
29
+ if (colonIdx === -1) continue;
30
+ const key = line.slice(0, colonIdx).trim();
31
+ let value: unknown = line.slice(colonIdx + 1).trim();
32
+
33
+ // Strip surrounding quotes
34
+ if (
35
+ typeof value === "string" &&
36
+ value.startsWith('"') &&
37
+ value.endsWith('"')
38
+ ) {
39
+ value = value.slice(1, -1);
40
+ }
41
+
42
+ // Parse arrays: ["a", "b"]
43
+ if (typeof value === "string" && value.startsWith("[")) {
44
+ try {
45
+ value = JSON.parse(value);
46
+ } catch {
47
+ // keep as string
48
+ }
49
+ }
50
+
51
+ frontmatter[key] = value;
52
+ }
53
+
54
+ return { frontmatter, body: match[2], slug };
55
+ }
56
+
57
+ /** Read a single doc by slug (searches features/ then journeys/ then root) */
58
+ export function getDocBySlug(slug: string): ParsedDoc | null {
59
+ const dirs = ["features", "journeys", ""];
60
+ for (const dir of dirs) {
61
+ const filePath = join(docsDir(), dir, `${slug}.md`);
62
+ if (existsSync(filePath)) {
63
+ const content = readFileSync(filePath, "utf-8");
64
+ return parseMarkdown(content, slug);
65
+ }
66
+ }
67
+ return null;
68
+ }
69
+
70
+ /** Read all docs from features/ and journeys/ */
71
+ export function getAllDocs(): ParsedDoc[] {
72
+ const docs: ParsedDoc[] = [];
73
+ const subDirs = ["features", "journeys"];
74
+
75
+ for (const dir of subDirs) {
76
+ const dirPath = join(docsDir(), dir);
77
+ if (!existsSync(dirPath)) continue;
78
+
79
+ const files = readdirSync(dirPath).filter((f) => f.endsWith(".md"));
80
+ for (const file of files) {
81
+ const content = readFileSync(join(dirPath, file), "utf-8");
82
+ const slug = basename(file, ".md");
83
+ docs.push(parseMarkdown(content, slug));
84
+ }
85
+ }
86
+
87
+ // Also check for getting-started.md at root
88
+ const gsPath = join(docsDir(), "getting-started.md");
89
+ if (existsSync(gsPath)) {
90
+ const content = readFileSync(gsPath, "utf-8");
91
+ docs.push(parseMarkdown(content, "getting-started"));
92
+ }
93
+
94
+ return docs;
95
+ }
96
+
97
+ /** Read the .last-generated timestamp */
98
+ export function getDocsLastGenerated(): string | null {
99
+ const filePath = join(docsDir(), ".last-generated");
100
+ if (!existsSync(filePath)) return null;
101
+ return readFileSync(filePath, "utf-8").trim();
102
+ }
@@ -0,0 +1,54 @@
1
+ // Types for the Playbook documentation system
2
+
3
+ export interface DocManifest {
4
+ generated: string;
5
+ version: number;
6
+ sections: DocSection[];
7
+ journeys: DocJourney[];
8
+ metadata: {
9
+ totalDocs: number;
10
+ totalScreengrabs: number;
11
+ featuresCovered: number;
12
+ appSections: number;
13
+ };
14
+ }
15
+
16
+ export interface DocSection {
17
+ slug: string;
18
+ title: string;
19
+ category: string;
20
+ path: string;
21
+ route: string;
22
+ tags: string[];
23
+ features: string[];
24
+ screengrabCount: number;
25
+ }
26
+
27
+ export interface DocJourney {
28
+ slug: string;
29
+ title: string;
30
+ persona: string;
31
+ difficulty: string;
32
+ path: string;
33
+ sections: string[];
34
+ stepCount: number;
35
+ }
36
+
37
+ export interface ParsedDoc {
38
+ frontmatter: Record<string, unknown>;
39
+ body: string;
40
+ slug: string;
41
+ }
42
+
43
+ export type UsageStage = "new" | "early" | "active" | "power";
44
+
45
+ export interface AdoptionEntry {
46
+ adopted: boolean;
47
+ depth: "none" | "light" | "deep";
48
+ }
49
+
50
+ export interface JourneyCompletion {
51
+ completed: number;
52
+ total: number;
53
+ percentage: number;
54
+ }
@@ -0,0 +1,60 @@
1
+ import { db } from "@/lib/db";
2
+ import {
3
+ tasks,
4
+ projects,
5
+ workflows,
6
+ schedules,
7
+ usageLedger,
8
+ } from "@/lib/db/schema";
9
+ import { count, isNotNull } from "drizzle-orm";
10
+ import type { UsageStage } from "./types";
11
+
12
+ /** Compute the user's usage stage from DB state */
13
+ export async function getUsageStage(): Promise<UsageStage> {
14
+ const [taskCount, projectCount, workflowCount, scheduleCount, profileUsed] =
15
+ await Promise.all([
16
+ db
17
+ .select({ n: count() })
18
+ .from(tasks)
19
+ .then((r) => r[0]?.n ?? 0),
20
+ db
21
+ .select({ n: count() })
22
+ .from(projects)
23
+ .then((r) => r[0]?.n ?? 0),
24
+ db
25
+ .select({ n: count() })
26
+ .from(workflows)
27
+ .then((r) => r[0]?.n ?? 0),
28
+ db
29
+ .select({ n: count() })
30
+ .from(schedules)
31
+ .then((r) => r[0]?.n ?? 0),
32
+ db
33
+ .select({ n: count() })
34
+ .from(tasks)
35
+ .where(isNotNull(tasks.agentProfile))
36
+ .then((r) => r[0]?.n ?? 0),
37
+ ]);
38
+
39
+ // Power: has workflows + schedules + profiles
40
+ if (workflowCount > 0 && scheduleCount > 0 && profileUsed > 0) {
41
+ return "power";
42
+ }
43
+
44
+ // Active: 6+ tasks OR 3+ projects OR any workflows/schedules
45
+ if (
46
+ taskCount >= 6 ||
47
+ projectCount >= 3 ||
48
+ workflowCount > 0 ||
49
+ scheduleCount > 0
50
+ ) {
51
+ return "active";
52
+ }
53
+
54
+ // Early: 1-5 tasks, <=2 projects
55
+ if (taskCount >= 1 || projectCount >= 1) {
56
+ return "early";
57
+ }
58
+
59
+ return "new";
60
+ }
@@ -80,6 +80,7 @@ export async function listPendingApprovalPayloads(
80
80
  inArray(notifications.type, [
81
81
  "permission_required",
82
82
  "context_proposal",
83
+ "context_proposal_batch",
83
84
  ]),
84
85
  isNull(notifications.response)
85
86
  )
@@ -89,6 +90,7 @@ export async function listPendingApprovalPayloads(
89
90
 
90
91
  return rows.map((row) => {
91
92
  const isContextProposal = row.type === "context_proposal";
93
+ const isBatchProposal = row.type === "context_proposal_batch";
92
94
  const parsedInput = parseNotificationToolInput(row.toolInput);
93
95
 
94
96
  return {
@@ -97,16 +99,22 @@ export async function listPendingApprovalPayloads(
97
99
  taskId: row.taskId,
98
100
  workflowId: row.workflowId,
99
101
  toolName: row.toolName,
100
- permissionLabel: isContextProposal
101
- ? "Context Proposal"
102
- : getPermissionKindLabel(row.toolName),
103
- compactSummary: isContextProposal
104
- ? `Learned patterns proposed for profile "${row.toolName}"`
105
- : buildPermissionSummary(row.toolName, parsedInput),
106
- deepLink: isContextProposal
107
- ? `/profiles/${row.toolName}`
108
- : buildDeepLink(row.taskId, row.workflowId),
109
- supportedActionIds: isContextProposal
102
+ permissionLabel: isBatchProposal
103
+ ? "Workflow Learning"
104
+ : isContextProposal
105
+ ? "Context Proposal"
106
+ : getPermissionKindLabel(row.toolName),
107
+ compactSummary: isBatchProposal
108
+ ? `Batch of learned patterns from workflow execution`
109
+ : isContextProposal
110
+ ? `Learned patterns proposed for profile "${row.toolName}"`
111
+ : buildPermissionSummary(row.toolName, parsedInput),
112
+ deepLink: isBatchProposal
113
+ ? "/inbox"
114
+ : isContextProposal
115
+ ? `/profiles/${row.toolName}`
116
+ : buildDeepLink(row.taskId, row.workflowId),
117
+ supportedActionIds: (isContextProposal || isBatchProposal)
110
118
  ? (["allow_once", "deny"] as ApprovalActionId[])
111
119
  : [...APPROVAL_ACTION_IDS],
112
120
  title: row.title,