Skip to content
Open
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
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,21 @@ DevSpace gives ChatGPT tools to:
- discover local agent skills from your skill folders
- show tool cards and optional change summaries in ChatGPT Apps-compatible hosts

## DevSpace Subagents

With `DEVSPACE_SUBAGENTS=1`, DevSpace can delegate bounded coding work to
subagent workers backed by your locally installed agent CLIs (`codex`,
`claude`, `opencode`, `pi`, `cursor-agent`, `copilot`). DevSpace runs each
provider's host-installed binary; it bundles no copies of them.

For the Codex provider, DevSpace executes your PATH `codex` binary, or the one
given by the `CODEX_COMMAND` environment override. It reports the detected Codex
CLI version and a minimum supported version (0.142.5) wherever provider
availability is shown (`open_workspace`, the `devspace serve` banner, and
`devspace agents` output), and stamps failed runs with the Codex version and raw
CLI stderr. Sessions persist in your normal `~/.codex/sessions`, so follow-ups
resume work the same way your own `codex` session would.

## Mental Model

DevSpace is remote access to selected local folders.
Expand Down
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ sessions.
| `DEVSPACE_SUBAGENTS` | Set to `1` to expose configured agent profiles as Subagents. Experimental and disabled by default. |
| `DEVSPACE_AGENT_DIR` | Defaults to `~/.codex`; its `skills` child is loaded for compatibility. |
| `DEVSPACE_SKILL_PATHS` | Optional comma-separated additional skill directories. |
| `CODEX_COMMAND` | Optional absolute path to the `codex` binary used for the Codex subagent provider, mirroring `CLAUDE_COMMAND` and `PI_COMMAND`. Defaults to `codex` on `PATH`. |

DevSpace discovers standard Agent Skills from:

Expand Down
139 changes: 2 additions & 137 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
"@earendil-works/pi-coding-agent": "^0.80.3",
"@modelcontextprotocol/ext-apps": "^1.7.2",
"@modelcontextprotocol/sdk": "^1.29.0",
"@openai/codex-sdk": "^0.142.5",
"@opencode-ai/sdk": "^1.17.13",
"@pierre/diffs": "^1.2.5",
"better-sqlite3": "^12.10.0",
Expand Down
8 changes: 8 additions & 0 deletions skills/subagent-delegation/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,14 @@ Do not run provider CLIs such as `codex`, `claude`, `opencode`, `pi`,
`cursor-agent`, or `copilot` directly unless you are explicitly debugging
DevSpace agent integration.

DevSpace runs each provider's host-installed CLI; it does not bundle hidden
copies. For the Codex provider, DevSpace executes your PATH `codex` (or the
binary at `CODEX_COMMAND`) and reports the detected CLI version and the minimum
supported version in `open_workspace` and the availability summary. If a run
fails, the session error includes the detected Codex version and the CLI's raw
stderr, so a model gate keyed on CLI version can be diagnosed without digging
into process internals.

## Choosing a profile

Choose profiles from the compact subagent profile catalog returned by
Expand Down
8 changes: 8 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import {
assertLocalAgentProviderAvailable,
formatLocalAgentProviderAvailabilitySummary,
getLocalAgentProviderAvailabilitySnapshot,
} from "./local-agent-availability.js";
import {
formatAvailableLocalAgentTargets,
Expand Down Expand Up @@ -349,6 +350,13 @@ async function runAgentsCommand(args: string[]): Promise<void> {

async function runAgentsList(): Promise<void> {
const config = loadConfig();
if (config.subagents) {
console.log(
`subagent providers: ${formatLocalAgentProviderAvailabilitySummary(
getLocalAgentProviderAvailabilitySnapshot(),
)}`,
);
}
const store = createLocalAgentStore(config);
const agents = store.list(resolveCurrentWorkspaceScope());

Expand Down
23 changes: 23 additions & 0 deletions src/local-agent-adapters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
resolveAcpModelConfigUpdate,
resolveAcpThinkingConfigUpdate,
} from "./local-agent-adapters.js";
import { codexCommandEnvironment } from "./local-agent-codex.js";
import { removeDevspaceNodeModulesBinFromPath } from "./local-agent-path.js";
import type { LocalAgentProvider } from "./local-agent-profiles.js";

Expand Down Expand Up @@ -388,3 +389,25 @@ assert.equal(

assert.equal(env.PATH, [devspaceBin, "/home/user/.local/bin"].join(delimiter));
}

{
const devspaceBin = `${process.cwd()}/node_modules/.bin`;
const userBin = "/home/user/.local/bin";
const env = codexCommandEnvironment({
CODEX_INTERNAL_ORIGINATOR_OVERRIDE: "devspace",
PATH: [devspaceBin, userBin].join(delimiter),
});

assert.equal(env.CODEX_INTERNAL_ORIGINATOR_OVERRIDE, undefined);
assert.equal(env.PATH, userBin);
}

{
const devspaceBin = `${process.cwd()}/node_modules/.bin`;
const env = codexCommandEnvironment({
CODEX_COMMAND: "/custom/codex",
PATH: [devspaceBin, "/home/user/.local/bin"].join(delimiter),
});

assert.equal(env.PATH, [devspaceBin, "/home/user/.local/bin"].join(delimiter));
}
19 changes: 17 additions & 2 deletions src/local-agent-adapters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import { resolve } from "node:path";
import { Readable, Writable } from "node:stream";
import type { EffortLevel } from "@anthropic-ai/claude-agent-sdk";
import type { LocalAgentProvider } from "./local-agent-profiles.js";
import {
codexCommandEnvironment,
resolveCodexCommand,
} from "./local-agent-codex.js";
import { removeDevspaceNodeModulesBinFromPath } from "./local-agent-path.js";
import {
createCodexSdkLocalAgentRuntime,
CodexCliLocalAgentRuntime,
type LocalAgentRunInput,
type LocalAgentRunResult,
} from "./local-agent-runtime.js";
Expand Down Expand Up @@ -48,7 +52,18 @@ class CodexLocalAgentAdapter implements LocalAgentAdapter {
readonly provider = "codex" as const;

async run(input: LocalAgentRunInput): Promise<LocalAgentRunResult> {
const runtime = await createCodexSdkLocalAgentRuntime();
const env = codexCommandEnvironment(process.env);
const resolved = resolveCodexCommand(env);
if (!resolved) {
throw new Error(
"codex provider is not available: codex executable not found. Install codex or set CODEX_COMMAND.",
);
}
const runtime = new CodexCliLocalAgentRuntime({
command: resolved.executable,
env,
version: resolved.version,
});
return runtime.run(input);
}
}
Expand Down
37 changes: 34 additions & 3 deletions src/local-agent-availability.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,42 @@
import assert from "node:assert/strict";
import { chmodSync, mkdtempSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
checkLocalAgentProviderAvailability,
formatLocalAgentProviderAvailabilitySummary,
getLocalAgentProviderAvailabilitySnapshot,
} from "./local-agent-availability.js";

assert.equal(checkLocalAgentProviderAvailability("codex").available, true);
const availableCodex = checkLocalAgentProviderAvailability("codex");
assert.equal(availableCodex.available, true);
assert.match(availableCodex.version ?? "", /^\d+\.\d+/);
assert.equal(availableCodex.minimumVersion, "0.142.5");
Comment on lines +11 to +14

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Do not require a host Codex installation in this unit test.

This assertion probes the real host environment. It fails on clean developer or CI hosts without a compatible Codex CLI. Use a controlled CODEX_COMMAND fixture that emits a supported version. If this is an integration check, separate it and skip it when the host requirement is absent.

🧰 Tools
🪛 GitHub Actions: CI / 0_Smoke (macos-latest).txt

[error] 12-12: npm test failed: AssertionError expected true but received false (false !== true).

🪛 GitHub Actions: CI / 1_Smoke (ubuntu-latest).txt

[error] 12-12: npm test failed: AssertionError [ERR_ASSERTION] expected true but received false.

🪛 GitHub Actions: CI / 2_Smoke (windows-latest).txt

[error] 12-12: Test assertion failed: expected true but received false (AssertionError [ERR_ASSERTION]). The npm test command failed with exit code 1.

🪛 GitHub Actions: CI / Smoke (macos-latest)

[error] 12-12: npm test failed: assertion expected true but received false (AssertionError [ERR_ASSERTION]). Command failed with exit code 1.

🪛 GitHub Actions: CI / Smoke (ubuntu-latest)

[error] 12-12: Test assertion failed: expected true but received false (AssertionError [ERR_ASSERTION]). The 'npm test' command failed with exit code 1.

🪛 GitHub Actions: CI / Smoke (windows-latest)

[error] 12-12: npm test failed: AssertionError because the actual value was false but the expected value was true. Command failed with exit code 1.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/local-agent-availability.test.ts` around lines 11 - 14, Update the test
around checkLocalAgentProviderAvailability to use a controlled CODEX_COMMAND
fixture that emits a supported Codex version instead of probing the host
installation. Preserve the assertions for availability, version format, and
minimumVersion; if host validation is intended separately, move it to an
integration check that skips when Codex is unavailable.


{
const availability = checkLocalAgentProviderAvailability("codex", {
...process.env,
CODEX_COMMAND: "/definitely/missing/devspace-codex",
});
assert.equal(availability.available, false);
assert.match(availability.reason ?? "", /executable not found/);
}

{
const directory = mkdtempSync(join(tmpdir(), "devspace-codex-test-"));
const oldCodex = join(directory, "codex");
writeFileSync(oldCodex, "#!/bin/sh\necho 'codex-cli 0.130.0'\n", { mode: 0o755 });
chmodSync(oldCodex, 0o755);

const availability = checkLocalAgentProviderAvailability("codex", {
...process.env,
CODEX_COMMAND: oldCodex,
});
assert.equal(availability.available, false);
assert.equal(availability.version, "0.130.0");
assert.equal(availability.minimumVersion, "0.142.5");
assert.match(availability.reason ?? "", /below the minimum supported version 0\.142\.5/);
}

{
const availability = checkLocalAgentProviderAvailability("pi", {
Expand All @@ -30,8 +61,8 @@ assert.equal(checkLocalAgentProviderAvailability("codex").available, true);

assert.equal(
formatLocalAgentProviderAvailabilitySummary([
{ name: "codex", available: true },
{ name: "codex", available: true, version: "0.147.0", minimumVersion: "0.142.5" },
{ name: "pi", available: false, reason: "pi executable not found" },
]),
"available: codex; unavailable: pi (pi executable not found)",
"available: codex (0.147.0, min 0.142.5); unavailable: pi (pi executable not found)",
);
Loading
Loading