stagent 0.1.9 → 0.1.11

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 (81) hide show
  1. package/README.md +144 -62
  2. package/package.json +1 -2
  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/profiles/route.ts +0 -1
  27. package/src/app/api/workflows/from-assist/route.ts +143 -0
  28. package/src/app/dashboard/page.tsx +24 -2
  29. package/src/app/globals.css +0 -5
  30. package/src/app/tasks/page.tsx +5 -0
  31. package/src/app/workflows/from-assist/page.tsx +35 -0
  32. package/src/components/profiles/profile-detail-view.tsx +1 -16
  33. package/src/components/profiles/profile-form-view.tsx +0 -22
  34. package/src/components/projects/project-card.tsx +47 -35
  35. package/src/components/tasks/ai-assist-panel.tsx +31 -10
  36. package/src/components/tasks/task-card.tsx +16 -1
  37. package/src/components/tasks/task-create-panel.tsx +39 -0
  38. package/src/components/workflows/workflow-confirmation-view.tsx +447 -0
  39. package/src/lib/agents/__tests__/claude-agent.test.ts +7 -2
  40. package/src/lib/agents/__tests__/learned-context.test.ts +500 -0
  41. package/src/lib/agents/__tests__/pattern-extractor.test.ts +243 -0
  42. package/src/lib/agents/__tests__/sweep.test.ts +202 -0
  43. package/src/lib/agents/claude-agent.ts +104 -78
  44. package/src/lib/agents/learned-context.ts +5 -13
  45. package/src/lib/agents/pattern-extractor.ts +15 -64
  46. package/src/lib/agents/profiles/__tests__/suggest.test.ts +67 -0
  47. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +0 -1
  48. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +0 -1
  49. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +0 -1
  50. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +0 -1
  51. package/src/lib/agents/profiles/builtins/general/profile.yaml +0 -1
  52. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +0 -1
  53. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +0 -1
  54. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +0 -1
  55. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +0 -1
  56. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +0 -1
  57. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +0 -1
  58. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +0 -1
  59. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +0 -1
  60. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +0 -1
  61. package/src/lib/agents/profiles/registry.ts +0 -1
  62. package/src/lib/agents/profiles/suggest.ts +36 -0
  63. package/src/lib/agents/profiles/types.ts +0 -1
  64. package/src/lib/agents/runtime/catalog.ts +1 -1
  65. package/src/lib/agents/runtime/claude.ts +102 -6
  66. package/src/lib/agents/runtime/task-assist-types.ts +12 -2
  67. package/src/lib/constants/task-status.ts +6 -0
  68. package/src/lib/data/__tests__/clear.test.ts +42 -0
  69. package/src/lib/data/clear.ts +3 -0
  70. package/src/lib/data/seed-data/profiles.ts +0 -3
  71. package/src/lib/notifications/permissions.ts +6 -2
  72. package/src/lib/usage/__tests__/ledger.test.ts +29 -5
  73. package/src/lib/usage/ledger.ts +3 -1
  74. package/src/lib/usage/pricing.ts +61 -7
  75. package/src/lib/validators/__tests__/profile.test.ts +0 -15
  76. package/src/lib/validators/profile.ts +0 -1
  77. package/src/lib/workflows/__tests__/assist-builder.test.ts +255 -0
  78. package/src/lib/workflows/__tests__/engine.test.ts +2 -0
  79. package/src/lib/workflows/assist-builder.ts +248 -0
  80. package/src/lib/workflows/assist-session.ts +78 -0
  81. package/src/lib/workflows/engine.ts +47 -1
@@ -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,
@@ -149,6 +149,11 @@ export function getPermissionDetailEntries(
149
149
  export function getPermissionResponseLabel(response: string | null): string | null {
150
150
  if (!response) return null;
151
151
 
152
+ // Handle legacy plain-string responses (pre-JSON format)
153
+ const legacy = response.toLowerCase();
154
+ if (legacy === "approved" || legacy === "allowed") return "Allowed";
155
+ if (legacy === "denied" || legacy === "rejected") return "Denied";
156
+
152
157
  try {
153
158
  const parsed = JSON.parse(response) as {
154
159
  behavior?: "allow" | "deny";
@@ -164,8 +169,7 @@ export function getPermissionResponseLabel(response: string | null): string | nu
164
169
  }
165
170
 
166
171
  return null;
167
- } catch (err) {
168
- console.error("[permissions] Failed to parse permission response:", err);
172
+ } catch {
169
173
  return null;
170
174
  }
171
175
  }
@@ -33,9 +33,10 @@ function formatLocalDay(date: Date) {
33
33
  }
34
34
 
35
35
  describe("usage ledger", () => {
36
- it("records normalized ledger rows with derived and unknown pricing states", async () => {
36
+ it("records normalized ledger rows with derived, fallback, and unknown pricing states", async () => {
37
37
  const { db, usageLedger, recordUsageLedgerEntry } = await loadUsageModules();
38
38
 
39
+ // Known model — gets specific pricing rule
39
40
  await recordUsageLedgerEntry({
40
41
  activityType: "task_assist",
41
42
  runtimeId: "claude-code",
@@ -49,6 +50,7 @@ describe("usage ledger", () => {
49
50
  finishedAt: new Date("2026-03-10T08:01:00.000Z"),
50
51
  });
51
52
 
53
+ // Unknown model — hits catch-all fallback pricing (conservative estimate)
52
54
  await recordUsageLedgerEntry({
53
55
  activityType: "task_assist",
54
56
  runtimeId: "openai-codex-app-server",
@@ -62,15 +64,37 @@ describe("usage ledger", () => {
62
64
  finishedAt: new Date("2026-03-10T09:01:00.000Z"),
63
65
  });
64
66
 
67
+ // Null modelId — gets unknown_pricing (no model to match)
68
+ await recordUsageLedgerEntry({
69
+ activityType: "task_run",
70
+ runtimeId: "claude-code",
71
+ providerId: "anthropic",
72
+ modelId: null,
73
+ inputTokens: 100,
74
+ outputTokens: 50,
75
+ totalTokens: 150,
76
+ status: "completed",
77
+ startedAt: new Date("2026-03-10T10:00:00.000Z"),
78
+ finishedAt: new Date("2026-03-10T10:01:00.000Z"),
79
+ });
80
+
65
81
  const rows = await db.select().from(usageLedger);
66
- expect(rows).toHaveLength(2);
82
+ expect(rows).toHaveLength(3);
67
83
 
68
- const priced = rows.find((row) => row.providerId === "anthropic");
84
+ // Known: specific pricing
85
+ const priced = rows.find((row) => row.modelId === "claude-sonnet-4-20250514");
69
86
  expect(priced?.costMicros).toBe(10_500);
70
87
  expect(priced?.status).toBe("completed");
71
- expect(priced?.pricingVersion).toBe("registry-2026-03-12");
88
+ expect(priced?.pricingVersion).toBe("registry-2026-03-15");
89
+
90
+ // Unknown model: fallback pricing (conservative Opus-tier for OpenAI: $10/$30)
91
+ const fallback = rows.find((row) => row.modelId === "codex-unknown");
92
+ expect(fallback?.costMicros).toBeGreaterThan(0);
93
+ expect(fallback?.status).toBe("completed");
94
+ expect(fallback?.pricingVersion).toBe("registry-2026-03-15-fallback");
72
95
 
73
- const unknown = rows.find((row) => row.providerId === "openai");
96
+ // Null modelId: truly unknown
97
+ const unknown = rows.find((row) => row.modelId === null);
74
98
  expect(unknown?.costMicros).toBeNull();
75
99
  expect(unknown?.status).toBe("unknown_pricing");
76
100
  expect(unknown?.pricingVersion).toBeNull();
@@ -15,7 +15,9 @@ export type UsageActivityType =
15
15
  | "workflow_step"
16
16
  | "scheduled_firing"
17
17
  | "task_assist"
18
- | "profile_test";
18
+ | "profile_test"
19
+ | "pattern_extraction"
20
+ | "context_summarization";
19
21
 
20
22
  export type UsageLedgerStatus =
21
23
  | "completed"
@@ -7,25 +7,79 @@ export interface PricingRule {
7
7
  }
8
8
 
9
9
  const PRICING_RULES: PricingRule[] = [
10
+ // ── Anthropic ──────────────────────────────────────────────────────
10
11
  {
11
12
  providerId: "anthropic",
12
- pricingVersion: "registry-2026-03-12",
13
+ pricingVersion: "registry-2026-03-15",
14
+ inputCostPerMillionMicros: 15_000_000,
15
+ outputCostPerMillionMicros: 75_000_000,
16
+ matchesModel(modelId) {
17
+ return modelId.startsWith("claude-opus");
18
+ },
19
+ },
20
+ {
21
+ providerId: "anthropic",
22
+ pricingVersion: "registry-2026-03-15",
13
23
  inputCostPerMillionMicros: 3_000_000,
14
24
  outputCostPerMillionMicros: 15_000_000,
15
25
  matchesModel(modelId) {
16
- return (
17
- modelId === "claude-sonnet-4-20250514" ||
18
- modelId.startsWith("claude-sonnet-4")
19
- );
26
+ return modelId.startsWith("claude-sonnet");
27
+ },
28
+ },
29
+ {
30
+ providerId: "anthropic",
31
+ pricingVersion: "registry-2026-03-15",
32
+ inputCostPerMillionMicros: 800_000,
33
+ outputCostPerMillionMicros: 4_000_000,
34
+ matchesModel(modelId) {
35
+ return modelId.startsWith("claude-haiku");
20
36
  },
21
37
  },
38
+ // ── OpenAI ─────────────────────────────────────────────────────────
22
39
  {
23
40
  providerId: "openai",
24
- pricingVersion: "registry-2026-03-12",
41
+ pricingVersion: "registry-2026-03-15",
25
42
  inputCostPerMillionMicros: 1_500_000,
26
43
  outputCostPerMillionMicros: 6_000_000,
27
44
  matchesModel(modelId) {
28
- return modelId === "codex-mini-latest" || modelId.startsWith("codex-mini");
45
+ return modelId.startsWith("codex-mini") || modelId === "codex-mini-latest";
46
+ },
47
+ },
48
+ {
49
+ providerId: "openai",
50
+ pricingVersion: "registry-2026-03-15",
51
+ inputCostPerMillionMicros: 2_500_000,
52
+ outputCostPerMillionMicros: 10_000_000,
53
+ matchesModel(modelId) {
54
+ return modelId.startsWith("gpt-4o");
55
+ },
56
+ },
57
+ {
58
+ providerId: "openai",
59
+ pricingVersion: "registry-2026-03-15",
60
+ inputCostPerMillionMicros: 10_000_000,
61
+ outputCostPerMillionMicros: 30_000_000,
62
+ matchesModel(modelId) {
63
+ return modelId.startsWith("gpt-5") || modelId.startsWith("o3") || modelId.startsWith("o4");
64
+ },
65
+ },
66
+ // ── Catch-all (conservative estimate to prevent null costs) ────────
67
+ {
68
+ providerId: "anthropic",
69
+ pricingVersion: "registry-2026-03-15-fallback",
70
+ inputCostPerMillionMicros: 15_000_000,
71
+ outputCostPerMillionMicros: 75_000_000,
72
+ matchesModel() {
73
+ return true;
74
+ },
75
+ },
76
+ {
77
+ providerId: "openai",
78
+ pricingVersion: "registry-2026-03-15-fallback",
79
+ inputCostPerMillionMicros: 10_000_000,
80
+ outputCostPerMillionMicros: 30_000_000,
81
+ matchesModel() {
82
+ return true;
29
83
  },
30
84
  },
31
85
  ];
@@ -28,7 +28,6 @@ describe("ProfileConfigSchema", () => {
28
28
  preToolCall: ["echo pre"],
29
29
  postToolCall: ["echo post"],
30
30
  },
31
- temperature: 0.5,
32
31
  maxTurns: 20,
33
32
  outputFormat: "markdown",
34
33
  author: "stagent",
@@ -77,20 +76,6 @@ describe("ProfileConfigSchema", () => {
77
76
  expect(result.success).toBe(false);
78
77
  });
79
78
 
80
- it("rejects temperature out of range", () => {
81
- const tooHigh = ProfileConfigSchema.safeParse({
82
- ...validProfile,
83
- temperature: 1.5,
84
- });
85
- expect(tooHigh.success).toBe(false);
86
-
87
- const tooLow = ProfileConfigSchema.safeParse({
88
- ...validProfile,
89
- temperature: -0.1,
90
- });
91
- expect(tooLow.success).toBe(false);
92
- });
93
-
94
79
  it("rejects invalid source URL", () => {
95
80
  const result = ProfileConfigSchema.safeParse({
96
81
  ...validProfile,
@@ -38,7 +38,6 @@ export const ProfileConfigSchema = z.object({
38
38
  postToolCall: z.array(z.string()).optional(),
39
39
  })
40
40
  .optional(),
41
- temperature: z.number().min(0).max(1).optional(),
42
41
  maxTurns: z.number().positive().optional(),
43
42
  outputFormat: z.string().optional(),
44
43
  author: z.string().optional(),
@@ -0,0 +1,255 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { buildWorkflowDefinitionFromAssist } from "../assist-builder";
3
+ import type { TaskAssistResponse } from "@/lib/agents/runtime/task-assist-types";
4
+
5
+ const MAIN_TASK = {
6
+ title: "Build Auth System",
7
+ description: "Implement authentication with OAuth2",
8
+ agentProfile: "general",
9
+ };
10
+
11
+ function makeAssistResponse(
12
+ overrides: Partial<TaskAssistResponse> = {}
13
+ ): TaskAssistResponse {
14
+ return {
15
+ improvedDescription: "Build a complete auth system",
16
+ breakdown: [
17
+ { title: "Set up middleware", description: "Create auth middleware" },
18
+ { title: "Create endpoints", description: "Build user API endpoints" },
19
+ { title: "Write tests", description: "Integration tests for auth" },
20
+ ],
21
+ recommendedPattern: "sequence",
22
+ complexity: "complex",
23
+ needsCheckpoint: false,
24
+ reasoning: "Multi-step ordered work",
25
+ ...overrides,
26
+ };
27
+ }
28
+
29
+ describe("buildWorkflowDefinitionFromAssist", () => {
30
+ describe("sequence pattern", () => {
31
+ it("creates a sequence workflow with main task as step 1", () => {
32
+ const result = buildWorkflowDefinitionFromAssist({
33
+ mainTask: MAIN_TASK,
34
+ assistResponse: makeAssistResponse(),
35
+ });
36
+
37
+ expect(result.pattern).toBe("sequence");
38
+ expect(result.steps).toHaveLength(4); // main + 3 breakdown
39
+ expect(result.steps[0].name).toBe("Build Auth System");
40
+ expect(result.steps[1].name).toBe("Set up middleware");
41
+ expect(result.steps[3].name).toBe("Write tests");
42
+ });
43
+
44
+ it("assigns profiles from main task and suggestions", () => {
45
+ const result = buildWorkflowDefinitionFromAssist({
46
+ mainTask: MAIN_TASK,
47
+ assistResponse: makeAssistResponse({
48
+ breakdown: [
49
+ { title: "Research", description: "Research patterns", suggestedProfile: "researcher" },
50
+ { title: "Code", description: "Write code" },
51
+ ],
52
+ }),
53
+ });
54
+
55
+ expect(result.steps[0].agentProfile).toBe("general"); // from mainTask
56
+ expect(result.steps[1].agentProfile).toBe("researcher"); // from suggestion
57
+ expect(result.steps[2].agentProfile).toBeUndefined(); // no suggestion = undefined
58
+ });
59
+ });
60
+
61
+ describe("checkpoint pattern", () => {
62
+ it("preserves requiresApproval on steps", () => {
63
+ const result = buildWorkflowDefinitionFromAssist({
64
+ mainTask: MAIN_TASK,
65
+ assistResponse: makeAssistResponse({
66
+ recommendedPattern: "checkpoint",
67
+ breakdown: [
68
+ { title: "Plan", description: "Plan deployment", requiresApproval: true },
69
+ { title: "Deploy", description: "Execute deployment" },
70
+ ],
71
+ }),
72
+ });
73
+
74
+ expect(result.pattern).toBe("checkpoint");
75
+ expect(result.steps[1].requiresApproval).toBe(true);
76
+ expect(result.steps[2].requiresApproval).toBeUndefined();
77
+ });
78
+ });
79
+
80
+ describe("parallel pattern", () => {
81
+ it("auto-generates synthesis step when none provided", () => {
82
+ const result = buildWorkflowDefinitionFromAssist({
83
+ mainTask: MAIN_TASK,
84
+ assistResponse: makeAssistResponse({
85
+ recommendedPattern: "parallel",
86
+ breakdown: [
87
+ { title: "Branch A", description: "Research area A" },
88
+ { title: "Branch B", description: "Research area B" },
89
+ ],
90
+ }),
91
+ });
92
+
93
+ expect(result.pattern).toBe("parallel");
94
+ // main + 2 branches + auto-synthesis = 4
95
+ expect(result.steps).toHaveLength(4);
96
+ expect(result.steps[3].name).toBe("Synthesize results");
97
+ expect(result.steps[3].dependsOn).toEqual(["step_1", "step_2", "step_3"]);
98
+ });
99
+
100
+ it("preserves explicit synthesis step with dependsOn", () => {
101
+ const result = buildWorkflowDefinitionFromAssist({
102
+ mainTask: MAIN_TASK,
103
+ assistResponse: makeAssistResponse({
104
+ recommendedPattern: "parallel",
105
+ breakdown: [
106
+ { title: "Branch A", description: "Research A" },
107
+ { title: "Merge", description: "Merge results", dependsOn: [0, 1] },
108
+ ],
109
+ }),
110
+ });
111
+
112
+ // main + Branch A + Merge = 3 (no auto-synthesis because dependsOn exists)
113
+ expect(result.steps).toHaveLength(3);
114
+ expect(result.steps[2].dependsOn).toEqual(["step_1", "step_2"]);
115
+ });
116
+ });
117
+
118
+ describe("loop pattern", () => {
119
+ it("creates single-step loop with config", () => {
120
+ const result = buildWorkflowDefinitionFromAssist({
121
+ mainTask: MAIN_TASK,
122
+ assistResponse: makeAssistResponse({
123
+ recommendedPattern: "loop",
124
+ suggestedLoopConfig: { maxIterations: 3, timeBudgetMs: 60000 },
125
+ }),
126
+ });
127
+
128
+ expect(result.pattern).toBe("loop");
129
+ expect(result.steps).toHaveLength(1);
130
+ expect(result.loopConfig?.maxIterations).toBe(3);
131
+ expect(result.loopConfig?.timeBudgetMs).toBe(60000);
132
+ });
133
+
134
+ it("defaults to 5 iterations", () => {
135
+ const result = buildWorkflowDefinitionFromAssist({
136
+ mainTask: MAIN_TASK,
137
+ assistResponse: makeAssistResponse({ recommendedPattern: "loop" }),
138
+ });
139
+
140
+ expect(result.loopConfig?.maxIterations).toBe(5);
141
+ });
142
+
143
+ it("applies loop config overrides", () => {
144
+ const result = buildWorkflowDefinitionFromAssist({
145
+ mainTask: MAIN_TASK,
146
+ assistResponse: makeAssistResponse({
147
+ recommendedPattern: "loop",
148
+ suggestedLoopConfig: { maxIterations: 3 },
149
+ }),
150
+ overrides: { loopConfig: { maxIterations: 10 } },
151
+ });
152
+
153
+ expect(result.loopConfig?.maxIterations).toBe(10);
154
+ });
155
+ });
156
+
157
+ describe("swarm pattern", () => {
158
+ it("creates mayor/workers/refinery structure", () => {
159
+ const result = buildWorkflowDefinitionFromAssist({
160
+ mainTask: MAIN_TASK,
161
+ assistResponse: makeAssistResponse({
162
+ recommendedPattern: "swarm",
163
+ breakdown: [
164
+ { title: "Worker 1", description: "Task 1" },
165
+ { title: "Worker 2", description: "Task 2" },
166
+ ],
167
+ }),
168
+ });
169
+
170
+ expect(result.pattern).toBe("swarm");
171
+ // mayor + 2 workers + refinery = 4
172
+ expect(result.steps).toHaveLength(4);
173
+ expect(result.steps[0].name).toBe("Build Auth System"); // mayor
174
+ expect(result.steps[3].name).toBe("Refine and merge results"); // refinery
175
+ expect(result.swarmConfig?.workerConcurrencyLimit).toBe(2);
176
+ });
177
+
178
+ it("applies swarm config overrides", () => {
179
+ const result = buildWorkflowDefinitionFromAssist({
180
+ mainTask: MAIN_TASK,
181
+ assistResponse: makeAssistResponse({
182
+ recommendedPattern: "swarm",
183
+ breakdown: [
184
+ { title: "W1", description: "T1" },
185
+ { title: "W2", description: "T2" },
186
+ ],
187
+ suggestedSwarmConfig: { workerConcurrencyLimit: 1 },
188
+ }),
189
+ overrides: { swarmConfig: { workerConcurrencyLimit: 2 } },
190
+ });
191
+
192
+ expect(result.swarmConfig?.workerConcurrencyLimit).toBe(2);
193
+ });
194
+ });
195
+
196
+ describe("pattern override", () => {
197
+ it("overrides AI-recommended pattern", () => {
198
+ const result = buildWorkflowDefinitionFromAssist({
199
+ mainTask: MAIN_TASK,
200
+ assistResponse: makeAssistResponse({ recommendedPattern: "sequence" }),
201
+ overrides: { pattern: "checkpoint" },
202
+ });
203
+
204
+ expect(result.pattern).toBe("checkpoint");
205
+ });
206
+ });
207
+
208
+ describe("step overrides", () => {
209
+ it("applies partial step overrides", () => {
210
+ const result = buildWorkflowDefinitionFromAssist({
211
+ mainTask: MAIN_TASK,
212
+ assistResponse: makeAssistResponse(),
213
+ overrides: {
214
+ steps: [
215
+ undefined,
216
+ { agentProfile: "code-reviewer" },
217
+ ] as Partial<import("../types").WorkflowStep>[],
218
+ },
219
+ });
220
+
221
+ expect(result.steps[1].agentProfile).toBe("code-reviewer");
222
+ });
223
+ });
224
+
225
+ describe("validation", () => {
226
+ it("throws on invalid definition", () => {
227
+ expect(() =>
228
+ buildWorkflowDefinitionFromAssist({
229
+ mainTask: MAIN_TASK,
230
+ assistResponse: makeAssistResponse({
231
+ recommendedPattern: "loop",
232
+ // Missing loopConfig
233
+ }),
234
+ overrides: { loopConfig: { maxIterations: 0 } },
235
+ })
236
+ ).toThrow("Invalid workflow definition");
237
+ });
238
+ });
239
+
240
+ describe("auto profile handling", () => {
241
+ it('treats "auto" suggestedProfile as undefined', () => {
242
+ const result = buildWorkflowDefinitionFromAssist({
243
+ mainTask: { ...MAIN_TASK, agentProfile: undefined },
244
+ assistResponse: makeAssistResponse({
245
+ breakdown: [
246
+ { title: "Step", description: "Do thing", suggestedProfile: "auto" },
247
+ ],
248
+ }),
249
+ });
250
+
251
+ expect(result.steps[0].agentProfile).toBeUndefined();
252
+ expect(result.steps[1].agentProfile).toBeUndefined();
253
+ });
254
+ });
255
+ });
@@ -103,6 +103,8 @@ describe("executeWorkflow", () => {
103
103
  .mockResolvedValueOnce([workflow])
104
104
  .mockResolvedValueOnce([failedTask])
105
105
  .mockResolvedValueOnce([workflow])
106
+ .mockResolvedValueOnce([workflow])
107
+ // syncSourceTaskStatus reads the workflow to find sourceTaskId
106
108
  .mockResolvedValueOnce([workflow]);
107
109
 
108
110
  const { executeWorkflow } = await import("../engine");