diff --git a/docs/chatgpt-coding-workflow.md b/docs/chatgpt-coding-workflow.md index a90f3b859..7d53fa397 100644 --- a/docs/chatgpt-coding-workflow.md +++ b/docs/chatgpt-coding-workflow.md @@ -116,10 +116,14 @@ DevSpace discovers standard Agent Skills from: It also keeps compatibility with: -- the bundled `subagents` skill when Subagents are enabled, unless `~/.devspace/skills/subagents/SKILL.md` exists - `skills.agentDir/skills`, defaulting to `~/.codex/skills` - additional paths from `skills.paths` +When Subagents are enabled, DevSpace synchronizes its bundled workflow to the +managed path `~/.devspace/skills/subagents/SKILL.md`. That copy is refreshed +from the installed DevSpace package and wins over other skills named +`subagents`. + When Subagents are enabled, DevSpace discovers agent profiles from `~/.devspace/agents/*.md` and project `.devspace/agents/*.md`. `open_workspace` exposes a compact catalog with profile names, descriptions, @@ -141,7 +145,10 @@ Skill paths may be outside the workspace. DevSpace only permits reading: Set `skills.enabled` to `false` to hide skills from workspace output. Enable Subagents and choose providers through `devspace init` or the persisted provider -configuration. The bundled `subagents` skill teaches the minimal +configuration. `subagents.instructions` defaults to `on-demand`, which exposes +the managed `subagents` skill for a separate read only when the model decides +delegation would help. Set it to `preload` to include those instructions in the +initial `open_workspace` result instead. The skill teaches the minimal `devspace agents targets`, `devspace agents ls`, `devspace agents run`, `devspace agents continue`, and `devspace agents show` workflow. The catalog comes from `open_workspace`; `devspace agents ls` lists existing subagent diff --git a/docs/configuration.md b/docs/configuration.md index 6a6f607bd..d064465ea 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -54,6 +54,7 @@ Run `devspace init` to create both files. `devspace config set publicBaseUrl }, "subagents": { "enabled": false, + "instructions": "on-demand", "providers": [], }, "logging": { @@ -102,6 +103,11 @@ DevSpace discovers standard Agent Skills from `~/.agents/skills`, project `skills.agentDir/skills` and each path in `skills.paths`. Relative custom paths are resolved from the active workspace. +When Subagents are enabled for MCP workspaces, DevSpace keeps its bundled +`subagents` skill synchronized at `~/.devspace/skills/subagents/SKILL.md`. +That managed copy is the authoritative `subagents` skill for DevSpace and is +refreshed when the packaged skill changes. + Subagent providers are explicit. Omitted providers are disabled: ```jsonc @@ -109,6 +115,7 @@ Subagent providers are explicit. Omitted providers are disabled: "configVersion": 1, "subagents": { "enabled": true, + "instructions": "on-demand", "providers": [ { "id": "codex", @@ -126,6 +133,16 @@ Subagent providers are explicit. Omitted providers are disabled: } ``` +`subagents.instructions` controls when ChatGPT receives the managed workflow: + +| Value | Behavior | +| --- | --- | +| `on-demand` | Default. `open_workspace` advertises the `subagents` skill and the model reads it only when the task benefits from delegation. | +| `preload` | `open_workspace` includes the `subagents` workflow in its initial workspace instructions instead of advertising that skill for a separate read. | + +Both modes only make the workflow available; neither tells the model to prefer +subagents for routine work. + Profiles are loaded from `~/.devspace/agents/*.md` and project `.devspace/agents/*.md`. `devspace agents targets` prints the configured targets available in the current workspace. diff --git a/docs/gotchas.md b/docs/gotchas.md index 842cf301f..a43a59d68 100644 --- a/docs/gotchas.md +++ b/docs/gotchas.md @@ -222,14 +222,15 @@ DevSpace looks in standard Agent Skills locations: It also checks compatibility and custom paths: -- the bundled `subagents` skill when Subagents are enabled, unless `~/.devspace/skills/subagents/SKILL.md` exists - `skills.agentDir/skills`, defaulting to `~/.codex/skills` - additional paths from `skills.paths` When Subagents are enabled, DevSpace loads agent profiles from `~/.devspace/agents/*.md` and project `.devspace/agents/*.md`, then exposes a -compact profile catalog through `open_workspace`. The bundled -`subagents` skill keeps the model-facing workflow to +compact profile catalog through `open_workspace`. DevSpace also synchronizes +its managed `subagents` skill to `~/.devspace/skills/subagents/SKILL.md` and +uses that copy instead of a package-manager path. The skill keeps the +model-facing workflow to `devspace agents targets`, `devspace agents ls`, `devspace agents run`, `devspace agents continue`, and `devspace agents show`. Those commands automatically manage the internal local agent daemon; `devspace @@ -237,6 +238,11 @@ serve` is not a prerequisite. `devspace agents ls` lists existing subagent sessions, not profile definitions. +By default, `subagents.instructions` is `on-demand`, so `open_workspace` +advertises the skill and the model reads it only when useful. Set it to +`preload` to include the workflow directly in the initial workspace +instructions instead. + For a Coding Agent, run the installation command printed by `devspace init`: @@ -245,7 +251,9 @@ npx skills add Waishnav/devspace --skill subagents --global ``` The Skills CLI handles agent discovery and installation. DevSpace setup does -not copy files into agent skill directories. +not copy files into agent skill directories. The managed +`~/.devspace/skills/subagents` copy is for DevSpace MCP workspaces and is +separate from Coding Agent installation. Packaged agent profile examples under `examples/agents/` are starter templates. Copy or adapt them into one of the active profile directories before use. diff --git a/docs/setup.md b/docs/setup.md index 12ae05b0c..b7c044d3b 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -68,6 +68,11 @@ The Skills CLI asks which installed Coding Agents should receive the skill. The skill uses `devspace agents targets`, `run`, `continue`, `show`, and `ls`. These commands do not require `devspace serve`. +This Coding Agent installation is separate from ChatGPT MCP usage. For MCP +workspaces with Subagents enabled, DevSpace manages its own copy at +`~/.devspace/skills/subagents/SKILL.md`; users do not install that copy +manually. + ### Connect ChatGPT Setup only asks for a public URL if you selected ChatGPT. Start your tunnel or diff --git a/schema/v1/devspace.schema.json b/schema/v1/devspace.schema.json index e7c18466e..c4fa82d64 100644 --- a/schema/v1/devspace.schema.json +++ b/schema/v1/devspace.schema.json @@ -158,6 +158,7 @@ "subagents": { "default": { "enabled": false, + "instructions": "on-demand", "providers": [] }, "type": "object", @@ -165,6 +166,14 @@ "enabled": { "type": "boolean" }, + "instructions": { + "default": "on-demand", + "type": "string", + "enum": [ + "on-demand", + "preload" + ] + }, "providers": { "type": "array", "items": { diff --git a/skills/subagents/SKILL.md b/skills/subagents/SKILL.md index 2e6180b5c..95ca378ed 100644 --- a/skills/subagents/SKILL.md +++ b/skills/subagents/SKILL.md @@ -5,6 +5,8 @@ description: Delegate focused coding, research, review, or verification work to # DevSpace subagents +Subagents are optional. Use the normal workspace tools for routine work; delegate only when a separate worker materially helps through independent context, specialization, or follow-up. + Use the DevSpace CLI through the shell or process tool. Run commands from the project the subagent should work on. ## Choose a target diff --git a/src/cli.test.ts b/src/cli.test.ts index 0983417c3..26d41d0ae 100644 --- a/src/cli.test.ts +++ b/src/cli.test.ts @@ -42,7 +42,7 @@ try { const cliConfigEnv = writeTestDevspaceConfig(configDir, { workspaces: { allowedRoots: [projectRoot] }, storage: { stateDir }, - subagents: { enabled: true, providers: [] }, + subagents: { enabled: true, instructions: "on-demand", providers: [] }, }); writeFileSync( join(configDir, "agents", "reviewer.md"), diff --git a/src/config-schema.ts b/src/config-schema.ts index c30bb3612..78e610f56 100644 --- a/src/config-schema.ts +++ b/src/config-schema.ts @@ -71,7 +71,11 @@ export const devspaceConfigSchema = z.object({ ui: uiConfigSchema, artifacts: artifactsConfigSchema, skills: skillsConfigSchema, - subagents: subagentsConfigSchema.default({ enabled: false, providers: [] }), + subagents: subagentsConfigSchema.default({ + enabled: false, + instructions: "on-demand", + providers: [], + }), logging: loggingConfigSchema, oauth: oauthConfigSchema, }).strict(); diff --git a/src/config.test.ts b/src/config.test.ts index 5fd24b490..d21b39e88 100644 --- a/src/config.test.ts +++ b/src/config.test.ts @@ -22,7 +22,11 @@ try { assert.equal(defaults.uiEnabled, true); assert.equal(defaults.skillsEnabled, true); assert.equal(defaults.artifactsEnabled, false); - assert.deepEqual(defaults.subagents, { enabled: false, providers: [] }); + assert.deepEqual(defaults.subagents, { + enabled: false, + instructions: "on-demand", + providers: [], + }); assert.deepEqual(defaults.logging, { level: "info", format: "json", @@ -53,6 +57,7 @@ try { skills: { enabled: false, paths: ["~/skills"], agentDir: "~/agent" }, subagents: { enabled: true, + instructions: "preload", providers: [{ id: "codex", enabled: true }], }, logging: { @@ -96,6 +101,7 @@ try { assert.deepEqual(configured.skillPaths, ["~/skills"]); assert.equal(configured.agentDir, resolve(homedir(), "agent")); assert.equal(configured.subagents.enabled, true); + assert.equal(configured.subagents.instructions, "preload"); assert.equal(configured.oauth.ownerToken, "persisted-owner-token-long-enough"); assert.equal(configured.oauth.accessTokenTtlSeconds, 120); assert.deepEqual(configured.oauth.scopes, ["devspace", "admin"]); diff --git a/src/local-agent-catalog.test.ts b/src/local-agent-catalog.test.ts index 51a564921..cc156251d 100644 --- a/src/local-agent-catalog.test.ts +++ b/src/local-agent-catalog.test.ts @@ -8,6 +8,7 @@ import type { SubagentsConfig } from "./local-agent-config.js"; const config: SubagentsConfig = { enabled: true, + instructions: "on-demand", providers: [ { id: "codex", enabled: true, model: "gpt-default", effort: "medium" }, { id: "claude", enabled: true, model: "sonnet" }, diff --git a/src/local-agent-config.test.ts b/src/local-agent-config.test.ts index e37ceafac..665d96f2d 100644 --- a/src/local-agent-config.test.ts +++ b/src/local-agent-config.test.ts @@ -14,6 +14,7 @@ const config = subagentsConfigSchema.parse({ }); assert.deepEqual(config, { enabled: true, + instructions: "on-demand", providers: [ { id: "codex", enabled: true, model: "gpt-5.4", effort: "high" }, { id: "claude", enabled: false, model: "sonnet" }, @@ -23,6 +24,10 @@ assert.equal(isSubagentProviderEnabled(config, "codex"), true); assert.equal(isSubagentProviderEnabled(config, "claude"), false); assert.equal(isSubagentProviderEnabled(config, "pi"), false); assert.equal(subagentProviderConfig(config, "codex")?.model, "gpt-5.4"); +assert.equal( + subagentsConfigSchema.parse({ enabled: true, instructions: "preload", providers: [] }).instructions, + "preload", +); assert.throws( () => subagentsConfigSchema.parse({ diff --git a/src/local-agent-config.ts b/src/local-agent-config.ts index 62e9c35a0..9b889eafc 100644 --- a/src/local-agent-config.ts +++ b/src/local-agent-config.ts @@ -13,6 +13,7 @@ const providerSchema = z.object({ export const subagentsConfigSchema = z.object({ enabled: z.boolean(), + instructions: z.enum(["on-demand", "preload"]).default("on-demand"), providers: z.array(providerSchema), }).strict().superRefine((value, context) => { const seen = new Set(); diff --git a/src/local-agent-manager.test.ts b/src/local-agent-manager.test.ts index 4ca5ed28d..a3b50d202 100644 --- a/src/local-agent-manager.test.ts +++ b/src/local-agent-manager.test.ts @@ -40,6 +40,7 @@ const disabledProfile: LocalAgentProfile = { }; const subagents: SubagentsConfig = { enabled: true, + instructions: "on-demand", providers: [ { id: "codex", enabled: true, model: "gpt-default", effort: "medium" }, { id: "claude", enabled: true }, diff --git a/src/local-agent-profiles.test.ts b/src/local-agent-profiles.test.ts index d802c17f3..8a7d57db1 100644 --- a/src/local-agent-profiles.test.ts +++ b/src/local-agent-profiles.test.ts @@ -60,7 +60,7 @@ try { const enabledConfig = loadConfig(writeTestDevspaceConfig(configDir, { workspaces: { allowedRoots: [workspaceRoot] }, - subagents: { enabled: true, providers: [] }, + subagents: { enabled: true, instructions: "on-demand", providers: [] }, })); const profiles = await loadLocalAgentProfiles(enabledConfig, workspaceRoot); @@ -89,7 +89,7 @@ try { const disabledConfig = loadConfig(writeTestDevspaceConfig(configDir, { workspaces: { allowedRoots: [workspaceRoot] }, - subagents: { enabled: false, providers: [] }, + subagents: { enabled: false, instructions: "on-demand", providers: [] }, })); assert.deepEqual(await loadLocalAgentProfiles(disabledConfig, workspaceRoot), []); } finally { diff --git a/src/onboarding.test.ts b/src/onboarding.test.ts index b4236e9c0..ba3c0a224 100644 --- a/src/onboarding.test.ts +++ b/src/onboarding.test.ts @@ -15,11 +15,12 @@ assert.throws(() => resolveOnboardingUsage([]), /Choose ChatGPT, Coding Agents, assert.deepEqual( updateOnboardingSubagentsConfig( - { enabled: false, providers: [] }, + { enabled: false, instructions: "on-demand", providers: [] }, ["codex", "claude"], ), { enabled: true, + instructions: "on-demand", providers: [ { id: "codex", enabled: true }, { id: "claude", enabled: true }, @@ -29,6 +30,7 @@ assert.deepEqual( const configured = { enabled: true, + instructions: "preload" as const, providers: [ { id: "codex" as const, enabled: true, model: "gpt-5.4", effort: "high" }, { id: "claude" as const, enabled: true, model: "sonnet" }, @@ -38,6 +40,7 @@ assert.deepEqual( updateOnboardingSubagentsConfig(configured, ["claude"]), { enabled: true, + instructions: "preload", providers: [ { id: "codex", enabled: false, model: "gpt-5.4", effort: "high" }, { id: "claude", enabled: true, model: "sonnet" }, diff --git a/src/onboarding.ts b/src/onboarding.ts index 2642e776c..193dd57dd 100644 --- a/src/onboarding.ts +++ b/src/onboarding.ts @@ -36,6 +36,7 @@ export function updateOnboardingSubagentsConfig( const selected = new Set(selectedProviders); return { enabled: true, + instructions: current.instructions, providers: LOCAL_AGENT_PROVIDERS .filter((id) => selected.has(id) || current.providers.some((provider) => provider.id === id)) .map((id) => { diff --git a/src/server.test.ts b/src/server.test.ts index 254ce0a61..e31bad244 100644 --- a/src/server.test.ts +++ b/src/server.test.ts @@ -259,6 +259,7 @@ test("open_workspace omits providers disabled by configuration", async (t) => { ], subagents: { enabled: true, + instructions: "on-demand", providers: [ { id: "codex", enabled: true }, { id: "claude", enabled: false }, @@ -273,6 +274,33 @@ test("open_workspace omits providers disabled by configuration", async (t) => { ); }); +test("open_workspace advertises subagent instructions on demand by default", async (t) => { + const context = await fixture(t, { + localAgentProviders: [{ name: "codex", available: true }], + }); + + const opened = structuredContent(await callOpen(context.client, context.project, "chat-1")); + const skills = opened.skills as Array>; + assert.equal(skills.some((skill) => skill.name === "subagents"), true); + assert.doesNotMatch(String(opened.instruction), /# DevSpace subagents/); +}); + +test("open_workspace preloads subagent instructions when configured", async (t) => { + const context = await fixture(t, { + localAgentProviders: [{ name: "codex", available: true }], + subagents: { + enabled: true, + instructions: "preload", + providers: [{ id: "codex", enabled: true }], + }, + }); + + const opened = structuredContent(await callOpen(context.client, context.project, "chat-1")); + const skills = opened.skills as Array>; + assert.equal(skills.some((skill) => skill.name === "subagents"), false); + assert.match(String(opened.instruction), /# DevSpace subagents/); +}); + test("open_workspace scopes checkout reuse to OpenAI session metadata", async (t) => { const context = await fixture(t); const first = await callOpen(context.client, context.project, "chat-1"); @@ -551,7 +579,11 @@ async function fixture( server: { port: 1 }, workspaces: { allowedRoots: [root], worktreeRoot: join(root, ".worktrees") }, skills: { agentDir }, - subagents: { enabled: options.localAgentProviders !== undefined, providers: [] }, + subagents: { + enabled: options.localAgentProviders !== undefined, + instructions: "on-demand", + providers: [], + }, })); const modeConfig: ServerConfig = { ...loadedConfig, @@ -563,6 +595,7 @@ async function fixture( ...modeConfig, subagents: options.subagents ?? { enabled: true, + instructions: "on-demand", providers: initialProviderAvailability.map((provider) => ({ id: provider.name, enabled: true, diff --git a/src/server.ts b/src/server.ts index 03982a866..68a74bc31 100644 --- a/src/server.ts +++ b/src/server.ts @@ -453,8 +453,15 @@ function registerMcpSurface( workspaceId: workspace.id, root: workspace.root, }); + const preloadSubagents = config.subagents.enabled + && config.subagents.instructions === "preload"; + const subagentsSkill = workspace.skills.find((skill) => skill.name === "subagents"); + const preloadedSubagentInstructions = preloadSubagents && subagentsSkill + ? readFileSync(subagentsSkill.filePath, "utf8") + : undefined; const cardSkills = workspace.skills .filter((skill) => !skill.disableModelInvocation) + .filter((skill) => !(preloadSubagents && skill.name === "subagents")) .map((skill) => ({ name: skill.name, description: skill.description, @@ -489,7 +496,7 @@ function registerMcpSurface( const cardInstruction = config.skillsEnabled ? "Use this workspaceId for subsequent work in this project. Keep reusing it while working in this project. Follow loaded agentsFiles instructions. Before working under a path listed in availableAgentsFiles, read that instruction file. When a task matches an available skill in skills, read its path before proceeding." : "Use this workspaceId for subsequent work in this project. Keep reusing it while working in this project. Follow loaded agentsFiles instructions. Before working under a path listed in availableAgentsFiles, read that instruction file."; - const instruction = workspaceReused + const workspaceInstruction = workspaceReused ? [ `Workspace already open as ${workspace.id}.`, "Continue with this workspaceId.", @@ -498,6 +505,13 @@ function registerMcpSurface( : workspace.mode === "worktree" ? "Use this workspaceId for subsequent work in this isolated worktree. Keep reusing it while working in this worktree. Follow the project instructions, nested instruction files, skills, agent profiles, and diagnostics returned for it." : cardInstruction; + const instruction = preloadedSubagentInstructions && includeBootstrapContext + ? [ + workspaceInstruction, + "Subagent workflow instructions:", + preloadedSubagentInstructions, + ].join("\n\n") + : workspaceInstruction; const resultContent: ToolContent[] = [ { type: "text" as const, diff --git a/src/skills.test.ts b/src/skills.test.ts index d758dee81..357c417db 100644 --- a/src/skills.test.ts +++ b/src/skills.test.ts @@ -1,4 +1,4 @@ -import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import assert from "node:assert/strict"; @@ -195,17 +195,34 @@ try { false, ); + await mkdir(join(devspaceSkills, "subagents"), { recursive: true }); + await writeFile( + join(devspaceSkills, "subagents", "SKILL.md"), + [ + "---", + "name: subagents", + "description: stale user copy", + "---", + "", + "# Stale subagents skill", + ].join("\n"), + ); const experimentalConfig = loadConfig(writeTestDevspaceConfig(configDir, { server: { port: 1 }, workspaces: { allowedRoots: [projectRoot] }, skills: { agentDir }, - subagents: { enabled: true, providers: [] }, + subagents: { enabled: true, instructions: "on-demand", providers: [] }, })); + const experimentalSkills = loadWorkspaceSkills(experimentalConfig, projectRoot).skills; + const managedSubagents = experimentalSkills.find((skill) => skill.name === "subagents"); + assert.ok(managedSubagents); assert.equal( - loadWorkspaceSkills(experimentalConfig, projectRoot).skills.some( - (skill) => skill.name === "subagents", - ), - true, + managedSubagents.filePath, + join(devspaceSkills, "subagents", "SKILL.md"), + ); + assert.match( + await readFile(join(devspaceSkills, "subagents", "SKILL.md"), "utf8"), + /# DevSpace subagents/, ); const duplicateConfig = loadConfig(writeTestDevspaceConfig(configDir, { diff --git a/src/skills.ts b/src/skills.ts index 74c77344d..8f67d8e07 100644 --- a/src/skills.ts +++ b/src/skills.ts @@ -1,9 +1,18 @@ -import { existsSync } from "node:fs"; +import { + existsSync, + lstatSync, + mkdirSync, + readFileSync, + renameSync, + rmSync, + writeFileSync, +} from "node:fs"; import { homedir } from "node:os"; -import { join, resolve, sep } from "node:path"; +import { dirname, join, resolve, sep } from "node:path"; import { fileURLToPath } from "node:url"; import { loadSkills, + loadSkillsFromDir, type Skill, type LoadSkillsResult, } from "@earendil-works/pi-coding-agent"; @@ -27,20 +36,44 @@ function bundledSkillsDir(): string { return fileURLToPath(new URL("../skills", import.meta.url)); } -function hasSubagentsSkill(skillDir: string): boolean { - return existsSync(join(skillDir, SUBAGENTS_SKILL)); +function bundledSubagentsSkillPath(): string { + return join(bundledSkillsDir(), SUBAGENTS_SKILL); +} + +function syncManagedSubagentsSkill(config: ServerConfig): string { + const sourcePath = bundledSubagentsSkillPath(); + const targetPath = join(config.devspaceSkillsDir, SUBAGENTS_SKILL); + const source = readFileSync(sourcePath, "utf8"); + + if (existsSync(targetPath)) { + const stat = lstatSync(targetPath); + if (stat.isFile() && source === readFileSync(targetPath, "utf8")) { + return targetPath; + } + if (stat.isDirectory()) { + throw new Error(`Managed subagents skill path is a directory: ${targetPath}`); + } + } + + mkdirSync(dirname(targetPath), { recursive: true }); + const tempPath = `${targetPath}.${process.pid}.tmp`; + try { + writeFileSync(tempPath, source, { mode: 0o644 }); + rmSync(targetPath, { force: true }); + renameSync(tempPath, targetPath); + } finally { + rmSync(tempPath, { force: true }); + } + + return targetPath; } export function effectiveSkillPaths(config: ServerConfig, cwd: string): string[] { - const bundledSkills = bundledSkillsDir(); const defaultPathCandidates = [ join(homedir(), ".agents", "skills"), resolve(cwd, ".agents", "skills"), config.devspaceSkillsDir, join(config.agentDir, "skills"), - config.subagents.enabled && !hasSubagentsSkill(config.devspaceSkillsDir) - ? bundledSkills - : undefined, ]; const defaultPaths = defaultPathCandidates.filter( (path): path is string => path !== undefined && existsSync(path), @@ -63,6 +96,10 @@ function resolveSkillPath(path: string, cwd: string): string { export function loadWorkspaceSkills(config: ServerConfig, cwd: string): LoadedSkills { if (!config.skillsEnabled) return { skills: [], diagnostics: [] }; + if (config.subagents.enabled) { + syncManagedSubagentsSkill(config); + } + const result = loadSkills({ cwd, agentDir: config.agentDir, @@ -70,8 +107,25 @@ export function loadWorkspaceSkills(config: ServerConfig, cwd: string): LoadedSk includeDefaults: false, }); - if (config.subagents.enabled) return result; + const withoutSubagents = withoutSubagentsSkill(result); + if (!config.subagents.enabled) return withoutSubagents; + + const managedDir = dirname(join(config.devspaceSkillsDir, SUBAGENTS_SKILL)); + const managed = loadSkillsFromDir({ + dir: managedDir, + source: "devspace", + }).skills.find((skill) => skill.name === SUBAGENTS_SKILL_NAME); + if (!managed) { + throw new Error("Managed subagents skill could not be loaded."); + } + + return { + skills: [...withoutSubagents.skills, managed], + diagnostics: withoutSubagents.diagnostics, + }; +} +function withoutSubagentsSkill(result: LoadSkillsResult): LoadedSkills { return { skills: result.skills.filter((skill) => skill.name !== SUBAGENTS_SKILL_NAME), diagnostics: result.diagnostics.filter((diagnostic) => { diff --git a/src/workspace-conversation.test.ts b/src/workspace-conversation.test.ts index 45c45a14d..67ebbe2ac 100644 --- a/src/workspace-conversation.test.ts +++ b/src/workspace-conversation.test.ts @@ -421,7 +421,7 @@ async function fixture( server: { port: 1 }, workspaces: { allowedRoots: [root], worktreeRoot: join(root, ".worktrees") }, skills: { agentDir }, - subagents: { enabled: true, providers: [] }, + subagents: { enabled: true, instructions: "on-demand", providers: [] }, })); const openStore = () => { const store = new SqliteWorkspaceStore(stateDir); diff --git a/src/workspaces.test.ts b/src/workspaces.test.ts index e3506937f..06c354582 100644 --- a/src/workspaces.test.ts +++ b/src/workspaces.test.ts @@ -187,7 +187,7 @@ test("workspace cache evicts old contexts without losing advertised skill reads" worktreeRoot: join(context.root, ".devspace", "bounded-worktrees"), }, skills: { agentDir }, - subagents: { enabled: true, providers: [] }, + subagents: { enabled: true, instructions: "on-demand", providers: [] }, }, )); @@ -304,7 +304,7 @@ async function fixture(t: TestContext): Promise { worktreeRoot: join(root, ".devspace", "worktrees"), }, skills: { agentDir }, - subagents: { enabled: true, providers: [] }, + subagents: { enabled: true, instructions: "on-demand", providers: [] }, })); t.after(async () => {