stagent 0.1.0 → 0.1.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 (64) hide show
  1. package/README.md +33 -30
  2. package/dist/cli.js +376 -49
  3. package/package.json +20 -21
  4. package/public/desktop-icon-512.png +0 -0
  5. package/public/icon-512.png +0 -0
  6. package/src/app/api/data/clear/route.ts +0 -7
  7. package/src/app/api/data/seed/route.ts +0 -7
  8. package/src/app/api/profiles/[id]/context/route.ts +109 -0
  9. package/src/components/dashboard/__tests__/accessibility.test.tsx +42 -0
  10. package/src/components/documents/__tests__/document-upload-dialog.test.tsx +46 -0
  11. package/src/components/notifications/__tests__/pending-approval-host.test.tsx +122 -0
  12. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +79 -0
  13. package/src/components/notifications/pending-approval-host.tsx +49 -25
  14. package/src/components/profiles/context-proposal-review.tsx +145 -0
  15. package/src/components/profiles/learned-context-panel.tsx +286 -0
  16. package/src/components/profiles/profile-detail-view.tsx +4 -0
  17. package/src/components/projects/__tests__/dialog-focus.test.tsx +87 -0
  18. package/src/components/tasks/__tests__/kanban-board-accessibility.test.tsx +59 -0
  19. package/src/lib/__tests__/setup-verify.test.ts +28 -0
  20. package/src/lib/__tests__/utils.test.ts +29 -0
  21. package/src/lib/agents/__tests__/claude-agent.test.ts +946 -0
  22. package/src/lib/agents/__tests__/execution-manager.test.ts +63 -0
  23. package/src/lib/agents/__tests__/router.test.ts +61 -0
  24. package/src/lib/agents/claude-agent.ts +34 -5
  25. package/src/lib/agents/learned-context.ts +322 -0
  26. package/src/lib/agents/pattern-extractor.ts +150 -0
  27. package/src/lib/agents/profiles/__tests__/compatibility.test.ts +76 -0
  28. package/src/lib/agents/profiles/__tests__/registry.test.ts +177 -0
  29. package/src/lib/agents/profiles/builtins/sweep/SKILL.md +47 -0
  30. package/src/lib/agents/profiles/builtins/sweep/profile.yaml +12 -0
  31. package/src/lib/agents/runtime/__tests__/catalog.test.ts +38 -0
  32. package/src/lib/agents/runtime/openai-codex.ts +1 -1
  33. package/src/lib/agents/sweep.ts +65 -0
  34. package/src/lib/constants/__tests__/task-status.test.ts +119 -0
  35. package/src/lib/data/seed-data/__tests__/profiles.test.ts +141 -0
  36. package/src/lib/db/__tests__/bootstrap.test.ts +56 -0
  37. package/src/lib/db/bootstrap.ts +301 -0
  38. package/src/lib/db/index.ts +2 -205
  39. package/src/lib/db/migrations/0004_add_documents.sql +2 -1
  40. package/src/lib/db/migrations/0005_add_document_preprocessing.sql +2 -0
  41. package/src/lib/db/migrations/0006_add_agent_profile.sql +1 -0
  42. package/src/lib/db/migrations/0007_add_usage_metering_ledger.sql +9 -2
  43. package/src/lib/db/migrations/meta/_journal.json +43 -1
  44. package/src/lib/db/schema.ts +34 -0
  45. package/src/lib/desktop/__tests__/sidecar-launch.test.ts +70 -0
  46. package/src/lib/desktop/sidecar-launch.ts +85 -0
  47. package/src/lib/documents/__tests__/context-builder.test.ts +57 -0
  48. package/src/lib/documents/__tests__/output-scanner.test.ts +141 -0
  49. package/src/lib/notifications/actionable.ts +21 -7
  50. package/src/lib/settings/__tests__/auth.test.ts +220 -0
  51. package/src/lib/settings/__tests__/budget-guardrails.test.ts +181 -0
  52. package/src/lib/tauri-bridge.ts +138 -0
  53. package/src/lib/usage/__tests__/ledger.test.ts +284 -0
  54. package/src/lib/utils/__tests__/crypto.test.ts +90 -0
  55. package/src/lib/validators/__tests__/profile.test.ts +119 -0
  56. package/src/lib/validators/__tests__/project.test.ts +82 -0
  57. package/src/lib/validators/__tests__/settings.test.ts +151 -0
  58. package/src/lib/validators/__tests__/task.test.ts +144 -0
  59. package/src/lib/workflows/__tests__/definition-validation.test.ts +164 -0
  60. package/src/lib/workflows/__tests__/engine.test.ts +114 -0
  61. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  62. package/src/lib/workflows/__tests__/parallel.test.ts +75 -0
  63. package/src/lib/workflows/__tests__/swarm.test.ts +97 -0
  64. package/src/test/setup.ts +10 -0
@@ -0,0 +1,114 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const {
4
+ mockWhere,
5
+ mockFrom,
6
+ mockSelect,
7
+ mockUpdateWhere,
8
+ mockSet,
9
+ mockUpdate,
10
+ mockInsertValues,
11
+ mockInsert,
12
+ } = vi.hoisted(() => {
13
+ const mockWhere = vi.fn();
14
+ const mockFrom = vi.fn().mockReturnValue({ where: mockWhere });
15
+ const mockSelect = vi.fn().mockReturnValue({ from: mockFrom });
16
+ const mockUpdateWhere = vi.fn().mockResolvedValue(undefined);
17
+ const mockSet = vi.fn().mockReturnValue({ where: mockUpdateWhere });
18
+ const mockUpdate = vi.fn().mockReturnValue({ set: mockSet });
19
+ const mockInsertValues = vi.fn().mockResolvedValue(undefined);
20
+ const mockInsert = vi.fn().mockReturnValue({ values: mockInsertValues });
21
+
22
+ return {
23
+ mockWhere,
24
+ mockFrom,
25
+ mockSelect,
26
+ mockUpdateWhere,
27
+ mockSet,
28
+ mockUpdate,
29
+ mockInsertValues,
30
+ mockInsert,
31
+ };
32
+ });
33
+
34
+ const { mockExecuteTaskWithRuntime } = vi.hoisted(() => ({
35
+ mockExecuteTaskWithRuntime: vi.fn().mockResolvedValue(undefined),
36
+ }));
37
+
38
+ vi.mock("@/lib/db", () => ({
39
+ db: {
40
+ select: mockSelect,
41
+ update: mockUpdate,
42
+ insert: mockInsert,
43
+ },
44
+ }));
45
+
46
+ vi.mock("@/lib/db/schema", () => ({
47
+ workflows: {
48
+ id: "workflows.id",
49
+ },
50
+ tasks: {
51
+ id: "tasks.id",
52
+ },
53
+ agentLogs: {},
54
+ notifications: {},
55
+ }));
56
+
57
+ vi.mock("drizzle-orm", () => ({
58
+ eq: vi.fn((column: string, value: unknown) => ({ column, value })),
59
+ }));
60
+
61
+ vi.mock("@/lib/agents/runtime", () => ({
62
+ executeTaskWithRuntime: mockExecuteTaskWithRuntime,
63
+ }));
64
+
65
+ describe("executeWorkflow", () => {
66
+ beforeEach(() => {
67
+ mockWhere.mockReset();
68
+ mockFrom.mockClear();
69
+ mockSelect.mockClear();
70
+ mockUpdateWhere.mockReset();
71
+ mockSet.mockReset();
72
+ mockSet.mockReturnValue({ where: mockUpdateWhere });
73
+ mockUpdate.mockClear();
74
+ mockInsertValues.mockReset();
75
+ mockInsertValues.mockResolvedValue(undefined);
76
+ mockInsert.mockClear();
77
+ mockExecuteTaskWithRuntime.mockClear();
78
+ mockExecuteTaskWithRuntime.mockResolvedValue(undefined);
79
+ });
80
+
81
+ it("persists failed workflow executions with failed top-level status", async () => {
82
+ const workflowId = "workflow-1";
83
+ const workflow = {
84
+ id: workflowId,
85
+ name: "Parallel workflow",
86
+ projectId: null,
87
+ definition: JSON.stringify({
88
+ pattern: "sequence",
89
+ steps: [{ id: "step-1", name: "Step 1", prompt: "Do the work." }],
90
+ }),
91
+ status: "draft",
92
+ };
93
+ const failedTask = {
94
+ id: "task-1",
95
+ status: "failed",
96
+ result: "Provider runtime error",
97
+ };
98
+
99
+ mockWhere
100
+ .mockResolvedValueOnce([workflow])
101
+ .mockResolvedValueOnce([workflow])
102
+ .mockResolvedValueOnce([workflow])
103
+ .mockResolvedValueOnce([workflow])
104
+ .mockResolvedValueOnce([failedTask])
105
+ .mockResolvedValueOnce([workflow])
106
+ .mockResolvedValueOnce([workflow]);
107
+
108
+ const { executeWorkflow } = await import("../engine");
109
+
110
+ await executeWorkflow(workflowId);
111
+
112
+ expect(mockSet.mock.calls.at(-1)?.[0]).toMatchObject({ status: "failed" });
113
+ });
114
+ });
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ buildIterationPrompt,
4
+ detectCompletionSignal,
5
+ } from "../loop-executor";
6
+
7
+ describe("buildIterationPrompt", () => {
8
+ it("formats the first iteration without previous output", () => {
9
+ const result = buildIterationPrompt("Write a poem", "", 1, 5);
10
+ expect(result).toContain("Iteration 1 of 5.");
11
+ expect(result).toContain("Write a poem");
12
+ expect(result).toContain("LOOP_COMPLETE");
13
+ expect(result).not.toContain("Previous iteration output:");
14
+ });
15
+
16
+ it("includes previous output for subsequent iterations", () => {
17
+ const result = buildIterationPrompt("Write a poem", "Roses are red...", 2, 5);
18
+ expect(result).toContain("Iteration 2 of 5.");
19
+ expect(result).toContain("Previous iteration output:\nRoses are red...");
20
+ expect(result).toContain("Write a poem");
21
+ expect(result).toContain("LOOP_COMPLETE");
22
+ });
23
+
24
+ it("shows correct iteration and max in the header", () => {
25
+ const result = buildIterationPrompt("Do something", "", 7, 10);
26
+ expect(result).toContain("Iteration 7 of 10.");
27
+ });
28
+ });
29
+
30
+ describe("detectCompletionSignal", () => {
31
+ it("detects LOOP_COMPLETE by default", () => {
32
+ expect(detectCompletionSignal("Result: all done. LOOP_COMPLETE")).toBe(true);
33
+ });
34
+
35
+ it("is case-insensitive", () => {
36
+ expect(detectCompletionSignal("loop_complete")).toBe(true);
37
+ expect(detectCompletionSignal("Loop_Complete")).toBe(true);
38
+ });
39
+
40
+ it("returns false when signal is absent", () => {
41
+ expect(detectCompletionSignal("Still working on it...")).toBe(false);
42
+ });
43
+
44
+ it("detects custom signals", () => {
45
+ const signals = ["DONE", "FINISHED"];
46
+ expect(detectCompletionSignal("I am DONE now.", signals)).toBe(true);
47
+ expect(detectCompletionSignal("Task finished successfully", signals)).toBe(true);
48
+ expect(detectCompletionSignal("Not there yet", signals)).toBe(false);
49
+ });
50
+
51
+ it("uses defaults when signals array is empty", () => {
52
+ expect(detectCompletionSignal("LOOP_COMPLETE", [])).toBe(true);
53
+ });
54
+ });
@@ -0,0 +1,75 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { WorkflowDefinition } from "../types";
3
+ import {
4
+ buildParallelSynthesisPrompt,
5
+ getParallelWorkflowStructure,
6
+ } from "../parallel";
7
+
8
+ describe("getParallelWorkflowStructure", () => {
9
+ it("extracts branch and synthesis steps from a parallel workflow", () => {
10
+ const definition: WorkflowDefinition = {
11
+ pattern: "parallel",
12
+ steps: [
13
+ {
14
+ id: "branch-a",
15
+ name: "Competitor scan",
16
+ prompt: "Research direct competitors.",
17
+ },
18
+ {
19
+ id: "branch-b",
20
+ name: "User interviews",
21
+ prompt: "Review interview transcripts.",
22
+ },
23
+ {
24
+ id: "join",
25
+ name: "Synthesize",
26
+ prompt: "Combine the branch findings into a recommendation.",
27
+ dependsOn: ["branch-a", "branch-b"],
28
+ },
29
+ ],
30
+ };
31
+
32
+ const structure = getParallelWorkflowStructure(definition);
33
+
34
+ expect(structure?.branchSteps.map((step) => step.id)).toEqual([
35
+ "branch-a",
36
+ "branch-b",
37
+ ]);
38
+ expect(structure?.synthesisStep.id).toBe("join");
39
+ });
40
+
41
+ it("returns null when a parallel workflow does not have exactly one synthesis step", () => {
42
+ const definition: WorkflowDefinition = {
43
+ pattern: "parallel",
44
+ steps: [
45
+ { id: "branch-a", name: "A", prompt: "A" },
46
+ { id: "branch-b", name: "B", prompt: "B" },
47
+ ],
48
+ };
49
+
50
+ expect(getParallelWorkflowStructure(definition)).toBeNull();
51
+ });
52
+ });
53
+
54
+ describe("buildParallelSynthesisPrompt", () => {
55
+ it("formats labeled branch outputs ahead of the synthesis prompt", () => {
56
+ const prompt = buildParallelSynthesisPrompt({
57
+ branchOutputs: [
58
+ {
59
+ stepName: "Competitor scan",
60
+ result: "Competitors cluster around speed and reliability claims.",
61
+ },
62
+ {
63
+ stepName: "Customer interviews",
64
+ result: "Users mostly complain about setup complexity.",
65
+ },
66
+ ],
67
+ synthesisPrompt: "Write a concise synthesis and recommendation.",
68
+ });
69
+
70
+ expect(prompt).toContain("Parallel branch outputs:");
71
+ expect(prompt).toContain("Branch 1 - Competitor scan:");
72
+ expect(prompt).toContain("Branch 2 - Customer interviews:");
73
+ expect(prompt).toContain("Write a concise synthesis and recommendation.");
74
+ });
75
+ });
@@ -0,0 +1,97 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { WorkflowDefinition } from "../types";
3
+ import {
4
+ buildSwarmRefineryPrompt,
5
+ buildSwarmWorkerPrompt,
6
+ getSwarmWorkflowStructure,
7
+ } from "../swarm";
8
+
9
+ function createValidSwarmDefinition(): WorkflowDefinition {
10
+ return {
11
+ pattern: "swarm",
12
+ steps: [
13
+ {
14
+ id: "mayor",
15
+ name: "Mayor plan",
16
+ prompt: "Break the goal into worker assignments.",
17
+ },
18
+ {
19
+ id: "worker-a",
20
+ name: "Worker A",
21
+ prompt: "Handle the customer research slice.",
22
+ },
23
+ {
24
+ id: "worker-b",
25
+ name: "Worker B",
26
+ prompt: "Handle the competitor analysis slice.",
27
+ },
28
+ {
29
+ id: "refinery",
30
+ name: "Refine and merge",
31
+ prompt: "Merge the swarm output into one recommendation.",
32
+ },
33
+ ],
34
+ swarmConfig: {
35
+ workerConcurrencyLimit: 1,
36
+ },
37
+ };
38
+ }
39
+
40
+ describe("getSwarmWorkflowStructure", () => {
41
+ it("extracts mayor, workers, refinery, and concurrency from a swarm workflow", () => {
42
+ const structure = getSwarmWorkflowStructure(createValidSwarmDefinition());
43
+
44
+ expect(structure?.mayorStep.id).toBe("mayor");
45
+ expect(structure?.workerSteps.map((step) => step.id)).toEqual([
46
+ "worker-a",
47
+ "worker-b",
48
+ ]);
49
+ expect(structure?.refineryStep.id).toBe("refinery");
50
+ expect(structure?.workerConcurrencyLimit).toBe(1);
51
+ });
52
+
53
+ it("returns null when a swarm workflow does not have a mayor, workers, and refinery", () => {
54
+ const definition: WorkflowDefinition = {
55
+ pattern: "swarm",
56
+ steps: [
57
+ { id: "mayor", name: "Mayor", prompt: "Plan" },
58
+ { id: "worker", name: "Worker", prompt: "Do one slice" },
59
+ { id: "refinery", name: "Refinery", prompt: "Merge" },
60
+ ],
61
+ };
62
+
63
+ expect(getSwarmWorkflowStructure(definition)).toBeNull();
64
+ });
65
+ });
66
+
67
+ describe("swarm prompt builders", () => {
68
+ it("formats a worker prompt with mayor context", () => {
69
+ const prompt = buildSwarmWorkerPrompt({
70
+ mayorName: "Mayor plan",
71
+ mayorResult: "Worker A handles customers. Worker B handles competitors.",
72
+ workerName: "Worker A",
73
+ workerPrompt: "Analyze customer interviews.",
74
+ });
75
+
76
+ expect(prompt).toContain("Mayor plan:");
77
+ expect(prompt).toContain("Worker A assignment:");
78
+ expect(prompt).toContain("Analyze customer interviews.");
79
+ });
80
+
81
+ it("formats a refinery prompt with labeled worker outputs", () => {
82
+ const prompt = buildSwarmRefineryPrompt({
83
+ mayorName: "Mayor plan",
84
+ mayorResult: "Split the work across two workers.",
85
+ workerOutputs: [
86
+ { stepName: "Worker A", result: "Customer pain points center on setup." },
87
+ { stepName: "Worker B", result: "Competitors compete on speed." },
88
+ ],
89
+ refineryPrompt: "Produce one final recommendation.",
90
+ });
91
+
92
+ expect(prompt).toContain("Worker outputs:");
93
+ expect(prompt).toContain("Worker 1 - Worker A:");
94
+ expect(prompt).toContain("Worker 2 - Worker B:");
95
+ expect(prompt).toContain("Produce one final recommendation.");
96
+ });
97
+ });
@@ -0,0 +1,10 @@
1
+ import "@testing-library/jest-dom/vitest";
2
+ import { mkdtempSync, mkdirSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+
6
+ if (!process.env.STAGENT_DATA_DIR) {
7
+ const tempDataDir = mkdtempSync(join(tmpdir(), "stagent-vitest-"));
8
+ mkdirSync(tempDataDir, { recursive: true });
9
+ process.env.STAGENT_DATA_DIR = tempDataDir;
10
+ }