Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions docs/chatgpt-coding-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
17 changes: 17 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Run `devspace init` to create both files. `devspace config set publicBaseUrl
},
"subagents": {
"enabled": false,
"instructions": "on-demand",
"providers": [],
},
"logging": {
Expand Down Expand Up @@ -102,13 +103,19 @@ 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
{
"configVersion": 1,
"subagents": {
"enabled": true,
"instructions": "on-demand",
"providers": [
{
"id": "codex",
Expand All @@ -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.
Expand Down
16 changes: 12 additions & 4 deletions docs/gotchas.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,21 +222,27 @@ 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
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`:

Expand All @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions schema/v1/devspace.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,13 +158,22 @@
"subagents": {
"default": {
"enabled": false,
"instructions": "on-demand",
"providers": []
},
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"instructions": {
"default": "on-demand",
"type": "string",
"enum": [
"on-demand",
"preload"
]
},
"providers": {
"type": "array",
"items": {
Expand Down
2 changes: 2 additions & 0 deletions skills/subagents/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down
6 changes: 5 additions & 1 deletion src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
8 changes: 7 additions & 1 deletion src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -53,6 +57,7 @@ try {
skills: { enabled: false, paths: ["~/skills"], agentDir: "~/agent" },
subagents: {
enabled: true,
instructions: "preload",
providers: [{ id: "codex", enabled: true }],
},
logging: {
Expand Down Expand Up @@ -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"]);
Expand Down
1 change: 1 addition & 0 deletions src/local-agent-catalog.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand Down
5 changes: 5 additions & 0 deletions src/local-agent-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" },
Expand All @@ -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({
Expand Down
1 change: 1 addition & 0 deletions src/local-agent-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LocalAgentProvider>();
Expand Down
1 change: 1 addition & 0 deletions src/local-agent-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
4 changes: 2 additions & 2 deletions src/local-agent-profiles.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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 {
Expand Down
5 changes: 4 additions & 1 deletion src/onboarding.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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" },
Expand All @@ -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" },
Expand Down
1 change: 1 addition & 0 deletions src/onboarding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
35 changes: 34 additions & 1 deletion src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand All @@ -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<Record<string, unknown>>;
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<Record<string, unknown>>;
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");
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
Loading
Loading