stagent 0.1.9 → 0.1.10

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 (44) hide show
  1. package/README.md +129 -47
  2. package/package.json +1 -1
  3. package/public/readme/cost-usage-list.png +0 -0
  4. package/public/readme/dashboard-bulk-select.png +0 -0
  5. package/public/readme/dashboard-card-edit.png +0 -0
  6. package/public/readme/dashboard-create-form-ai-applied.png +0 -0
  7. package/public/readme/dashboard-create-form-ai-assist.png +0 -0
  8. package/public/readme/dashboard-create-form-empty.png +0 -0
  9. package/public/readme/dashboard-create-form-filled.png +0 -0
  10. package/public/readme/dashboard-filtered.png +0 -0
  11. package/public/readme/dashboard-list.png +0 -0
  12. package/public/readme/dashboard-sorted.png +0 -0
  13. package/public/readme/dashboard-workflow-confirm.png +0 -0
  14. package/public/readme/documents-grid.png +0 -0
  15. package/public/readme/documents-list.png +0 -0
  16. package/public/readme/home-below-fold.png +0 -0
  17. package/public/readme/home-list.png +0 -0
  18. package/public/readme/inbox-list.png +0 -0
  19. package/public/readme/monitor-list.png +0 -0
  20. package/public/readme/profiles-list.png +0 -0
  21. package/public/readme/projects-detail.png +0 -0
  22. package/public/readme/projects-list.png +0 -0
  23. package/public/readme/schedules-list.png +0 -0
  24. package/public/readme/settings-list.png +0 -0
  25. package/public/readme/workflows-list.png +0 -0
  26. package/src/app/api/workflows/from-assist/route.ts +143 -0
  27. package/src/app/dashboard/page.tsx +24 -2
  28. package/src/app/workflows/from-assist/page.tsx +35 -0
  29. package/src/components/projects/project-card.tsx +47 -35
  30. package/src/components/tasks/ai-assist-panel.tsx +31 -10
  31. package/src/components/tasks/task-card.tsx +16 -1
  32. package/src/components/tasks/task-create-panel.tsx +39 -0
  33. package/src/components/workflows/workflow-confirmation-view.tsx +447 -0
  34. package/src/lib/agents/profiles/__tests__/suggest.test.ts +67 -0
  35. package/src/lib/agents/profiles/suggest.ts +36 -0
  36. package/src/lib/agents/runtime/claude.ts +36 -6
  37. package/src/lib/agents/runtime/task-assist-types.ts +12 -2
  38. package/src/lib/data/__tests__/clear.test.ts +42 -0
  39. package/src/lib/data/clear.ts +3 -0
  40. package/src/lib/notifications/permissions.ts +6 -2
  41. package/src/lib/workflows/__tests__/assist-builder.test.ts +255 -0
  42. package/src/lib/workflows/assist-builder.ts +248 -0
  43. package/src/lib/workflows/assist-session.ts +78 -0
  44. package/src/lib/workflows/engine.ts +46 -1
@@ -0,0 +1,447 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { useRouter } from "next/navigation";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Input } from "@/components/ui/input";
7
+ import { Label } from "@/components/ui/label";
8
+ import { Badge } from "@/components/ui/badge";
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from "@/components/ui/select";
16
+ import {
17
+ ArrowUp,
18
+ ArrowDown,
19
+ Star,
20
+ Trash2,
21
+ Loader2,
22
+ GitBranch,
23
+ ListOrdered,
24
+ Settings,
25
+ } from "lucide-react";
26
+ import { toast } from "sonner";
27
+ import { FormSectionCard } from "@/components/shared/form-section-card";
28
+ import type { TaskAssistResponse } from "@/lib/agents/runtime/task-assist-types";
29
+ import type { WorkflowPattern } from "@/lib/workflows/types";
30
+ import { suggestProfileForStep } from "@/lib/agents/profiles/suggest";
31
+ import {
32
+ loadAssistState,
33
+ clearAssistState,
34
+ saveTaskFormState,
35
+ } from "@/lib/workflows/assist-session";
36
+
37
+ const PATTERN_OPTIONS: { value: WorkflowPattern; label: string; description: string }[] = [
38
+ { value: "sequence", label: "Sequence", description: "Steps run one after another" },
39
+ { value: "planner-executor", label: "Planner → Executor", description: "First step plans, rest execute" },
40
+ { value: "checkpoint", label: "Checkpoint", description: "Steps pause for human approval" },
41
+ { value: "parallel", label: "Parallel", description: "Independent steps run concurrently" },
42
+ { value: "loop", label: "Loop", description: "Single step repeats iteratively" },
43
+ { value: "swarm", label: "Swarm", description: "Mayor coordinates workers" },
44
+ ];
45
+
46
+ interface StepItem {
47
+ title: string;
48
+ description: string;
49
+ profile: string;
50
+ requiresApproval?: boolean;
51
+ }
52
+
53
+ interface ProfileOption {
54
+ id: string;
55
+ name: string;
56
+ }
57
+
58
+ interface WorkflowConfirmationViewProps {
59
+ projects: { id: string; name: string }[];
60
+ profiles: ProfileOption[];
61
+ }
62
+
63
+ export function WorkflowConfirmationView({
64
+ projects,
65
+ profiles,
66
+ }: WorkflowConfirmationViewProps) {
67
+ const router = useRouter();
68
+ const [workflowName, setWorkflowName] = useState("");
69
+ const [pattern, setPattern] = useState<WorkflowPattern>("sequence");
70
+ const [selectedProjectId, setSelectedProjectId] = useState("");
71
+ const [steps, setSteps] = useState<StepItem[]>([]);
72
+ const [maxIterations, setMaxIterations] = useState(5);
73
+ const [workerConcurrencyLimit, setWorkerConcurrencyLimit] = useState(2);
74
+ const [submitting, setSubmitting] = useState(false);
75
+ const [priority, setPriority] = useState(2);
76
+ const [assignedAgent, setAssignedAgent] = useState<string | undefined>();
77
+ const [loaded, setLoaded] = useState(false);
78
+
79
+ // Load state from sessionStorage on mount
80
+ useEffect(() => {
81
+ const state = loadAssistState();
82
+ if (!state) {
83
+ router.replace("/tasks/new");
84
+ return;
85
+ }
86
+
87
+ const { assistResult, formState } = state;
88
+ const recommended = assistResult.recommendedPattern;
89
+ const validPattern = recommended === "single" ? "sequence" : (recommended as WorkflowPattern);
90
+
91
+ setPattern(validPattern);
92
+ setWorkflowName(`Workflow: ${formState.title}`);
93
+ setSelectedProjectId(formState.projectId);
94
+ setPriority(parseInt(formState.priority, 10) || 2);
95
+ setAssignedAgent(formState.assignedAgent || undefined);
96
+ setMaxIterations(assistResult.suggestedLoopConfig?.maxIterations ?? 5);
97
+ setWorkerConcurrencyLimit(assistResult.suggestedSwarmConfig?.workerConcurrencyLimit ?? 2);
98
+
99
+ const profileIds = profiles.map((p) => p.id);
100
+
101
+ const newSteps: StepItem[] = [
102
+ {
103
+ title: formState.title,
104
+ description: formState.description,
105
+ profile: formState.agentProfile || "auto",
106
+ },
107
+ ...assistResult.breakdown.map((sub) => ({
108
+ title: sub.title,
109
+ description: sub.description,
110
+ profile: sub.suggestedProfile || suggestProfileForStep(sub.title, sub.description, profileIds),
111
+ requiresApproval: sub.requiresApproval,
112
+ })),
113
+ ];
114
+
115
+ setSteps(newSteps);
116
+ setLoaded(true);
117
+ }, [router, profiles]);
118
+
119
+ function navigateBackToTask() {
120
+ // Save form state so task form can restore
121
+ const state = loadAssistState();
122
+ if (state) {
123
+ saveTaskFormState(state.formState);
124
+ }
125
+ clearAssistState();
126
+ router.push("/tasks/new?restore=1");
127
+ }
128
+
129
+ function moveStep(index: number, direction: -1 | 1) {
130
+ if (index === 0) return;
131
+ const newIndex = index + direction;
132
+ if (newIndex < 1 || newIndex >= steps.length) return;
133
+ const newSteps = [...steps];
134
+ [newSteps[index], newSteps[newIndex]] = [newSteps[newIndex], newSteps[index]];
135
+ setSteps(newSteps);
136
+ }
137
+
138
+ function removeStep(index: number) {
139
+ if (index === 0 || steps.length <= 2) return;
140
+ setSteps(steps.filter((_, i) => i !== index));
141
+ }
142
+
143
+ function updateStepProfile(index: number, profile: string) {
144
+ const newSteps = [...steps];
145
+ newSteps[index] = { ...newSteps[index], profile };
146
+ setSteps(newSteps);
147
+ }
148
+
149
+ async function handleSubmit(executeImmediately: boolean) {
150
+ if (!workflowName.trim() || steps.length < 2) return;
151
+ setSubmitting(true);
152
+
153
+ try {
154
+ const definitionSteps = steps.map((step, i) => ({
155
+ id: `step_${i + 1}`,
156
+ name: step.title,
157
+ prompt: step.description,
158
+ agentProfile: step.profile === "auto" ? undefined : step.profile,
159
+ requiresApproval: pattern === "checkpoint" ? step.requiresApproval : undefined,
160
+ ...(pattern === "parallel" && i === steps.length - 1
161
+ ? { dependsOn: steps.slice(0, -1).map((_, j) => `step_${j + 1}`) }
162
+ : {}),
163
+ }));
164
+
165
+ const definition: Record<string, unknown> = {
166
+ pattern,
167
+ steps: definitionSteps,
168
+ };
169
+
170
+ if (pattern === "loop") {
171
+ definition.loopConfig = {
172
+ maxIterations,
173
+ agentProfile: steps[0]?.profile === "auto" ? undefined : steps[0]?.profile,
174
+ };
175
+ }
176
+
177
+ if (pattern === "swarm") {
178
+ definition.swarmConfig = { workerConcurrencyLimit };
179
+ }
180
+
181
+ const res = await fetch("/api/workflows/from-assist", {
182
+ method: "POST",
183
+ headers: { "Content-Type": "application/json" },
184
+ body: JSON.stringify({
185
+ name: workflowName.trim(),
186
+ projectId: selectedProjectId || undefined,
187
+ definition,
188
+ priority,
189
+ assignedAgent: assignedAgent || undefined,
190
+ executeImmediately,
191
+ parentTask: {
192
+ title: steps[0].title,
193
+ description: steps[0].description,
194
+ agentProfile: steps[0].profile === "auto" ? undefined : steps[0].profile,
195
+ },
196
+ }),
197
+ });
198
+
199
+ if (!res.ok) {
200
+ const data = await res.json().catch(() => null);
201
+ toast.error(data?.error ?? "Failed to create workflow");
202
+ return;
203
+ }
204
+
205
+ const data = await res.json();
206
+ toast.success(
207
+ executeImmediately
208
+ ? `Task created with workflow (${data.taskIds.length} steps)`
209
+ : `Task created with draft workflow (${data.taskIds.length} steps)`,
210
+ {
211
+ action: {
212
+ label: "View workflow",
213
+ onClick: () => window.open(`/workflows/${data.workflow.id}`, "_self"),
214
+ },
215
+ }
216
+ );
217
+ clearAssistState();
218
+ router.push("/dashboard");
219
+ } catch {
220
+ toast.error("Network error — could not create workflow");
221
+ } finally {
222
+ setSubmitting(false);
223
+ }
224
+ }
225
+
226
+ const isReorderDisabled = pattern === "parallel";
227
+
228
+ if (!loaded) {
229
+ return (
230
+ <div className="flex items-center justify-center py-12 text-muted-foreground">
231
+ <Loader2 className="h-5 w-5 animate-spin mr-2" />
232
+ Loading workflow data...
233
+ </div>
234
+ );
235
+ }
236
+
237
+ return (
238
+ <div className="space-y-6">
239
+ <div>
240
+ <h1 className="text-2xl font-bold">Create Workflow from AI Assist</h1>
241
+ <p className="text-sm text-muted-foreground mt-1">
242
+ Review and customize the AI-suggested workflow before creating it.
243
+ </p>
244
+ </div>
245
+
246
+ {/* Workflow Identity */}
247
+ <FormSectionCard icon={GitBranch} title="Workflow Identity">
248
+ <div className="space-y-3">
249
+ <div className="space-y-1.5">
250
+ <Label htmlFor="workflow-name">Name</Label>
251
+ <Input
252
+ id="workflow-name"
253
+ value={workflowName}
254
+ onChange={(e) => setWorkflowName(e.target.value)}
255
+ placeholder="Workflow name"
256
+ />
257
+ </div>
258
+
259
+ <div className="grid grid-cols-2 gap-3">
260
+ <div className="space-y-1.5">
261
+ <Label>Pattern</Label>
262
+ <Select value={pattern} onValueChange={(v) => setPattern(v as WorkflowPattern)}>
263
+ <SelectTrigger>
264
+ <SelectValue />
265
+ </SelectTrigger>
266
+ <SelectContent>
267
+ {PATTERN_OPTIONS.map((opt) => (
268
+ <SelectItem key={opt.value} value={opt.value}>
269
+ {opt.label}
270
+ </SelectItem>
271
+ ))}
272
+ </SelectContent>
273
+ </Select>
274
+ <p className="text-xs text-muted-foreground">
275
+ {PATTERN_OPTIONS.find((o) => o.value === pattern)?.description}
276
+ </p>
277
+ </div>
278
+ <div className="space-y-1.5">
279
+ <Label>Project</Label>
280
+ <Select
281
+ value={selectedProjectId || "none"}
282
+ onValueChange={(v) => setSelectedProjectId(v === "none" ? "" : v)}
283
+ >
284
+ <SelectTrigger>
285
+ <SelectValue placeholder="None" />
286
+ </SelectTrigger>
287
+ <SelectContent>
288
+ <SelectItem value="none">None</SelectItem>
289
+ {projects.map((p) => (
290
+ <SelectItem key={p.id} value={p.id}>{p.name}</SelectItem>
291
+ ))}
292
+ </SelectContent>
293
+ </Select>
294
+ </div>
295
+ </div>
296
+ </div>
297
+ </FormSectionCard>
298
+
299
+ {/* Steps */}
300
+ <FormSectionCard icon={ListOrdered} title={`Steps (${steps.length})`}>
301
+ <div className="space-y-2">
302
+ {steps.map((step, i) => (
303
+ <div
304
+ key={i}
305
+ className="flex items-center gap-2 p-2.5 rounded-md border bg-card text-sm"
306
+ >
307
+ <span className="text-xs text-muted-foreground w-5 shrink-0">
308
+ {i + 1}.
309
+ </span>
310
+ {i === 0 && (
311
+ <Badge variant="outline" className="text-xs shrink-0 px-1">
312
+ <Star className="h-2.5 w-2.5" />
313
+ </Badge>
314
+ )}
315
+ <div className="flex-1 min-w-0">
316
+ <span className="font-medium block truncate">{step.title}</span>
317
+ {step.description && (
318
+ <span className="text-xs text-muted-foreground block truncate">
319
+ {step.description}
320
+ </span>
321
+ )}
322
+ </div>
323
+
324
+ {/* Profile selector */}
325
+ <Select
326
+ value={step.profile}
327
+ onValueChange={(v) => updateStepProfile(i, v)}
328
+ >
329
+ <SelectTrigger className="w-32 h-7 text-xs shrink-0">
330
+ <SelectValue />
331
+ </SelectTrigger>
332
+ <SelectContent>
333
+ <SelectItem value="auto">Auto</SelectItem>
334
+ {profiles.map((p) => (
335
+ <SelectItem key={p.id} value={p.id}>
336
+ {p.name}
337
+ </SelectItem>
338
+ ))}
339
+ </SelectContent>
340
+ </Select>
341
+
342
+ {/* Reorder buttons */}
343
+ {!isReorderDisabled && (
344
+ <div className="flex flex-col gap-0.5">
345
+ <button
346
+ type="button"
347
+ onClick={() => moveStep(i, -1)}
348
+ disabled={i <= 1}
349
+ className="p-0.5 rounded hover:bg-muted disabled:opacity-30"
350
+ aria-label="Move step up"
351
+ >
352
+ <ArrowUp className="h-3 w-3" />
353
+ </button>
354
+ <button
355
+ type="button"
356
+ onClick={() => moveStep(i, 1)}
357
+ disabled={i === 0 || i === steps.length - 1}
358
+ className="p-0.5 rounded hover:bg-muted disabled:opacity-30"
359
+ aria-label="Move step down"
360
+ >
361
+ <ArrowDown className="h-3 w-3" />
362
+ </button>
363
+ </div>
364
+ )}
365
+
366
+ {/* Remove button */}
367
+ {i > 0 && steps.length > 2 && (
368
+ <button
369
+ type="button"
370
+ onClick={() => removeStep(i)}
371
+ className="p-0.5 rounded hover:bg-destructive/10 text-muted-foreground hover:text-destructive"
372
+ aria-label={`Remove step ${step.title}`}
373
+ >
374
+ <Trash2 className="h-3 w-3" />
375
+ </button>
376
+ )}
377
+ </div>
378
+ ))}
379
+ </div>
380
+ </FormSectionCard>
381
+
382
+ {/* Pattern-specific config */}
383
+ {(pattern === "loop" || pattern === "swarm") && (
384
+ <FormSectionCard icon={Settings} title="Configuration">
385
+ {pattern === "loop" && (
386
+ <div className="space-y-1.5">
387
+ <Label htmlFor="max-iterations">Max Iterations</Label>
388
+ <Input
389
+ id="max-iterations"
390
+ type="number"
391
+ min={1}
392
+ max={50}
393
+ value={maxIterations}
394
+ onChange={(e) => setMaxIterations(parseInt(e.target.value, 10) || 5)}
395
+ className="w-32"
396
+ />
397
+ </div>
398
+ )}
399
+ {pattern === "swarm" && (
400
+ <div className="space-y-1.5">
401
+ <Label htmlFor="concurrency-limit">Worker Concurrency Limit</Label>
402
+ <Input
403
+ id="concurrency-limit"
404
+ type="number"
405
+ min={1}
406
+ max={5}
407
+ value={workerConcurrencyLimit}
408
+ onChange={(e) => setWorkerConcurrencyLimit(parseInt(e.target.value, 10) || 2)}
409
+ className="w-32"
410
+ />
411
+ </div>
412
+ )}
413
+ <div className="flex items-center gap-2 text-xs text-muted-foreground opacity-50 mt-2">
414
+ <input type="checkbox" disabled />
415
+ <span>Save as Blueprint (coming soon)</span>
416
+ </div>
417
+ </FormSectionCard>
418
+ )}
419
+
420
+ {/* Action bar */}
421
+ <div className="flex justify-end gap-2 pt-2">
422
+ <Button
423
+ variant="ghost"
424
+ onClick={navigateBackToTask}
425
+ disabled={submitting}
426
+ >
427
+ Dismiss
428
+ </Button>
429
+ <Button
430
+ variant="outline"
431
+ onClick={() => handleSubmit(false)}
432
+ disabled={submitting || !workflowName.trim() || steps.length < 2}
433
+ >
434
+ {submitting ? <Loader2 className="h-3 w-3 mr-1 animate-spin" /> : null}
435
+ Accept
436
+ </Button>
437
+ <Button
438
+ onClick={() => handleSubmit(true)}
439
+ disabled={submitting || !workflowName.trim() || steps.length < 2}
440
+ >
441
+ {submitting ? <Loader2 className="h-3 w-3 mr-1 animate-spin" /> : null}
442
+ Accept & Run
443
+ </Button>
444
+ </div>
445
+ </div>
446
+ );
447
+ }
@@ -0,0 +1,67 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { suggestProfileForStep } from "../suggest";
3
+
4
+ const ALL_PROFILES = [
5
+ "general",
6
+ "researcher",
7
+ "code-reviewer",
8
+ "document-writer",
9
+ "devops-engineer",
10
+ "data-analyst",
11
+ ];
12
+
13
+ describe("suggestProfileForStep", () => {
14
+ it("suggests researcher for research-related tasks", () => {
15
+ expect(
16
+ suggestProfileForStep("Research API patterns", "Investigate best practices", ALL_PROFILES)
17
+ ).toBe("researcher");
18
+ });
19
+
20
+ it("suggests code-reviewer for review tasks", () => {
21
+ expect(
22
+ suggestProfileForStep("Security audit", "Review code for vulnerabilities", ALL_PROFILES)
23
+ ).toBe("code-reviewer");
24
+ });
25
+
26
+ it("suggests document-writer for writing tasks", () => {
27
+ expect(
28
+ suggestProfileForStep("Write documentation", "Document the API endpoints", ALL_PROFILES)
29
+ ).toBe("document-writer");
30
+ });
31
+
32
+ it("suggests devops-engineer for deployment tasks", () => {
33
+ expect(
34
+ suggestProfileForStep("Deploy to production", "Set up CI pipeline and infrastructure", ALL_PROFILES)
35
+ ).toBe("devops-engineer");
36
+ });
37
+
38
+ it("suggests data-analyst for data tasks", () => {
39
+ expect(
40
+ suggestProfileForStep("Analyze metrics", "Aggregate data and create statistics", ALL_PROFILES)
41
+ ).toBe("data-analyst");
42
+ });
43
+
44
+ it('returns "auto" when no keywords match', () => {
45
+ expect(
46
+ suggestProfileForStep("Fix the thing", "Make it work", ALL_PROFILES)
47
+ ).toBe("auto");
48
+ });
49
+
50
+ it("only suggests from available profiles", () => {
51
+ expect(
52
+ suggestProfileForStep("Research API patterns", "Investigate", ["general"])
53
+ ).toBe("auto");
54
+ });
55
+
56
+ it("picks highest-scoring profile when multiple match", () => {
57
+ // "review" + "security" + "vulnerability" = 3 hits for code-reviewer
58
+ // "investigate" = 1 hit for researcher
59
+ expect(
60
+ suggestProfileForStep(
61
+ "Security review",
62
+ "Investigate vulnerability audit",
63
+ ALL_PROFILES
64
+ )
65
+ ).toBe("code-reviewer");
66
+ });
67
+ });
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Lightweight keyword-based profile suggestion.
3
+ * Used as fallback when AI doesn't suggest a profile for a step.
4
+ */
5
+
6
+ const KEYWORD_MAP: Record<string, string[]> = {
7
+ researcher: ["research", "search", "investigate", "explore", "find", "discover", "analyze data", "survey"],
8
+ "code-reviewer": ["review", "audit", "security", "vulnerability", "lint", "inspect", "code quality"],
9
+ "document-writer": ["write", "document", "report", "summarize", "draft", "compose", "blog", "article"],
10
+ "devops-engineer": ["deploy", "ci", "infrastructure", "pipeline", "docker", "kubernetes", "terraform", "monitor"],
11
+ "data-analyst": ["analyze", "data", "statistics", "metrics", "chart", "visualization", "dashboard", "aggregate"],
12
+ };
13
+
14
+ export function suggestProfileForStep(
15
+ title: string,
16
+ description: string,
17
+ availableProfileIds: string[]
18
+ ): string {
19
+ const text = `${title} ${description}`.toLowerCase();
20
+ let bestProfile = "auto";
21
+ let bestScore = 0;
22
+
23
+ for (const [profileId, keywords] of Object.entries(KEYWORD_MAP)) {
24
+ if (!availableProfileIds.includes(profileId)) continue;
25
+ let score = 0;
26
+ for (const keyword of keywords) {
27
+ if (text.includes(keyword)) score++;
28
+ }
29
+ if (score > bestScore) {
30
+ bestScore = score;
31
+ bestProfile = profileId;
32
+ }
33
+ }
34
+
35
+ return bestScore >= 1 ? bestProfile : "auto";
36
+ }
@@ -4,7 +4,7 @@ import { tasks } from "@/lib/db/schema";
4
4
  import { eq } from "drizzle-orm";
5
5
  import { updateAuthStatus, getAuthEnv } from "@/lib/settings/auth";
6
6
  import { getExecution, removeExecution } from "@/lib/agents/execution-manager";
7
- import { getProfile } from "@/lib/agents/profiles/registry";
7
+ import { getProfile, listProfiles } from "@/lib/agents/profiles/registry";
8
8
  import { resolveProfileRuntimePayload } from "@/lib/agents/profiles/compatibility";
9
9
  import { executeClaudeTask, resumeClaudeTask } from "@/lib/agents/claude-agent";
10
10
  import { getRuntimeCapabilities, getRuntimeCatalogEntry } from "./catalog";
@@ -23,13 +23,41 @@ import {
23
23
  type UsageSnapshot,
24
24
  } from "@/lib/usage/ledger";
25
25
 
26
- const TASK_ASSIST_SYSTEM_PROMPT = `You are an AI task definition assistant. Analyze the given task and return ONLY a JSON object (no markdown, no code fences) with:
26
+ function buildTaskAssistSystemPrompt(profileIds: string[]): string {
27
+ const profileList = profileIds.length > 0
28
+ ? `Available agent profiles: ${profileIds.join(", ")}\nUse "auto" if unsure which profile fits a step.`
29
+ : `No explicit profiles available. Use "auto" for suggestedProfile.`;
30
+
31
+ return `You are an AI task definition assistant. Analyze the given task and return ONLY a JSON object (no markdown, no code fences) with:
27
32
  - "improvedDescription": A clearer version of the task for an AI agent to execute
28
- - "breakdown": Array of {title, description} sub-tasks if complex (empty array if simple)
29
- - "recommendedPattern": "single", "sequence", "planner-executor", or "checkpoint"
33
+ - "breakdown": Array of step objects if complex (empty array if simple). Each step: {title, description, suggestedProfile?, requiresApproval?, dependsOn?}
34
+ - "suggestedProfile": one of the available profile IDs or "auto"
35
+ - "requiresApproval": true if the step involves irreversible actions needing human review
36
+ - "dependsOn": array of step indices (0-based) this step depends on (for parallel/swarm patterns)
37
+ - "recommendedPattern": one of "single", "sequence", "planner-executor", "checkpoint", "parallel", "loop", "swarm"
38
+ - "sequence": steps run one after another in order
39
+ - "planner-executor": first step plans, remaining steps execute the plan
40
+ - "checkpoint": like sequence but certain steps pause for human approval
41
+ - "parallel": independent steps run concurrently, a final synthesis step merges results (use dependsOn to mark the synthesis step)
42
+ - "loop": a single step repeats iteratively until a goal is met (include suggestedLoopConfig)
43
+ - "swarm": first step is the mayor (coordinator), middle steps are workers (run in parallel), last step is the refinery (merges results)
30
44
  - "complexity": "simple", "moderate", or "complex"
31
45
  - "needsCheckpoint": true if irreversible actions or needs human review
32
- - "reasoning": Brief explanation`;
46
+ - "reasoning": Brief explanation of why you chose this pattern
47
+ - "suggestedLoopConfig": {maxIterations, timeBudgetMs?} — only for loop pattern
48
+ - "suggestedSwarmConfig": {workerConcurrencyLimit?} — only for swarm pattern
49
+
50
+ ${profileList}
51
+
52
+ Pattern selection guide:
53
+ - Use "single" for simple, atomic tasks
54
+ - Use "sequence" for ordered multi-step work where each step builds on the previous
55
+ - Use "planner-executor" when the task needs analysis before action
56
+ - Use "checkpoint" when steps involve deployments, deletions, or other irreversible actions
57
+ - Use "parallel" when sub-tasks are independent and can run concurrently (research, analysis)
58
+ - Use "loop" for iterative refinement (code review cycles, optimization passes)
59
+ - Use "swarm" for complex tasks needing multiple specialized agents coordinated by a lead`;
60
+ }
33
61
 
34
62
  async function collectResultText(
35
63
  response: AsyncIterable<Record<string, unknown>>
@@ -235,7 +263,9 @@ async function runClaudeTaskAssist(
235
263
  .join("\n");
236
264
 
237
265
  const authEnv = await getAuthEnv();
238
- const prompt = `${TASK_ASSIST_SYSTEM_PROMPT}\n\n${userMessage}`;
266
+ const profileIds = listProfiles().map((p) => p.id);
267
+ const systemPrompt = buildTaskAssistSystemPrompt(profileIds);
268
+ const prompt = `${systemPrompt}\n\n${userMessage}`;
239
269
  const startedAt = new Date();
240
270
  let usage: UsageSnapshot = {};
241
271
 
@@ -1,8 +1,18 @@
1
+ export interface TaskAssistBreakdownStep {
2
+ title: string;
3
+ description: string;
4
+ suggestedProfile?: string;
5
+ requiresApproval?: boolean;
6
+ dependsOn?: number[];
7
+ }
8
+
1
9
  export interface TaskAssistResponse {
2
10
  improvedDescription: string;
3
- breakdown: { title: string; description: string }[];
4
- recommendedPattern: "single" | "sequence" | "planner-executor" | "checkpoint";
11
+ breakdown: TaskAssistBreakdownStep[];
12
+ recommendedPattern: "single" | "sequence" | "planner-executor" | "checkpoint" | "parallel" | "loop" | "swarm";
5
13
  complexity: "simple" | "moderate" | "complex";
6
14
  needsCheckpoint: boolean;
7
15
  reasoning: string;
16
+ suggestedLoopConfig?: { maxIterations: number; timeBudgetMs?: number };
17
+ suggestedSwarmConfig?: { workerConcurrencyLimit?: number };
8
18
  }
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { readFileSync } from "fs";
3
+ import { join } from "path";
4
+ import * as schema from "@/lib/db/schema";
5
+
6
+ /**
7
+ * Safety-net test: every table exported from schema.ts must appear in clear.ts
8
+ * (except `settings`, which is intentionally preserved across clears).
9
+ *
10
+ * When you add a new table to schema.ts, this test will fail until you add a
11
+ * corresponding db.delete() call to clear.ts in the correct FK-safe order.
12
+ */
13
+ describe("clearAllData coverage", () => {
14
+ const INTENTIONALLY_PRESERVED = ["settings"];
15
+
16
+ it("deletes every schema table (except settings)", () => {
17
+ const clearSource = readFileSync(
18
+ join(__dirname, "..", "clear.ts"),
19
+ "utf-8"
20
+ );
21
+
22
+ // Collect all sqliteTable exports from schema
23
+ const tableExports = Object.entries(schema)
24
+ .filter(
25
+ ([, value]) =>
26
+ value != null &&
27
+ typeof value === "object" &&
28
+ "getSQL" in (value as Record<string, unknown>)
29
+ )
30
+ .map(([name]) => name);
31
+
32
+ expect(tableExports.length).toBeGreaterThan(0);
33
+
34
+ const missing = tableExports.filter(
35
+ (name) =>
36
+ !INTENTIONALLY_PRESERVED.includes(name) &&
37
+ !clearSource.includes(`db.delete(${name})`)
38
+ );
39
+
40
+ expect(missing, `Tables missing from clear.ts: ${missing.join(", ")}`).toEqual([]);
41
+ });
42
+ });