stagent 0.10.0 → 0.11.0

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 (170) hide show
  1. package/README.md +15 -2
  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/index.md +1 -1
  18. package/docs/journeys/developer.md +25 -2
  19. package/docs/journeys/personal-use.md +12 -5
  20. package/docs/journeys/power-user.md +45 -14
  21. package/docs/journeys/work-use.md +17 -8
  22. package/docs/manifest.json +15 -15
  23. package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
  24. package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
  25. package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
  26. package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
  27. package/next.config.mjs +1 -0
  28. package/package.json +1 -1
  29. package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
  30. package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
  31. package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
  32. package/src/app/api/chat/export/route.ts +52 -0
  33. package/src/app/api/chat/files/search/route.ts +50 -0
  34. package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
  35. package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
  36. package/src/app/api/environment/skills/route.ts +13 -0
  37. package/src/app/api/schedules/[id]/execute/route.ts +2 -2
  38. package/src/app/api/settings/chat/pins/route.ts +94 -0
  39. package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
  40. package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
  41. package/src/app/api/settings/environment/route.ts +26 -0
  42. package/src/app/api/tasks/[id]/execute/route.ts +52 -12
  43. package/src/app/api/tasks/[id]/respond/route.ts +31 -15
  44. package/src/app/api/tasks/[id]/resume/route.ts +24 -3
  45. package/src/app/documents/page.tsx +4 -1
  46. package/src/app/settings/page.tsx +2 -0
  47. package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
  48. package/src/components/chat/__tests__/chat-session-provider.test.tsx +166 -1
  49. package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
  50. package/src/components/chat/capability-banner.tsx +68 -0
  51. package/src/components/chat/chat-command-popover.tsx +668 -47
  52. package/src/components/chat/chat-input.tsx +103 -8
  53. package/src/components/chat/chat-message.tsx +12 -3
  54. package/src/components/chat/chat-session-provider.tsx +73 -3
  55. package/src/components/chat/chat-shell.tsx +62 -3
  56. package/src/components/chat/command-tab-bar.tsx +68 -0
  57. package/src/components/chat/conversation-template-picker.tsx +421 -0
  58. package/src/components/chat/help-dialog.tsx +39 -0
  59. package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
  60. package/src/components/chat/skill-row.tsx +147 -0
  61. package/src/components/documents/document-browser.tsx +37 -19
  62. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
  63. package/src/components/notifications/permission-response-actions.tsx +155 -1
  64. package/src/components/settings/environment-section.tsx +102 -0
  65. package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
  66. package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
  67. package/src/components/shared/command-palette.tsx +262 -2
  68. package/src/components/shared/filter-hint.tsx +70 -0
  69. package/src/components/shared/filter-input.tsx +59 -0
  70. package/src/components/shared/saved-searches-manager.tsx +199 -0
  71. package/src/components/tasks/task-bento-grid.tsx +12 -2
  72. package/src/components/tasks/task-card.tsx +3 -0
  73. package/src/components/tasks/task-chip-bar.tsx +30 -1
  74. package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
  75. package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
  76. package/src/hooks/use-active-skills.ts +110 -0
  77. package/src/hooks/use-chat-autocomplete.ts +120 -7
  78. package/src/hooks/use-enriched-skills.ts +19 -0
  79. package/src/hooks/use-pinned-entries.ts +104 -0
  80. package/src/hooks/use-recent-user-messages.ts +19 -0
  81. package/src/hooks/use-saved-searches.ts +142 -0
  82. package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
  83. package/src/lib/agents/__tests__/claude-agent.test.ts +17 -4
  84. package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
  85. package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
  86. package/src/lib/agents/claude-agent.ts +105 -46
  87. package/src/lib/agents/handoff/bus.ts +2 -2
  88. package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
  89. package/src/lib/agents/profiles/__tests__/registry.test.ts +47 -0
  90. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +30 -3
  91. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +6 -2
  92. package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
  93. package/src/lib/agents/profiles/registry.ts +18 -0
  94. package/src/lib/agents/profiles/types.ts +7 -1
  95. package/src/lib/agents/router.ts +3 -6
  96. package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
  97. package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
  98. package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
  99. package/src/lib/agents/runtime/catalog.ts +121 -0
  100. package/src/lib/agents/runtime/claude-sdk.ts +32 -0
  101. package/src/lib/agents/runtime/execution-target.ts +456 -0
  102. package/src/lib/agents/runtime/index.ts +4 -0
  103. package/src/lib/agents/runtime/launch-failure.ts +101 -0
  104. package/src/lib/agents/runtime/openai-codex.ts +35 -0
  105. package/src/lib/agents/runtime/openai-direct.ts +8 -0
  106. package/src/lib/agents/task-dispatch.ts +220 -0
  107. package/src/lib/agents/tool-permissions.ts +16 -1
  108. package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
  109. package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
  110. package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
  111. package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
  112. package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
  113. package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
  114. package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
  115. package/src/lib/chat/__tests__/types.test.ts +28 -0
  116. package/src/lib/chat/active-skills.ts +31 -0
  117. package/src/lib/chat/clean-filter-input.ts +30 -0
  118. package/src/lib/chat/codex-engine.ts +30 -7
  119. package/src/lib/chat/command-tabs.ts +61 -0
  120. package/src/lib/chat/context-builder.ts +141 -1
  121. package/src/lib/chat/dismissals.ts +73 -0
  122. package/src/lib/chat/engine.ts +109 -15
  123. package/src/lib/chat/files/__tests__/search.test.ts +135 -0
  124. package/src/lib/chat/files/expand-mention.ts +76 -0
  125. package/src/lib/chat/files/search.ts +99 -0
  126. package/src/lib/chat/skill-composition.ts +210 -0
  127. package/src/lib/chat/skill-conflict.ts +105 -0
  128. package/src/lib/chat/stagent-tools.ts +6 -19
  129. package/src/lib/chat/stream-telemetry.ts +9 -4
  130. package/src/lib/chat/system-prompt.ts +22 -0
  131. package/src/lib/chat/tool-catalog.ts +33 -3
  132. package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
  133. package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
  134. package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
  135. package/src/lib/chat/tools/__tests__/task-tools.test.ts +47 -0
  136. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +134 -0
  137. package/src/lib/chat/tools/blueprint-tools.ts +190 -0
  138. package/src/lib/chat/tools/helpers.ts +2 -0
  139. package/src/lib/chat/tools/profile-tools.ts +120 -23
  140. package/src/lib/chat/tools/skill-tools.ts +183 -0
  141. package/src/lib/chat/tools/task-tools.ts +6 -2
  142. package/src/lib/chat/tools/workflow-tools.ts +61 -20
  143. package/src/lib/chat/types.ts +15 -0
  144. package/src/lib/constants/settings.ts +2 -0
  145. package/src/lib/data/clear.ts +2 -6
  146. package/src/lib/db/bootstrap.ts +17 -0
  147. package/src/lib/db/schema.ts +26 -0
  148. package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
  149. package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
  150. package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
  151. package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
  152. package/src/lib/environment/data.ts +9 -0
  153. package/src/lib/environment/list-skills.ts +176 -0
  154. package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
  155. package/src/lib/environment/parsers/skill.ts +26 -5
  156. package/src/lib/environment/profile-generator.ts +54 -0
  157. package/src/lib/environment/skill-enrichment.ts +106 -0
  158. package/src/lib/environment/skill-recommendations.ts +66 -0
  159. package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
  160. package/src/lib/filters/__tests__/parse.test.ts +135 -0
  161. package/src/lib/filters/parse.ts +86 -0
  162. package/src/lib/instance/__tests__/upgrade-poller.test.ts +50 -0
  163. package/src/lib/instance/fingerprint.ts +7 -9
  164. package/src/lib/instance/upgrade-poller.ts +53 -1
  165. package/src/lib/schedules/scheduler.ts +4 -4
  166. package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
  167. package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
  168. package/src/lib/workflows/blueprints/types.ts +6 -0
  169. package/src/lib/workflows/engine.ts +5 -3
  170. package/src/test/setup.ts +10 -0
@@ -1,15 +1,12 @@
1
1
  import { listProfiles, getProfile } from "./profiles/registry";
2
2
  import { profileSupportsRuntime } from "./profiles/compatibility";
3
- import {
4
- executeTaskWithRuntime,
5
- resumeTaskWithRuntime,
6
- } from "./runtime";
7
3
  import {
8
4
  DEFAULT_AGENT_RUNTIME,
9
5
  SUPPORTED_AGENT_RUNTIMES,
10
6
  type AgentRuntimeId,
11
7
  } from "./runtime/catalog";
12
8
  import type { RoutingPreference } from "@/lib/constants/settings";
9
+ import { resumeTaskExecution, startTaskExecution } from "./task-dispatch";
13
10
 
14
11
  // ── Keyword signal maps for runtime scoring ──────────────────────────
15
12
 
@@ -217,12 +214,12 @@ export async function executeTaskWithAgent(
217
214
  taskId: string,
218
215
  agentType: string | null | undefined = DEFAULT_AGENT_RUNTIME
219
216
  ): Promise<void> {
220
- return executeTaskWithRuntime(taskId, agentType);
217
+ return startTaskExecution(taskId, { requestedRuntimeId: agentType });
221
218
  }
222
219
 
223
220
  export async function resumeTaskWithAgent(
224
221
  taskId: string,
225
222
  agentType: string | null | undefined = DEFAULT_AGENT_RUNTIME
226
223
  ): Promise<void> {
227
- return resumeTaskWithRuntime(taskId, agentType);
224
+ return resumeTaskExecution(taskId, { requestedRuntimeId: agentType });
228
225
  }
@@ -3,6 +3,7 @@ import {
3
3
  DEFAULT_AGENT_RUNTIME,
4
4
  getRuntimeCapabilities,
5
5
  getRuntimeCatalogEntry,
6
+ getRuntimeFeatures,
6
7
  listRuntimeCatalog,
7
8
  resolveAgentRuntime,
8
9
  } from "@/lib/agents/runtime/catalog";
@@ -46,4 +47,133 @@ describe("runtime catalog", () => {
46
47
  expect(result).toBe("claude-code");
47
48
  warnSpy.mockRestore();
48
49
  });
50
+
51
+ it("exposes LLM-surface features via getRuntimeFeatures", () => {
52
+ const features = getRuntimeFeatures("claude-code");
53
+ expect(features.hasNativeSkills).toBe(true);
54
+ expect(features.hasProgressiveDisclosure).toBe(true);
55
+ expect(features.autoLoadsInstructions).toBe("CLAUDE.md");
56
+ expect(features.stagentInjectsSkills).toBe(false);
57
+ });
58
+
59
+ it("marks Ollama as requiring Stagent-injected skills", () => {
60
+ const features = getRuntimeFeatures("ollama");
61
+ expect(features.hasNativeSkills).toBe(false);
62
+ expect(features.stagentInjectsSkills).toBe(true);
63
+ expect(features.autoLoadsInstructions).toBeNull();
64
+ });
65
+
66
+ it("declares Codex auto-loads AGENTS.md", () => {
67
+ expect(getRuntimeFeatures("openai-codex-app-server").autoLoadsInstructions).toBe("AGENTS.md");
68
+ });
69
+
70
+ it("every runtime declares every feature key (exhaustiveness guard)", () => {
71
+ const runtimes = listRuntimeCatalog();
72
+ const expectedKeys: Array<keyof ReturnType<typeof getRuntimeFeatures>> = [
73
+ "hasNativeSkills",
74
+ "hasProgressiveDisclosure",
75
+ "hasFilesystemTools",
76
+ "hasBash",
77
+ "hasTodoWrite",
78
+ "hasSubagentDelegation",
79
+ "hasHooks",
80
+ "autoLoadsInstructions",
81
+ "stagentInjectsSkills",
82
+ "supportsSkillComposition",
83
+ "maxActiveSkills",
84
+ ];
85
+
86
+ // Guard against the "list grows stale" failure mode: if a new key is added
87
+ // to RuntimeFeatures but not to expectedKeys above, this catches it.
88
+ expect(expectedKeys.length).toBe(Object.keys(getRuntimeFeatures()).length);
89
+
90
+ for (const runtime of runtimes) {
91
+ for (const key of expectedKeys) {
92
+ expect(
93
+ runtime.features,
94
+ `${runtime.id} missing feature "${key}"`
95
+ ).toHaveProperty(key);
96
+ }
97
+ }
98
+ });
99
+
100
+ it("feature matrix snapshot matches declared values", () => {
101
+ // Guard against silent regressions: the declared feature matrix must match
102
+ // this snapshot exactly. Update intentionally when flipping a capability flag
103
+ // (and reference the spec change in the commit message).
104
+ const snapshot = listRuntimeCatalog().reduce<Record<string, unknown>>((acc, r) => {
105
+ acc[r.id] = r.features;
106
+ return acc;
107
+ }, {});
108
+
109
+ expect(snapshot).toMatchInlineSnapshot(`
110
+ {
111
+ "anthropic-direct": {
112
+ "autoLoadsInstructions": null,
113
+ "hasBash": false,
114
+ "hasFilesystemTools": false,
115
+ "hasHooks": false,
116
+ "hasNativeSkills": false,
117
+ "hasProgressiveDisclosure": false,
118
+ "hasSubagentDelegation": false,
119
+ "hasTodoWrite": false,
120
+ "maxActiveSkills": 3,
121
+ "stagentInjectsSkills": false,
122
+ "supportsSkillComposition": true,
123
+ },
124
+ "claude-code": {
125
+ "autoLoadsInstructions": "CLAUDE.md",
126
+ "hasBash": true,
127
+ "hasFilesystemTools": true,
128
+ "hasHooks": false,
129
+ "hasNativeSkills": true,
130
+ "hasProgressiveDisclosure": true,
131
+ "hasSubagentDelegation": false,
132
+ "hasTodoWrite": true,
133
+ "maxActiveSkills": 3,
134
+ "stagentInjectsSkills": false,
135
+ "supportsSkillComposition": true,
136
+ },
137
+ "ollama": {
138
+ "autoLoadsInstructions": null,
139
+ "hasBash": false,
140
+ "hasFilesystemTools": false,
141
+ "hasHooks": false,
142
+ "hasNativeSkills": false,
143
+ "hasProgressiveDisclosure": false,
144
+ "hasSubagentDelegation": false,
145
+ "hasTodoWrite": false,
146
+ "maxActiveSkills": 1,
147
+ "stagentInjectsSkills": true,
148
+ "supportsSkillComposition": false,
149
+ },
150
+ "openai-codex-app-server": {
151
+ "autoLoadsInstructions": "AGENTS.md",
152
+ "hasBash": true,
153
+ "hasFilesystemTools": true,
154
+ "hasHooks": false,
155
+ "hasNativeSkills": true,
156
+ "hasProgressiveDisclosure": true,
157
+ "hasSubagentDelegation": false,
158
+ "hasTodoWrite": true,
159
+ "maxActiveSkills": 3,
160
+ "stagentInjectsSkills": false,
161
+ "supportsSkillComposition": true,
162
+ },
163
+ "openai-direct": {
164
+ "autoLoadsInstructions": null,
165
+ "hasBash": false,
166
+ "hasFilesystemTools": false,
167
+ "hasHooks": false,
168
+ "hasNativeSkills": false,
169
+ "hasProgressiveDisclosure": false,
170
+ "hasSubagentDelegation": false,
171
+ "hasTodoWrite": false,
172
+ "maxActiveSkills": 3,
173
+ "stagentInjectsSkills": false,
174
+ "supportsSkillComposition": true,
175
+ },
176
+ }
177
+ `);
178
+ });
49
179
  });
@@ -0,0 +1,183 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { AgentRuntimeId } from "@/lib/agents/runtime/catalog";
3
+
4
+ const {
5
+ mockGetRuntimeSetupStates,
6
+ mockListConfiguredRuntimeIds,
7
+ mockGetRoutingPreference,
8
+ mockTestRuntimeConnection,
9
+ mockGetProfile,
10
+ mockProfileSupportsRuntime,
11
+ mockSuggestRuntime,
12
+ } = vi.hoisted(() => ({
13
+ mockGetRuntimeSetupStates: vi.fn(),
14
+ mockListConfiguredRuntimeIds: vi.fn(),
15
+ mockGetRoutingPreference: vi.fn(),
16
+ mockTestRuntimeConnection: vi.fn(),
17
+ mockGetProfile: vi.fn(),
18
+ mockProfileSupportsRuntime: vi.fn(),
19
+ mockSuggestRuntime: vi.fn(),
20
+ }));
21
+
22
+ vi.mock("@/lib/settings/runtime-setup", () => ({
23
+ getRuntimeSetupStates: mockGetRuntimeSetupStates,
24
+ listConfiguredRuntimeIds: mockListConfiguredRuntimeIds,
25
+ }));
26
+
27
+ vi.mock("@/lib/settings/routing", () => ({
28
+ getRoutingPreference: mockGetRoutingPreference,
29
+ }));
30
+
31
+ vi.mock("@/lib/agents/runtime/index", () => ({
32
+ testRuntimeConnection: mockTestRuntimeConnection,
33
+ }));
34
+
35
+ vi.mock("@/lib/agents/profiles/registry", () => ({
36
+ getProfile: mockGetProfile,
37
+ }));
38
+
39
+ vi.mock("@/lib/agents/profiles/compatibility", () => ({
40
+ profileSupportsRuntime: mockProfileSupportsRuntime,
41
+ }));
42
+
43
+ vi.mock("@/lib/agents/router", () => ({
44
+ suggestRuntime: mockSuggestRuntime,
45
+ }));
46
+
47
+ import {
48
+ RequestedModelUnavailableError,
49
+ resolveChatExecutionTarget,
50
+ resolveResumeExecutionTarget,
51
+ resolveTaskExecutionTarget,
52
+ } from "../execution-target";
53
+
54
+ function makeStates(configured: AgentRuntimeId[]) {
55
+ const all: AgentRuntimeId[] = [
56
+ "claude-code",
57
+ "openai-codex-app-server",
58
+ "anthropic-direct",
59
+ "openai-direct",
60
+ "ollama",
61
+ ];
62
+
63
+ return Object.fromEntries(
64
+ all.map((runtimeId) => [
65
+ runtimeId,
66
+ {
67
+ runtimeId,
68
+ configured: configured.includes(runtimeId),
69
+ },
70
+ ])
71
+ );
72
+ }
73
+
74
+ describe("execution target resolver", () => {
75
+ beforeEach(() => {
76
+ vi.clearAllMocks();
77
+ mockGetRuntimeSetupStates.mockResolvedValue(
78
+ makeStates(["claude-code", "openai-codex-app-server"])
79
+ );
80
+ mockListConfiguredRuntimeIds.mockReturnValue([
81
+ "claude-code",
82
+ "openai-codex-app-server",
83
+ ]);
84
+ mockGetRoutingPreference.mockResolvedValue("latency");
85
+ mockProfileSupportsRuntime.mockReturnValue(true);
86
+ mockSuggestRuntime.mockImplementation(
87
+ (
88
+ _title: string,
89
+ _description: string | undefined,
90
+ _profileId: string | undefined,
91
+ availableRuntimeIds: AgentRuntimeId[]
92
+ ) => ({
93
+ runtimeId: availableRuntimeIds[0],
94
+ reason: "test",
95
+ })
96
+ );
97
+ mockTestRuntimeConnection.mockImplementation((runtimeId: AgentRuntimeId) => {
98
+ if (runtimeId === "claude-code") {
99
+ return Promise.resolve({
100
+ connected: false,
101
+ error: "Claude Code process exited with code 1",
102
+ });
103
+ }
104
+ return Promise.resolve({ connected: true });
105
+ });
106
+ });
107
+
108
+ it("falls back from an unavailable requested task runtime to a compatible alternate", async () => {
109
+ mockGetProfile.mockReturnValue({
110
+ id: "upgrade-assistant",
111
+ allowedTools: ["Bash(git status)", "Read", "Write"],
112
+ });
113
+
114
+ const target = await resolveTaskExecutionTarget({
115
+ title: "Upgrade local branch",
116
+ description: "Merge upstream main safely",
117
+ requestedRuntimeId: "claude-code",
118
+ profileId: "upgrade-assistant",
119
+ });
120
+
121
+ expect(target.requestedRuntimeId).toBe("claude-code");
122
+ expect(target.effectiveRuntimeId).toBe("openai-codex-app-server");
123
+ expect(target.fallbackApplied).toBe(true);
124
+ expect(target.fallbackReason).toContain("Fell back to OpenAI Codex App Server");
125
+ });
126
+
127
+ it("auto-selects a healthy runtime when no task runtime was requested", async () => {
128
+ mockGetProfile.mockReturnValue({
129
+ id: "general",
130
+ allowedTools: [],
131
+ preferredRuntime: "anthropic-direct",
132
+ });
133
+ mockSuggestRuntime.mockReturnValue({
134
+ runtimeId: "openai-codex-app-server",
135
+ reason: "test",
136
+ });
137
+
138
+ const target = await resolveTaskExecutionTarget({
139
+ title: "Fix failing build",
140
+ description: "Debug and patch the repo",
141
+ profileId: "general",
142
+ });
143
+
144
+ expect(target.requestedRuntimeId).toBeNull();
145
+ expect(target.effectiveRuntimeId).toBe("openai-codex-app-server");
146
+ expect(target.fallbackApplied).toBe(false);
147
+ });
148
+
149
+ it("falls back chat turns to the mapped alternate model when the requested runtime is unavailable", async () => {
150
+ const target = await resolveChatExecutionTarget({
151
+ requestedRuntimeId: "claude-code",
152
+ requestedModelId: "sonnet",
153
+ });
154
+
155
+ expect(target.requestedRuntimeId).toBe("claude-code");
156
+ expect(target.effectiveRuntimeId).toBe("openai-codex-app-server");
157
+ expect(target.effectiveModelId).toBe("gpt-5.3-codex");
158
+ expect(target.fallbackApplied).toBe(true);
159
+ });
160
+
161
+ it("refuses resume when the last effective runtime is unavailable", async () => {
162
+ await expect(
163
+ resolveResumeExecutionTarget({
164
+ requestedRuntimeId: "claude-code",
165
+ effectiveRuntimeId: "claude-code",
166
+ })
167
+ ).rejects.toThrow("Claude Code process exited with code 1");
168
+ });
169
+
170
+ it("throws a named error when no chat runtime is healthy", async () => {
171
+ mockTestRuntimeConnection.mockResolvedValue({
172
+ connected: false,
173
+ error: "all down",
174
+ });
175
+
176
+ await expect(
177
+ resolveChatExecutionTarget({
178
+ requestedRuntimeId: "claude-code",
179
+ requestedModelId: "sonnet",
180
+ })
181
+ ).rejects.toBeInstanceOf(RequestedModelUnavailableError);
182
+ });
183
+ });
@@ -481,6 +481,14 @@ async function executeAnthropicDirectTask(taskId: string, isResume = false): Pro
481
481
  startedAt: usageState.startedAt,
482
482
  finishedAt: new Date(),
483
483
  });
484
+
485
+ await db
486
+ .update(tasks)
487
+ .set({
488
+ effectiveModelId: result.totalUsage.modelId ?? modelId,
489
+ updatedAt: new Date(),
490
+ })
491
+ .where(eq(tasks.id, taskId));
484
492
  } catch (err) {
485
493
  if (!abortController.signal.aborted) {
486
494
  const errorMsg = err instanceof Error ? err.message : String(err);
@@ -21,6 +21,51 @@ export interface RuntimeCapabilities {
21
21
  authHealthCheck: boolean;
22
22
  }
23
23
 
24
+ /**
25
+ * LLM-surface features that affect what the model sees and which tools/skills
26
+ * Stagent exposes to it. Distinct from RuntimeCapabilities above, which is
27
+ * adapter-plumbing concerns (can the adapter resume/cancel/etc.).
28
+ *
29
+ * Values reflect post-Phase-1 capability (what the runtime SDK *can* do),
30
+ * not current engagement (what `engine.ts` currently activates). Downstream
31
+ * features read this bag to decide rendering, filtering, and dispatch.
32
+ */
33
+ export interface RuntimeFeatures {
34
+ /** SDK provides a native skill-invocation tool (e.g. Claude SDK `Skill` tool). */
35
+ hasNativeSkills: boolean;
36
+ /** SDK loads skill metadata first, full SKILL.md on demand. */
37
+ hasProgressiveDisclosure: boolean;
38
+ /** Read/Grep/Glob/Edit/Write available as LLM tools. */
39
+ hasFilesystemTools: boolean;
40
+ /** Bash tool available (Stagent gates via permission bridge). */
41
+ hasBash: boolean;
42
+ /** TodoWrite tool available. */
43
+ hasTodoWrite: boolean;
44
+ /** Runtime supports delegating to sub-agents (e.g. Task tool). */
45
+ hasSubagentDelegation: boolean;
46
+ /** Runtime loads filesystem hooks (pre/post tool-use shell scripts). */
47
+ hasHooks: boolean;
48
+ /** Which project-level instructions file the runtime auto-loads, if any. */
49
+ autoLoadsInstructions: "CLAUDE.md" | "AGENTS.md" | null;
50
+ /**
51
+ * Runtime has no native skill support — Stagent must inject SKILL.md content
52
+ * into the system prompt to expose skills to the LLM.
53
+ */
54
+ stagentInjectsSkills: boolean;
55
+ /**
56
+ * Runtime supports composing multiple active skills in one conversation.
57
+ * When false, only one skill may be active at a time (Ollama: context
58
+ * budget too tight). When true, `activate_skill mode:"add"` is allowed
59
+ * up to `maxActiveSkills`.
60
+ */
61
+ supportsSkillComposition: boolean;
62
+ /**
63
+ * Maximum number of skills that may be simultaneously active. Enforced
64
+ * by the activate_skill tool. Ignored when supportsSkillComposition=false.
65
+ */
66
+ maxActiveSkills: number;
67
+ }
68
+
24
69
  export interface RuntimeModelConfig {
25
70
  /** Default model ID for this runtime */
26
71
  default: string;
@@ -34,6 +79,7 @@ export interface RuntimeCatalogEntry {
34
79
  description: string;
35
80
  providerId: "anthropic" | "openai" | "ollama";
36
81
  capabilities: RuntimeCapabilities;
82
+ features: RuntimeFeatures;
37
83
  /** Model catalog — default and supported model IDs for this runtime */
38
84
  models: RuntimeModelConfig;
39
85
  }
@@ -54,6 +100,19 @@ const RUNTIME_CATALOG: Record<AgentRuntimeId, RuntimeCatalogEntry> = {
54
100
  profileAssist: true,
55
101
  authHealthCheck: true,
56
102
  },
103
+ features: {
104
+ hasNativeSkills: true,
105
+ hasProgressiveDisclosure: true,
106
+ hasFilesystemTools: true,
107
+ hasBash: true,
108
+ hasTodoWrite: true,
109
+ hasSubagentDelegation: false, // Stagent task primitives replace SDK Task tool
110
+ hasHooks: false, // excluded per Q2
111
+ autoLoadsInstructions: "CLAUDE.md",
112
+ stagentInjectsSkills: false,
113
+ supportsSkillComposition: true,
114
+ maxActiveSkills: 3,
115
+ },
57
116
  models: {
58
117
  default: "sonnet",
59
118
  supported: ["haiku", "sonnet", "opus"],
@@ -74,6 +133,19 @@ const RUNTIME_CATALOG: Record<AgentRuntimeId, RuntimeCatalogEntry> = {
74
133
  profileAssist: false,
75
134
  authHealthCheck: true,
76
135
  },
136
+ features: {
137
+ hasNativeSkills: true,
138
+ hasProgressiveDisclosure: true,
139
+ hasFilesystemTools: true,
140
+ hasBash: true,
141
+ hasTodoWrite: true,
142
+ hasSubagentDelegation: false,
143
+ hasHooks: false,
144
+ autoLoadsInstructions: "AGENTS.md",
145
+ stagentInjectsSkills: false,
146
+ supportsSkillComposition: true,
147
+ maxActiveSkills: 3,
148
+ },
77
149
  models: {
78
150
  default: "gpt-5.4",
79
151
  supported: ["gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex"],
@@ -94,6 +166,21 @@ const RUNTIME_CATALOG: Record<AgentRuntimeId, RuntimeCatalogEntry> = {
94
166
  profileAssist: true,
95
167
  authHealthCheck: true,
96
168
  },
169
+ features: {
170
+ // Direct Messages API — no SDK-native skill machinery.
171
+ // Revisit when chat-claude-sdk-skills designs direct-API skill injection.
172
+ hasNativeSkills: false,
173
+ hasProgressiveDisclosure: false,
174
+ hasFilesystemTools: false,
175
+ hasBash: false,
176
+ hasTodoWrite: false,
177
+ hasSubagentDelegation: false,
178
+ hasHooks: false,
179
+ autoLoadsInstructions: null,
180
+ stagentInjectsSkills: false,
181
+ supportsSkillComposition: true,
182
+ maxActiveSkills: 3,
183
+ },
97
184
  models: {
98
185
  default: "claude-sonnet-4-20250514",
99
186
  supported: ["claude-haiku-4-5-20251001", "claude-sonnet-4-20250514", "claude-opus-4-20250514"],
@@ -114,6 +201,21 @@ const RUNTIME_CATALOG: Record<AgentRuntimeId, RuntimeCatalogEntry> = {
114
201
  profileAssist: false,
115
202
  authHealthCheck: true,
116
203
  },
204
+ features: {
205
+ // Direct Responses API — no SDK-native skill machinery.
206
+ // Revisit when chat-claude-sdk-skills designs direct-API skill injection.
207
+ hasNativeSkills: false,
208
+ hasProgressiveDisclosure: false,
209
+ hasFilesystemTools: false,
210
+ hasBash: false,
211
+ hasTodoWrite: false,
212
+ hasSubagentDelegation: false,
213
+ hasHooks: false,
214
+ autoLoadsInstructions: null,
215
+ stagentInjectsSkills: false,
216
+ supportsSkillComposition: true,
217
+ maxActiveSkills: 3,
218
+ },
117
219
  models: {
118
220
  default: "gpt-4.1",
119
221
  supported: ["gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano"],
@@ -134,6 +236,19 @@ const RUNTIME_CATALOG: Record<AgentRuntimeId, RuntimeCatalogEntry> = {
134
236
  profileAssist: false,
135
237
  authHealthCheck: true,
136
238
  },
239
+ features: {
240
+ hasNativeSkills: false,
241
+ hasProgressiveDisclosure: false,
242
+ hasFilesystemTools: false,
243
+ hasBash: false,
244
+ hasTodoWrite: false, // Stagent MCP exposes todo tools separately
245
+ hasSubagentDelegation: false,
246
+ hasHooks: false,
247
+ autoLoadsInstructions: null,
248
+ stagentInjectsSkills: true,
249
+ supportsSkillComposition: false,
250
+ maxActiveSkills: 1,
251
+ },
137
252
  models: {
138
253
  default: "llama3",
139
254
  supported: [], // Dynamic — populated from Ollama API at runtime
@@ -157,6 +272,12 @@ export function getRuntimeCapabilities(
157
272
  return getRuntimeCatalogEntry(runtimeId).capabilities;
158
273
  }
159
274
 
275
+ export function getRuntimeFeatures(
276
+ runtimeId: AgentRuntimeId = DEFAULT_AGENT_RUNTIME
277
+ ): RuntimeFeatures {
278
+ return getRuntimeCatalogEntry(runtimeId).features;
279
+ }
280
+
160
281
  export function resolveAgentRuntime(runtimeId?: string | null): AgentRuntimeId {
161
282
  if (!runtimeId) return DEFAULT_AGENT_RUNTIME;
162
283
  if (isAgentRuntimeId(runtimeId)) return runtimeId;
@@ -1,3 +1,35 @@
1
+ // ─── Claude Agent SDK options shared by chat and task runtimes ──────
2
+ //
3
+ // Chat (src/lib/chat/engine.ts) and task (src/lib/agents/claude-agent.ts)
4
+ // both construct query() options for the `claude-code` runtime. These
5
+ // constants are the single source of truth so the two code paths cannot
6
+ // drift — a drift that would manifest as "skills work in chat but vanish
7
+ // in tasks on the same project." See features/task-runtime-skill-parity.md
8
+ // and features/chat-claude-sdk-skills.md.
9
+
10
+ export const CLAUDE_SDK_SETTING_SOURCES = ["user", "project"] as const;
11
+
12
+ export const CLAUDE_SDK_ALLOWED_TOOLS = [
13
+ "Skill",
14
+ "Read",
15
+ "Grep",
16
+ "Glob",
17
+ "Edit",
18
+ "Write",
19
+ "Bash",
20
+ "TodoWrite",
21
+ ] as const;
22
+
23
+ /**
24
+ * Filesystem tools safe to auto-allow without a permission prompt.
25
+ * Mirrors the existing browser/exa read-only auto-allow pattern.
26
+ */
27
+ export const CLAUDE_SDK_READ_ONLY_FS_TOOLS = new Set<string>([
28
+ "Read",
29
+ "Grep",
30
+ "Glob",
31
+ ]);
32
+
1
33
  /**
2
34
  * Build the environment for the Claude Agent SDK subprocess.
3
35
  *