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.
- package/README.md +44 -31
- package/dist/cli.js +24 -0
- package/docs/.coverage-gaps.json +154 -24
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +12 -2
- package/docs/features/chat.md +40 -5
- package/docs/features/cost-usage.md +1 -1
- package/docs/features/documents.md +5 -2
- package/docs/features/inbox-notifications.md +10 -2
- package/docs/features/keyboard-navigation.md +12 -3
- package/docs/features/provider-runtimes.md +16 -2
- package/docs/features/settings.md +2 -2
- package/docs/features/shared-components.md +7 -3
- package/docs/features/tables.md +3 -1
- package/docs/features/tool-permissions.md +6 -2
- package/docs/features/workflows.md +6 -2
- package/docs/getting-started.md +1 -1
- package/docs/index.md +1 -1
- package/docs/journeys/developer.md +25 -2
- package/docs/journeys/personal-use.md +12 -5
- package/docs/journeys/power-user.md +45 -14
- package/docs/journeys/work-use.md +17 -8
- package/docs/manifest.json +15 -15
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +2 -2
- package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
- package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
- package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
- package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
- package/next.config.mjs +1 -0
- package/package.json +3 -3
- package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
- package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
- package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
- package/src/app/api/chat/export/route.ts +52 -0
- package/src/app/api/chat/files/search/route.ts +50 -0
- package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
- package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
- package/src/app/api/environment/skills/route.ts +13 -0
- package/src/app/api/schedules/[id]/execute/route.ts +2 -2
- package/src/app/api/settings/chat/pins/route.ts +94 -0
- package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
- package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
- package/src/app/api/settings/environment/route.ts +26 -0
- package/src/app/api/tasks/[id]/execute/route.ts +52 -12
- package/src/app/api/tasks/[id]/respond/route.ts +31 -15
- package/src/app/api/tasks/[id]/resume/route.ts +24 -3
- package/src/app/documents/page.tsx +4 -1
- package/src/app/settings/page.tsx +2 -0
- package/src/components/book/content-blocks.tsx +1 -1
- package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +166 -1
- package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
- package/src/components/chat/capability-banner.tsx +68 -0
- package/src/components/chat/chat-command-popover.tsx +668 -47
- package/src/components/chat/chat-input.tsx +103 -8
- package/src/components/chat/chat-message.tsx +12 -3
- package/src/components/chat/chat-session-provider.tsx +73 -3
- package/src/components/chat/chat-shell.tsx +62 -3
- package/src/components/chat/command-tab-bar.tsx +68 -0
- package/src/components/chat/conversation-template-picker.tsx +421 -0
- package/src/components/chat/help-dialog.tsx +39 -0
- package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
- package/src/components/chat/skill-row.tsx +147 -0
- package/src/components/documents/document-browser.tsx +37 -19
- package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
- package/src/components/notifications/permission-response-actions.tsx +155 -1
- package/src/components/playbook/playbook-detail-view.tsx +1 -1
- package/src/components/settings/environment-section.tsx +102 -0
- package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
- package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
- package/src/components/shared/command-palette.tsx +262 -2
- package/src/components/shared/filter-hint.tsx +70 -0
- package/src/components/shared/filter-input.tsx +59 -0
- package/src/components/shared/saved-searches-manager.tsx +199 -0
- package/src/components/tasks/task-bento-grid.tsx +12 -2
- package/src/components/tasks/task-card.tsx +3 -0
- package/src/components/tasks/task-chip-bar.tsx +30 -1
- package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
- package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
- package/src/hooks/use-active-skills.ts +110 -0
- package/src/hooks/use-chat-autocomplete.ts +120 -7
- package/src/hooks/use-enriched-skills.ts +19 -0
- package/src/hooks/use-pinned-entries.ts +104 -0
- package/src/hooks/use-recent-user-messages.ts +19 -0
- package/src/hooks/use-saved-searches.ts +142 -0
- package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +17 -4
- package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
- package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
- package/src/lib/agents/claude-agent.ts +105 -46
- package/src/lib/agents/handoff/bus.ts +2 -2
- package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +47 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +30 -3
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +6 -2
- package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
- package/src/lib/agents/profiles/registry.ts +97 -22
- package/src/lib/agents/profiles/types.ts +7 -1
- package/src/lib/agents/router.ts +3 -6
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
- package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
- package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
- package/src/lib/agents/runtime/catalog.ts +121 -0
- package/src/lib/agents/runtime/claude-sdk.ts +32 -0
- package/src/lib/agents/runtime/execution-target.ts +456 -0
- package/src/lib/agents/runtime/index.ts +4 -0
- package/src/lib/agents/runtime/launch-failure.ts +101 -0
- package/src/lib/agents/runtime/openai-codex.ts +35 -0
- package/src/lib/agents/runtime/openai-direct.ts +8 -0
- package/src/lib/agents/task-dispatch.ts +220 -0
- package/src/lib/agents/tool-permissions.ts +16 -1
- package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
- package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
- package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
- package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
- package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
- package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
- package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
- package/src/lib/chat/__tests__/types.test.ts +28 -0
- package/src/lib/chat/active-skills.ts +31 -0
- package/src/lib/chat/clean-filter-input.ts +30 -0
- package/src/lib/chat/codex-engine.ts +30 -7
- package/src/lib/chat/command-tabs.ts +61 -0
- package/src/lib/chat/context-builder.ts +141 -1
- package/src/lib/chat/dismissals.ts +73 -0
- package/src/lib/chat/engine.ts +109 -15
- package/src/lib/chat/files/__tests__/search.test.ts +135 -0
- package/src/lib/chat/files/expand-mention.ts +76 -0
- package/src/lib/chat/files/search.ts +99 -0
- package/src/lib/chat/skill-composition.ts +210 -0
- package/src/lib/chat/skill-conflict.ts +105 -0
- package/src/lib/chat/stagent-tools.ts +6 -19
- package/src/lib/chat/stream-telemetry.ts +9 -4
- package/src/lib/chat/system-prompt.ts +22 -0
- package/src/lib/chat/tool-catalog.ts +33 -3
- package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
- package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
- package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +47 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +134 -0
- package/src/lib/chat/tools/blueprint-tools.ts +190 -0
- package/src/lib/chat/tools/helpers.ts +2 -0
- package/src/lib/chat/tools/profile-tools.ts +120 -23
- package/src/lib/chat/tools/skill-tools.ts +183 -0
- package/src/lib/chat/tools/task-tools.ts +6 -2
- package/src/lib/chat/tools/workflow-tools.ts +61 -20
- package/src/lib/chat/types.ts +15 -0
- package/src/lib/constants/settings.ts +2 -0
- package/src/lib/data/clear.ts +2 -6
- package/src/lib/db/bootstrap.ts +17 -0
- package/src/lib/db/schema.ts +26 -0
- package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
- package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
- package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
- package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
- package/src/lib/environment/data.ts +9 -0
- package/src/lib/environment/list-skills.ts +176 -0
- package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
- package/src/lib/environment/parsers/skill.ts +26 -5
- package/src/lib/environment/profile-generator.ts +56 -2
- package/src/lib/environment/skill-enrichment.ts +106 -0
- package/src/lib/environment/skill-recommendations.ts +66 -0
- package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
- package/src/lib/filters/__tests__/parse.test.ts +135 -0
- package/src/lib/filters/parse.ts +86 -0
- package/src/lib/instance/__tests__/detect.test.ts +1 -1
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +50 -0
- package/src/lib/instance/fingerprint.ts +8 -10
- package/src/lib/instance/upgrade-poller.ts +53 -1
- package/src/lib/schedules/scheduler.ts +4 -4
- package/src/lib/utils/stagent-paths.ts +4 -0
- package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
- package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
- package/src/lib/workflows/blueprints/types.ts +6 -0
- package/src/lib/workflows/engine.ts +5 -3
- package/src/test/setup.ts +10 -0
|
@@ -11,6 +11,7 @@ import { invalidateLatestScan, getLatestScan } from "@/lib/environment/data";
|
|
|
11
11
|
import { db } from "@/lib/db";
|
|
12
12
|
import { environmentArtifacts } from "@/lib/db/schema";
|
|
13
13
|
import { eq, and } from "drizzle-orm";
|
|
14
|
+
import { getStagentProfilesDir } from "@/lib/utils/stagent-paths";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Builtins ship inside the repo at src/lib/agents/profiles/builtins/.
|
|
@@ -36,6 +37,12 @@ const SKILLS_DIR = path.join(
|
|
|
36
37
|
"skills"
|
|
37
38
|
);
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Auto-promoted profiles (from environment discovery) are written here
|
|
42
|
+
* instead of SKILLS_DIR to avoid colliding with Claude Code's skill namespace.
|
|
43
|
+
*/
|
|
44
|
+
const PROMOTED_PROFILES_DIR = getStagentProfilesDir();
|
|
45
|
+
|
|
39
46
|
// ---------------------------------------------------------------------------
|
|
40
47
|
// Cache
|
|
41
48
|
// ---------------------------------------------------------------------------
|
|
@@ -43,37 +50,44 @@ const SKILLS_DIR = path.join(
|
|
|
43
50
|
let profileCache: Map<string, AgentProfile> | null = null;
|
|
44
51
|
let profileCacheSignature: string | null = null;
|
|
45
52
|
|
|
46
|
-
function
|
|
47
|
-
if (!fs.existsSync(
|
|
48
|
-
return "missing";
|
|
49
|
-
}
|
|
53
|
+
function getDirectorySignatureParts(baseDir: string): string[] {
|
|
54
|
+
if (!fs.existsSync(baseDir)) return [];
|
|
50
55
|
|
|
51
56
|
const entries = fs
|
|
52
|
-
.readdirSync(
|
|
57
|
+
.readdirSync(baseDir, { withFileTypes: true })
|
|
53
58
|
.filter((entry) => entry.isDirectory())
|
|
54
59
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
55
60
|
|
|
56
|
-
const
|
|
61
|
+
const parts: string[] = [];
|
|
57
62
|
|
|
58
63
|
for (const entry of entries) {
|
|
59
|
-
const dir = path.join(
|
|
64
|
+
const dir = path.join(baseDir, entry.name);
|
|
60
65
|
const yamlPath = path.join(dir, "profile.yaml");
|
|
61
66
|
const skillPath = path.join(dir, "SKILL.md");
|
|
62
67
|
|
|
63
|
-
|
|
68
|
+
parts.push(entry.name);
|
|
64
69
|
|
|
65
70
|
if (fs.existsSync(yamlPath)) {
|
|
66
71
|
const stats = fs.statSync(yamlPath);
|
|
67
|
-
|
|
72
|
+
parts.push(`yaml:${stats.mtimeMs}:${stats.size}`);
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
if (fs.existsSync(skillPath)) {
|
|
71
76
|
const stats = fs.statSync(skillPath);
|
|
72
|
-
|
|
77
|
+
parts.push(`skill:${stats.mtimeMs}:${stats.size}`);
|
|
73
78
|
}
|
|
74
79
|
}
|
|
75
80
|
|
|
76
|
-
return
|
|
81
|
+
return parts;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getSkillsDirectorySignature(): string {
|
|
85
|
+
const skillsParts = getDirectorySignatureParts(SKILLS_DIR);
|
|
86
|
+
const promotedParts = getDirectorySignatureParts(PROMOTED_PROFILES_DIR);
|
|
87
|
+
|
|
88
|
+
if (skillsParts.length === 0 && promotedParts.length === 0) return "missing";
|
|
89
|
+
|
|
90
|
+
return [...skillsParts, "||promoted||", ...promotedParts].join("|");
|
|
77
91
|
}
|
|
78
92
|
|
|
79
93
|
// ---------------------------------------------------------------------------
|
|
@@ -118,6 +132,22 @@ function ensureBuiltins(): void {
|
|
|
118
132
|
changed = true;
|
|
119
133
|
}
|
|
120
134
|
|
|
135
|
+
if (
|
|
136
|
+
source.preferredRuntime !== undefined &&
|
|
137
|
+
target.preferredRuntime !== source.preferredRuntime
|
|
138
|
+
) {
|
|
139
|
+
target.preferredRuntime = source.preferredRuntime;
|
|
140
|
+
changed = true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (
|
|
144
|
+
source.capabilityOverrides !== undefined &&
|
|
145
|
+
target.capabilityOverrides === undefined
|
|
146
|
+
) {
|
|
147
|
+
target.capabilityOverrides = source.capabilityOverrides;
|
|
148
|
+
changed = true;
|
|
149
|
+
}
|
|
150
|
+
|
|
121
151
|
if (changed) {
|
|
122
152
|
fs.writeFileSync(targetYaml, yaml.dump(target));
|
|
123
153
|
}
|
|
@@ -140,15 +170,16 @@ function ensureBuiltins(): void {
|
|
|
140
170
|
// scanProfiles — read .claude/skills/*/profile.yaml, validate, pair w/ SKILL.md
|
|
141
171
|
// ---------------------------------------------------------------------------
|
|
142
172
|
|
|
143
|
-
function
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
173
|
+
function scanProfilesFromDir(
|
|
174
|
+
baseDir: string,
|
|
175
|
+
profiles: Map<string, AgentProfile>
|
|
176
|
+
): void {
|
|
177
|
+
if (!fs.existsSync(baseDir)) return;
|
|
147
178
|
|
|
148
|
-
for (const entry of fs.readdirSync(
|
|
179
|
+
for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
|
|
149
180
|
if (!entry.isDirectory()) continue;
|
|
150
181
|
|
|
151
|
-
const dir = path.join(
|
|
182
|
+
const dir = path.join(baseDir, entry.name);
|
|
152
183
|
const yamlPath = path.join(dir, "profile.yaml");
|
|
153
184
|
const skillPath = path.join(dir, "SKILL.md");
|
|
154
185
|
|
|
@@ -206,7 +237,9 @@ function scanProfiles(): Map<string, AgentProfile> {
|
|
|
206
237
|
tests: config.tests,
|
|
207
238
|
importMeta: config.importMeta,
|
|
208
239
|
supportedRuntimes: getSupportedRuntimes(config),
|
|
240
|
+
preferredRuntime: config.preferredRuntime,
|
|
209
241
|
runtimeOverrides: config.runtimeOverrides,
|
|
242
|
+
capabilityOverrides: config.capabilityOverrides,
|
|
210
243
|
origin,
|
|
211
244
|
});
|
|
212
245
|
} catch (err) {
|
|
@@ -214,6 +247,12 @@ function scanProfiles(): Map<string, AgentProfile> {
|
|
|
214
247
|
}
|
|
215
248
|
}
|
|
216
249
|
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function scanProfiles(): Map<string, AgentProfile> {
|
|
253
|
+
const profiles = new Map<string, AgentProfile>();
|
|
254
|
+
scanProfilesFromDir(SKILLS_DIR, profiles);
|
|
255
|
+
scanProfilesFromDir(PROMOTED_PROFILES_DIR, profiles);
|
|
217
256
|
return profiles;
|
|
218
257
|
}
|
|
219
258
|
|
|
@@ -299,6 +338,28 @@ export function createProfile(config: ProfileConfig, skillMd: string): void {
|
|
|
299
338
|
invalidateLatestScan();
|
|
300
339
|
}
|
|
301
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Create an auto-promoted profile in ~/.stagent/profiles/ (not ~/.claude/skills/).
|
|
343
|
+
* This avoids colliding with Claude Code's skill discovery namespace.
|
|
344
|
+
*/
|
|
345
|
+
export function createPromotedProfile(config: ProfileConfig, skillMd: string): void {
|
|
346
|
+
const result = ProfileConfigSchema.safeParse(config);
|
|
347
|
+
if (!result.success) {
|
|
348
|
+
throw new Error(`Invalid profile: ${result.error.issues.map(i => i.message).join(", ")}`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const dir = path.join(PROMOTED_PROFILES_DIR, config.id);
|
|
352
|
+
if (fs.existsSync(path.join(dir, "profile.yaml"))) {
|
|
353
|
+
throw new Error(`Profile "${config.id}" already exists`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
357
|
+
fs.writeFileSync(path.join(dir, "profile.yaml"), yaml.dump(config));
|
|
358
|
+
fs.writeFileSync(path.join(dir, "SKILL.md"), skillMd);
|
|
359
|
+
reloadProfiles();
|
|
360
|
+
invalidateLatestScan();
|
|
361
|
+
}
|
|
362
|
+
|
|
302
363
|
/** Update an existing custom profile (rejects builtins) */
|
|
303
364
|
export function updateProfile(id: string, config: ProfileConfig, skillMd: string): void {
|
|
304
365
|
if (isBuiltin(id)) {
|
|
@@ -310,8 +371,15 @@ export function updateProfile(id: string, config: ProfileConfig, skillMd: string
|
|
|
310
371
|
throw new Error(`Invalid profile: ${result.error.issues.map(i => i.message).join(", ")}`);
|
|
311
372
|
}
|
|
312
373
|
|
|
313
|
-
const
|
|
314
|
-
|
|
374
|
+
const skillsDir = path.join(SKILLS_DIR, id);
|
|
375
|
+
const promotedDir = path.join(PROMOTED_PROFILES_DIR, id);
|
|
376
|
+
const dir = fs.existsSync(skillsDir)
|
|
377
|
+
? skillsDir
|
|
378
|
+
: fs.existsSync(promotedDir)
|
|
379
|
+
? promotedDir
|
|
380
|
+
: null;
|
|
381
|
+
|
|
382
|
+
if (!dir) {
|
|
315
383
|
throw new Error(`Profile "${id}" not found`);
|
|
316
384
|
}
|
|
317
385
|
|
|
@@ -321,14 +389,21 @@ export function updateProfile(id: string, config: ProfileConfig, skillMd: string
|
|
|
321
389
|
invalidateLatestScan();
|
|
322
390
|
}
|
|
323
391
|
|
|
324
|
-
/** Delete a custom profile (rejects builtins) */
|
|
392
|
+
/** Delete a custom profile (rejects builtins). Checks both user and promoted dirs. */
|
|
325
393
|
export function deleteProfile(id: string): void {
|
|
326
394
|
if (isBuiltin(id)) {
|
|
327
395
|
throw new Error("Cannot delete built-in profiles");
|
|
328
396
|
}
|
|
329
397
|
|
|
330
|
-
const
|
|
331
|
-
|
|
398
|
+
const skillsDir = path.join(SKILLS_DIR, id);
|
|
399
|
+
const promotedDir = path.join(PROMOTED_PROFILES_DIR, id);
|
|
400
|
+
const dir = fs.existsSync(skillsDir)
|
|
401
|
+
? skillsDir
|
|
402
|
+
: fs.existsSync(promotedDir)
|
|
403
|
+
? promotedDir
|
|
404
|
+
: null;
|
|
405
|
+
|
|
406
|
+
if (!dir) {
|
|
332
407
|
throw new Error(`Profile "${id}" not found`);
|
|
333
408
|
}
|
|
334
409
|
|
|
@@ -31,7 +31,13 @@ export interface ProfileRuntimeCapabilityOverride {
|
|
|
31
31
|
export type ProfileScope = "builtin" | "user" | "project";
|
|
32
32
|
|
|
33
33
|
/** How a profile entered the system — distinct from scope (where it lives). */
|
|
34
|
-
export type ProfileOrigin =
|
|
34
|
+
export type ProfileOrigin =
|
|
35
|
+
| "manual"
|
|
36
|
+
| "environment"
|
|
37
|
+
| "import"
|
|
38
|
+
| "ai-assist"
|
|
39
|
+
| "filesystem-project"
|
|
40
|
+
| "filesystem-user";
|
|
35
41
|
|
|
36
42
|
export interface AgentProfile {
|
|
37
43
|
id: string;
|
package/src/lib/agents/router.ts
CHANGED
|
@@ -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
|
|
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
|
|
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);
|