stagent 0.10.0 → 0.11.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 (176) hide show
  1. package/README.md +44 -31
  2. package/dist/cli.js +24 -0
  3. package/docs/.coverage-gaps.json +154 -24
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +12 -2
  6. package/docs/features/chat.md +40 -5
  7. package/docs/features/cost-usage.md +1 -1
  8. package/docs/features/documents.md +5 -2
  9. package/docs/features/inbox-notifications.md +10 -2
  10. package/docs/features/keyboard-navigation.md +12 -3
  11. package/docs/features/provider-runtimes.md +16 -2
  12. package/docs/features/settings.md +2 -2
  13. package/docs/features/shared-components.md +7 -3
  14. package/docs/features/tables.md +3 -1
  15. package/docs/features/tool-permissions.md +6 -2
  16. package/docs/features/workflows.md +6 -2
  17. package/docs/getting-started.md +1 -1
  18. package/docs/index.md +1 -1
  19. package/docs/journeys/developer.md +25 -2
  20. package/docs/journeys/personal-use.md +12 -5
  21. package/docs/journeys/power-user.md +45 -14
  22. package/docs/journeys/work-use.md +17 -8
  23. package/docs/manifest.json +15 -15
  24. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +2 -2
  25. package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
  26. package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
  27. package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
  28. package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
  29. package/next.config.mjs +1 -0
  30. package/package.json +3 -3
  31. package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
  32. package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
  33. package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
  34. package/src/app/api/chat/export/route.ts +52 -0
  35. package/src/app/api/chat/files/search/route.ts +50 -0
  36. package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
  37. package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
  38. package/src/app/api/environment/skills/route.ts +13 -0
  39. package/src/app/api/schedules/[id]/execute/route.ts +2 -2
  40. package/src/app/api/settings/chat/pins/route.ts +94 -0
  41. package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
  42. package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
  43. package/src/app/api/settings/environment/route.ts +26 -0
  44. package/src/app/api/tasks/[id]/execute/route.ts +52 -12
  45. package/src/app/api/tasks/[id]/respond/route.ts +31 -15
  46. package/src/app/api/tasks/[id]/resume/route.ts +24 -3
  47. package/src/app/documents/page.tsx +4 -1
  48. package/src/app/settings/page.tsx +2 -0
  49. package/src/components/book/content-blocks.tsx +1 -1
  50. package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
  51. package/src/components/chat/__tests__/chat-session-provider.test.tsx +166 -1
  52. package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
  53. package/src/components/chat/capability-banner.tsx +68 -0
  54. package/src/components/chat/chat-command-popover.tsx +668 -47
  55. package/src/components/chat/chat-input.tsx +103 -8
  56. package/src/components/chat/chat-message.tsx +12 -3
  57. package/src/components/chat/chat-session-provider.tsx +73 -3
  58. package/src/components/chat/chat-shell.tsx +62 -3
  59. package/src/components/chat/command-tab-bar.tsx +68 -0
  60. package/src/components/chat/conversation-template-picker.tsx +421 -0
  61. package/src/components/chat/help-dialog.tsx +39 -0
  62. package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
  63. package/src/components/chat/skill-row.tsx +147 -0
  64. package/src/components/documents/document-browser.tsx +37 -19
  65. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
  66. package/src/components/notifications/permission-response-actions.tsx +155 -1
  67. package/src/components/playbook/playbook-detail-view.tsx +1 -1
  68. package/src/components/settings/environment-section.tsx +102 -0
  69. package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
  70. package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
  71. package/src/components/shared/command-palette.tsx +262 -2
  72. package/src/components/shared/filter-hint.tsx +70 -0
  73. package/src/components/shared/filter-input.tsx +59 -0
  74. package/src/components/shared/saved-searches-manager.tsx +199 -0
  75. package/src/components/tasks/task-bento-grid.tsx +12 -2
  76. package/src/components/tasks/task-card.tsx +3 -0
  77. package/src/components/tasks/task-chip-bar.tsx +30 -1
  78. package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
  79. package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
  80. package/src/hooks/use-active-skills.ts +110 -0
  81. package/src/hooks/use-chat-autocomplete.ts +120 -7
  82. package/src/hooks/use-enriched-skills.ts +19 -0
  83. package/src/hooks/use-pinned-entries.ts +104 -0
  84. package/src/hooks/use-recent-user-messages.ts +19 -0
  85. package/src/hooks/use-saved-searches.ts +142 -0
  86. package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
  87. package/src/lib/agents/__tests__/claude-agent.test.ts +17 -4
  88. package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
  89. package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
  90. package/src/lib/agents/claude-agent.ts +105 -46
  91. package/src/lib/agents/handoff/bus.ts +2 -2
  92. package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
  93. package/src/lib/agents/profiles/__tests__/registry.test.ts +47 -0
  94. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +30 -3
  95. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +6 -2
  96. package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
  97. package/src/lib/agents/profiles/registry.ts +97 -22
  98. package/src/lib/agents/profiles/types.ts +7 -1
  99. package/src/lib/agents/router.ts +3 -6
  100. package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
  101. package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
  102. package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
  103. package/src/lib/agents/runtime/catalog.ts +121 -0
  104. package/src/lib/agents/runtime/claude-sdk.ts +32 -0
  105. package/src/lib/agents/runtime/execution-target.ts +456 -0
  106. package/src/lib/agents/runtime/index.ts +4 -0
  107. package/src/lib/agents/runtime/launch-failure.ts +101 -0
  108. package/src/lib/agents/runtime/openai-codex.ts +35 -0
  109. package/src/lib/agents/runtime/openai-direct.ts +8 -0
  110. package/src/lib/agents/task-dispatch.ts +220 -0
  111. package/src/lib/agents/tool-permissions.ts +16 -1
  112. package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
  113. package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
  114. package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
  115. package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
  116. package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
  117. package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
  118. package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
  119. package/src/lib/chat/__tests__/types.test.ts +28 -0
  120. package/src/lib/chat/active-skills.ts +31 -0
  121. package/src/lib/chat/clean-filter-input.ts +30 -0
  122. package/src/lib/chat/codex-engine.ts +30 -7
  123. package/src/lib/chat/command-tabs.ts +61 -0
  124. package/src/lib/chat/context-builder.ts +141 -1
  125. package/src/lib/chat/dismissals.ts +73 -0
  126. package/src/lib/chat/engine.ts +109 -15
  127. package/src/lib/chat/files/__tests__/search.test.ts +135 -0
  128. package/src/lib/chat/files/expand-mention.ts +76 -0
  129. package/src/lib/chat/files/search.ts +99 -0
  130. package/src/lib/chat/skill-composition.ts +210 -0
  131. package/src/lib/chat/skill-conflict.ts +105 -0
  132. package/src/lib/chat/stagent-tools.ts +6 -19
  133. package/src/lib/chat/stream-telemetry.ts +9 -4
  134. package/src/lib/chat/system-prompt.ts +22 -0
  135. package/src/lib/chat/tool-catalog.ts +33 -3
  136. package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
  137. package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
  138. package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
  139. package/src/lib/chat/tools/__tests__/task-tools.test.ts +47 -0
  140. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +134 -0
  141. package/src/lib/chat/tools/blueprint-tools.ts +190 -0
  142. package/src/lib/chat/tools/helpers.ts +2 -0
  143. package/src/lib/chat/tools/profile-tools.ts +120 -23
  144. package/src/lib/chat/tools/skill-tools.ts +183 -0
  145. package/src/lib/chat/tools/task-tools.ts +6 -2
  146. package/src/lib/chat/tools/workflow-tools.ts +61 -20
  147. package/src/lib/chat/types.ts +15 -0
  148. package/src/lib/constants/settings.ts +2 -0
  149. package/src/lib/data/clear.ts +2 -6
  150. package/src/lib/db/bootstrap.ts +17 -0
  151. package/src/lib/db/schema.ts +26 -0
  152. package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
  153. package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
  154. package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
  155. package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
  156. package/src/lib/environment/data.ts +9 -0
  157. package/src/lib/environment/list-skills.ts +176 -0
  158. package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
  159. package/src/lib/environment/parsers/skill.ts +26 -5
  160. package/src/lib/environment/profile-generator.ts +56 -2
  161. package/src/lib/environment/skill-enrichment.ts +106 -0
  162. package/src/lib/environment/skill-recommendations.ts +66 -0
  163. package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
  164. package/src/lib/filters/__tests__/parse.test.ts +135 -0
  165. package/src/lib/filters/parse.ts +86 -0
  166. package/src/lib/instance/__tests__/detect.test.ts +1 -1
  167. package/src/lib/instance/__tests__/upgrade-poller.test.ts +50 -0
  168. package/src/lib/instance/fingerprint.ts +8 -10
  169. package/src/lib/instance/upgrade-poller.ts +53 -1
  170. package/src/lib/schedules/scheduler.ts +4 -4
  171. package/src/lib/utils/stagent-paths.ts +4 -0
  172. package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
  173. package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
  174. package/src/lib/workflows/blueprints/types.ts +6 -0
  175. package/src/lib/workflows/engine.ts +5 -3
  176. package/src/test/setup.ts +10 -0
@@ -0,0 +1,294 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ const { mockState } = vi.hoisted(() => ({
4
+ mockState: {
5
+ store: new Map<string, string | null>(),
6
+ lastWriteKey: null as string | null,
7
+ lastWriteValue: null as string | null,
8
+ setShouldThrow: false as boolean | string,
9
+ },
10
+ }));
11
+
12
+ vi.mock("@/lib/settings/helpers", () => ({
13
+ getSetting: vi.fn(async (key: string) => mockState.store.get(key) ?? null),
14
+ setSetting: vi.fn(async (key: string, value: string) => {
15
+ if (mockState.setShouldThrow) {
16
+ throw new Error(
17
+ typeof mockState.setShouldThrow === "string"
18
+ ? mockState.setShouldThrow
19
+ : "mock DB write failure"
20
+ );
21
+ }
22
+ mockState.lastWriteKey = key;
23
+ mockState.lastWriteValue = value;
24
+ mockState.store.set(key, value);
25
+ }),
26
+ }));
27
+
28
+ vi.mock("@/lib/environment/workspace-context", () => ({
29
+ getWorkspaceContext: () => ({
30
+ cwd: "/tmp/mock-cwd",
31
+ gitBranch: "main",
32
+ isWorktree: false,
33
+ folderName: "mock-project",
34
+ }),
35
+ }));
36
+
37
+ import { settingsTools } from "../settings-tools";
38
+
39
+ function getTool(name: string) {
40
+ const tools = settingsTools({ projectId: "proj-1" } as never);
41
+ const tool = tools.find((t) => t.name === name);
42
+ if (!tool) throw new Error(`Tool not found: ${name}`);
43
+ return tool;
44
+ }
45
+
46
+ async function call(toolName: string, args: Record<string, unknown>) {
47
+ const tool = getTool(toolName);
48
+ return tool.handler(args);
49
+ }
50
+
51
+ /** Parse a tool's text content back into an object for assertions. */
52
+ function parse(result: { content: Array<{ type: string; text: string }>; isError?: boolean }) {
53
+ return {
54
+ data: JSON.parse(result.content[0].text) as Record<string, unknown>,
55
+ isError: result.isError ?? false,
56
+ };
57
+ }
58
+
59
+ beforeEach(() => {
60
+ mockState.store.clear();
61
+ mockState.lastWriteKey = null;
62
+ mockState.lastWriteValue = null;
63
+ mockState.setShouldThrow = false;
64
+ vi.clearAllMocks();
65
+ });
66
+
67
+ describe("set_settings", () => {
68
+ describe("positive path", () => {
69
+ it("writes a valid value and returns oldValue + newValue", async () => {
70
+ mockState.store.set("runtime.sdkTimeoutSeconds", "60");
71
+ const { data, isError } = parse(
72
+ await call("set_settings", {
73
+ key: "runtime.sdkTimeoutSeconds",
74
+ value: "120",
75
+ })
76
+ );
77
+ expect(isError).toBe(false);
78
+ expect(data).toEqual({
79
+ key: "runtime.sdkTimeoutSeconds",
80
+ oldValue: "60",
81
+ newValue: "120",
82
+ });
83
+ expect(mockState.lastWriteKey).toBe("runtime.sdkTimeoutSeconds");
84
+ expect(mockState.lastWriteValue).toBe("120");
85
+ });
86
+
87
+ it("returns '(unset)' as oldValue when the key had no prior value", async () => {
88
+ const { data, isError } = parse(
89
+ await call("set_settings", {
90
+ key: "ollama.defaultModel",
91
+ value: "llama3",
92
+ })
93
+ );
94
+ expect(isError).toBe(false);
95
+ expect(data.oldValue).toBe("(unset)");
96
+ expect(data.newValue).toBe("llama3");
97
+ });
98
+
99
+ it("surfaces DB write failures as tool errors", async () => {
100
+ mockState.setShouldThrow = "disk full";
101
+ const { data, isError } = parse(
102
+ await call("set_settings", {
103
+ key: "ollama.baseUrl",
104
+ value: "http://localhost:11434",
105
+ })
106
+ );
107
+ expect(isError).toBe(true);
108
+ expect(data.error).toBe("disk full");
109
+ });
110
+ });
111
+
112
+ describe("unknown-key rejection", () => {
113
+ it("rejects an unknown key with a clear error listing all valid keys", async () => {
114
+ const { data, isError } = parse(
115
+ await call("set_settings", {
116
+ key: "totally.made.up.key",
117
+ value: "whatever",
118
+ })
119
+ );
120
+ expect(isError).toBe(true);
121
+ expect(typeof data.error).toBe("string");
122
+ const msg = data.error as string;
123
+ // The error must name the bad key and list at least a few writable keys
124
+ expect(msg).toContain("totally.made.up.key");
125
+ expect(msg).toContain("runtime.sdkTimeoutSeconds");
126
+ expect(msg).toContain("routing.preference");
127
+ // A setSetting must NEVER be attempted for an unknown key
128
+ expect(mockState.lastWriteKey).toBeNull();
129
+ });
130
+ });
131
+
132
+ describe("secret / internal keys are NOT writable", () => {
133
+ // This is the security guardrail: these keys are intentionally excluded
134
+ // from WRITABLE_SETTINGS. If any ever get added, this test fails
135
+ // immediately — a noisy failure is exactly what we want on regressions.
136
+ const forbiddenKeys = [
137
+ "auth.apiKey",
138
+ "openai.authApiKey",
139
+ "auth.apiKeySource",
140
+ "openai.authApiKeySource",
141
+ "auth.method",
142
+ "permissions.allow",
143
+ "usage.budgetPolicy",
144
+ "usage.budgetWarningState",
145
+ "usage.pricingRegistry",
146
+ "browser.chromeDevtoolsConfig",
147
+ "browser.playwrightConfig",
148
+ ];
149
+ for (const key of forbiddenKeys) {
150
+ it(`rejects write to "${key}"`, async () => {
151
+ const { data, isError } = parse(
152
+ await call("set_settings", { key, value: "attacker-value" })
153
+ );
154
+ expect(isError).toBe(true);
155
+ expect((data.error as string).toLowerCase()).toContain("not writable");
156
+ expect(mockState.lastWriteKey).toBeNull();
157
+ });
158
+ }
159
+ });
160
+
161
+ describe("per-key validation", () => {
162
+ it("rejects runtime.sdkTimeoutSeconds below 10", async () => {
163
+ const { data, isError } = parse(
164
+ await call("set_settings", { key: "runtime.sdkTimeoutSeconds", value: "5" })
165
+ );
166
+ expect(isError).toBe(true);
167
+ expect(data.error as string).toContain("10");
168
+ expect(mockState.lastWriteKey).toBeNull();
169
+ });
170
+
171
+ it("rejects runtime.sdkTimeoutSeconds above 300", async () => {
172
+ const { data, isError } = parse(
173
+ await call("set_settings", { key: "runtime.sdkTimeoutSeconds", value: "301" })
174
+ );
175
+ expect(isError).toBe(true);
176
+ expect(data.error as string).toContain("300");
177
+ });
178
+
179
+ it("rejects non-numeric runtime.sdkTimeoutSeconds", async () => {
180
+ const { isError } = parse(
181
+ await call("set_settings", { key: "runtime.sdkTimeoutSeconds", value: "many" })
182
+ );
183
+ expect(isError).toBe(true);
184
+ });
185
+
186
+ it("rejects routing.preference not in enum", async () => {
187
+ const { isError } = parse(
188
+ await call("set_settings", { key: "routing.preference", value: "random" })
189
+ );
190
+ expect(isError).toBe(true);
191
+ });
192
+
193
+ it("accepts routing.preference enum values", async () => {
194
+ for (const v of ["cost", "latency", "quality", "manual"]) {
195
+ const { isError } = parse(
196
+ await call("set_settings", { key: "routing.preference", value: v })
197
+ );
198
+ expect(isError).toBe(false);
199
+ }
200
+ });
201
+
202
+ it("rejects browser.playwrightEnabled with non-bool string", async () => {
203
+ const { isError } = parse(
204
+ await call("set_settings", { key: "browser.playwrightEnabled", value: "yes" })
205
+ );
206
+ expect(isError).toBe(true);
207
+ });
208
+
209
+ it("accepts browser.playwrightEnabled 'true' and 'false'", async () => {
210
+ for (const v of ["true", "false"]) {
211
+ const { isError } = parse(
212
+ await call("set_settings", { key: "browser.playwrightEnabled", value: v })
213
+ );
214
+ expect(isError).toBe(false);
215
+ }
216
+ });
217
+
218
+ it("rejects learning.contextCharLimit not aligned to step of 1000", async () => {
219
+ const { isError } = parse(
220
+ await call("set_settings", { key: "learning.contextCharLimit", value: "2500" })
221
+ );
222
+ expect(isError).toBe(true);
223
+ });
224
+
225
+ it("accepts learning.contextCharLimit on step boundary", async () => {
226
+ const { isError } = parse(
227
+ await call("set_settings", { key: "learning.contextCharLimit", value: "4000" })
228
+ );
229
+ expect(isError).toBe(false);
230
+ });
231
+
232
+ it("rejects empty ollama.baseUrl", async () => {
233
+ const { isError } = parse(
234
+ await call("set_settings", { key: "ollama.baseUrl", value: " " })
235
+ );
236
+ expect(isError).toBe(true);
237
+ });
238
+
239
+ it("rejects budget_max_cost_per_task below 0.5", async () => {
240
+ const { isError } = parse(
241
+ await call("set_settings", { key: "budget_max_cost_per_task", value: "0.1" })
242
+ );
243
+ expect(isError).toBe(true);
244
+ });
245
+
246
+ it("rejects budget_max_cost_per_task above 50", async () => {
247
+ const { isError } = parse(
248
+ await call("set_settings", { key: "budget_max_cost_per_task", value: "51" })
249
+ );
250
+ expect(isError).toBe(true);
251
+ });
252
+
253
+ it("accepts budget_max_cost_per_task fractional value in range", async () => {
254
+ const { isError } = parse(
255
+ await call("set_settings", { key: "budget_max_cost_per_task", value: "12.5" })
256
+ );
257
+ expect(isError).toBe(false);
258
+ });
259
+ });
260
+ });
261
+
262
+ describe("get_settings", () => {
263
+ it("tags writable keys with writable: true and read-only keys with false", async () => {
264
+ mockState.store.set("runtime.sdkTimeoutSeconds", "60");
265
+ mockState.store.set("auth_method", "oauth");
266
+ const { data, isError } = parse(await call("get_settings", {}));
267
+ expect(isError).toBe(false);
268
+ const entries = data as Record<string, { value: string | null; writable: boolean }>;
269
+ expect(entries["runtime.sdkTimeoutSeconds"].writable).toBe(true);
270
+ expect(entries["auth_method"].writable).toBe(false);
271
+ // Workspace context keys are always read-only
272
+ expect(entries["workspace_cwd"].writable).toBe(false);
273
+ expect(entries["workspace_cwd"].value).toBe("/tmp/mock-cwd");
274
+ });
275
+
276
+ it("single-key lookup returns writable flag", async () => {
277
+ mockState.store.set("runtime.maxTurns", "25");
278
+ const { data } = parse(
279
+ await call("get_settings", { key: "runtime.maxTurns" })
280
+ );
281
+ expect(data).toEqual({
282
+ key: "runtime.maxTurns",
283
+ value: "25",
284
+ writable: true,
285
+ });
286
+ });
287
+
288
+ it("workspace_* keys route through workspace context", async () => {
289
+ const { data } = parse(
290
+ await call("get_settings", { key: "workspace_git_branch" })
291
+ );
292
+ expect(data).toEqual({ key: "workspace_git_branch", value: "main" });
293
+ });
294
+ });