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
10 changes: 5 additions & 5 deletions docs/agent-profile-schema.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,11 @@ Recommended body content:
The Subagent skill teaches only:

```bash
devspace agents ls
devspace agents targets
devspace agents run <profile-or-provider> "<prompt>"
devspace agents continue <id> "<prompt>"
devspace agents show <id>
devspace agents ls --json
devspace agents targets --json
devspace agents run <profile-or-provider> "<prompt>" --json
devspace agents continue <id> "<prompt>" --json
devspace agents show <id> --json
```

`open_workspace` exposes compact profile metadata:
Expand Down
7 changes: 4 additions & 3 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,9 +182,10 @@ profile value, which wins over the provider default. The legacy boolean
configuration should use the explicit object form.

`devspace agents targets` shows usable providers and profiles for the current
workspace. Add `--json` to inspect the availability and usability of enabled
providers. Disabled and unconfigured providers are omitted. Provider
availability is runtime state and never rewrites the configuration.
workspace. Add `--json` for a compact list of exact target names and their
selection metadata. Disabled, unavailable, and unconfigured providers are
omitted. Provider availability is runtime state and never rewrites the
configuration.

Grok Build is discovered from the `grok` executable. Authenticate it with
`grok login` or `XAI_API_KEY`; DevSpace does not read or store Grok credentials.
Expand Down
9 changes: 6 additions & 3 deletions docs/local-agent-daemon.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,12 @@ devspace agents daemon logs
```

Agent commands accept `--json` when a machine-readable response is needed.
Immediate failures are emitted as `{ ok: false, error: { code, message,
retryable, ... } }`; successful `show`, `ls`, `run`, and `continue` output keeps
the structured error fields on agent records when present.
They emit one compact JSON value. `run` and `continue` return only the logical
agent ID and status, `ls` returns session summaries, and `show` returns the
response or structured failure for one agent. Internal workspace paths,
provider session IDs, timestamps, and prior responses are not included in list
or receipt output. Immediate failures are emitted as
`{ error: { code, message, retryable, ... } }` with a non-zero exit code.
Successful `daemon status` and `daemon stop` output the daemon status object,
and successful `daemon logs` output is `{ "logs": "<text>" }`.

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"dev": "node scripts/dev-server.mjs",
"postinstall": "node scripts/fix-node-pty-permissions.mjs",
"start": "node dist/cli.js serve",
"test": "tsx src/config.test.ts && tsx src/onboarding.test.ts && tsx src/cli-workspace.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-config.test.ts && tsx src/local-agent-catalog.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-daemon-lifecycle.test.ts && tsx src/local-agent-daemon-protocol.test.ts && tsx src/local-agent-daemon.test.ts && tsx src/local-agent-codex.test.ts && tsx src/local-agent-opencode.test.ts && tsx src/local-agent-acp.test.ts && tsx src/local-agent-grok.test.ts && tsx src/local-agent-pi-sandbox.test.ts && tsx src/local-agent-pi.test.ts && tsx src/local-agent-claude.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/local-agent-manager.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
"test": "tsx src/config.test.ts && tsx src/onboarding.test.ts && tsx src/cli-workspace.test.ts && tsx src/request-meta.test.ts && tsx src/incoming-artifacts.test.ts && tsx src/artifact-download.test.ts && tsx src/ui/card-types.test.ts && tsx src/ui/patch-display.test.ts && tsx src/ui/tool-display.test.ts && tsx src/apply-patch.test.ts && tsx src/process-platform.test.ts && tsx src/process-sessions.test.ts && tsx src/mcp-sessions.test.ts && tsx src/server-shutdown.test.ts && tsx src/local-agent-config.test.ts && tsx src/local-agent-catalog.test.ts && tsx src/local-agent-presentation.test.ts && tsx src/local-agent-runtime.test.ts && tsx src/local-agent-daemon-lifecycle.test.ts && tsx src/local-agent-daemon-protocol.test.ts && tsx src/local-agent-daemon.test.ts && tsx src/local-agent-codex.test.ts && tsx src/local-agent-opencode.test.ts && tsx src/local-agent-acp.test.ts && tsx src/local-agent-grok.test.ts && tsx src/local-agent-pi-sandbox.test.ts && tsx src/local-agent-pi.test.ts && tsx src/local-agent-claude.test.ts && tsx src/local-agent-adapters.test.ts && tsx src/local-agent-availability.test.ts && tsx src/local-agent-profiles.test.ts && tsx src/local-agent-targets.test.ts && tsx src/local-agent-store.test.ts && tsx src/local-agent-manager.test.ts && tsx src/roots.test.ts && tsx src/skills.test.ts && tsx src/workspaces.test.ts && tsx src/workspace-conversation.test.ts && tsx src/review-checkpoints.test.ts && tsx src/server.test.ts && tsx src/oauth-store.test.ts && tsx src/cli.test.ts",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"keywords": [],
Expand Down
7 changes: 5 additions & 2 deletions skills/subagents/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,14 @@ devspace agents ls --json
session and context.
- `ls` returns sessions belonging to the current project.

Call `show --json` again later while the status is `starting` or `running`. `idle` means the response is ready. `error` and `stopped` are terminal without a successful response. Continue an agent when its existing context is useful; start another agent for unrelated work.
Run `devspace agents show <id> --json` again later while the status is `running`.
`completed` includes the response. `failed` includes a structured error, and
`stopped` is terminal without a successful response. Continue an agent when its
existing context is useful; start another agent for unrelated work.

## Good uses

- Review a change for correctness, security, or missing tests.
- Investigate a bounded part of a codebase and report findings.
- Implement one isolated change with clear acceptance criteria.
- Run a focused verification pass after other work.
- Run a focused verification pass after other work.
31 changes: 26 additions & 5 deletions src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ try {
model: "gpt-5.4",
effort: "high",
}).id,
{ status: "idle" },
{ status: "idle", latestResponse: "Review complete.", providerSessionId: "provider_secret" },
);
const other = store.update(
store.create({
Expand Down Expand Up @@ -148,10 +148,33 @@ try {
},
});

assert.match(output, new RegExp(`${current.id} idle reviewer codex gpt-5\\.4 effort=high`));
assert.doesNotMatch(output, /profile reviewer/);
assert.equal(output.trim(), `${current.id} completed reviewer`);
assert.doesNotMatch(output, new RegExp(other.id));

const { stdout: jsonOutput } = await execFileAsync(
"node",
["--import", "tsx", "src/cli.ts", "agents", "ls", "--json"],
{
cwd: process.cwd(),
encoding: "utf8",
env: {
...process.env,
DEVSPACE_CONFIG_DIR: configDir,
DEVSPACE_ALLOWED_ROOTS: projectRoot,
DEVSPACE_STATE_DIR: stateDir,
DEVSPACE_WORKSPACE_ID: "ws_current",
DEVSPACE_WORKSPACE_ROOT: projectRoot,
DEVSPACE_SUBAGENTS: "1",
DEVSPACE_OAUTH_OWNER_TOKEN: "test-owner-token-that-is-long-enough",
},
},
);
assert.equal(
jsonOutput,
`${JSON.stringify([{ id: current.id, status: "completed", target: "reviewer" }])}\n`,
);
assert.doesNotMatch(jsonOutput, /Review complete|provider_secret|workspaceRoot|providerSessionId/);

const { stdout: directOutput } = await execFileAsync(
"node",
["--import", tsxLoader, cliPath, "agents", "ls"],
Expand Down Expand Up @@ -200,10 +223,8 @@ try {
assert.ok(commandFailure, "structured CLI errors should exit non-zero");
const stdout = (commandFailure as { stdout?: string }).stdout ?? "";
const payload = JSON.parse(stdout) as {
ok: boolean;
error: { code: string; message: string; retryable: boolean; target: string };
};
assert.equal(payload.ok, false);
assert.equal(payload.error.code, "UNKNOWN_TARGET");
assert.equal(payload.error.message, "Unknown subagent profile or provider: missing.");
assert.equal(payload.error.retryable, false);
Expand Down
76 changes: 36 additions & 40 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
import {
buildLocalAgentCatalog,
buildLocalAgentProviderStatuses,
formatLocalAgentCatalog,
formatLocalAgentProviderStatusSummary,
} from "./local-agent-catalog.js";
import { loadLocalAgentProfiles } from "./local-agent-profiles.js";
Expand All @@ -26,7 +25,16 @@ import {
} from "./local-agent-targets.js";
import { createLocalAgentClient } from "./local-agent-client.js";
import { toAgentErrorPayload, type LocalAgentError } from "./local-agent-errors.js";
import type { LocalAgentRecord } from "./local-agent-store.js";
import {
formatAgentObservation,
formatAgentReceipt,
formatAgentSummary,
formatAgentTargetCatalog,
presentAgentObservation,
presentAgentReceipt,
presentAgentSummary,
presentAgentTargetCatalog,
} from "./local-agent-presentation.js";
import {
type OnboardingDestination,
SUBAGENT_SKILL_INSTALL_COMMAND,
Expand Down Expand Up @@ -437,7 +445,9 @@ async function runAgentsTargets(args: string[], json: boolean): Promise<void> {
getLocalAgentProviderAvailabilitySnapshot(),
);
const catalog = buildLocalAgentCatalog(config.subagents, profiles, providers);
console.log(json ? JSON.stringify(catalog, null, 2) : formatLocalAgentCatalog(catalog));
const output = presentAgentTargetCatalog(catalog);
if (json) printJson(output);
else console.log(formatAgentTargetCatalog(output));
}

async function runAgentsList(args: string[], json: boolean): Promise<void> {
Expand All @@ -448,8 +458,9 @@ async function runAgentsList(args: string[], json: boolean): Promise<void> {
const agents = presentAgentResult(result, json);
if (!agents) return;

const summaries = agents.map(presentAgentSummary);
if (json) {
console.log(JSON.stringify(agents, null, 2));
printJson(summaries);
return;
}

Expand All @@ -458,8 +469,8 @@ async function runAgentsList(args: string[], json: boolean): Promise<void> {
return;
}

for (const agent of agents) {
console.log(formatAgentLine(agent));
for (const summary of summaries) {
console.log(formatAgentSummary(summary));
}
}

Expand All @@ -478,11 +489,12 @@ async function runAgentsRun(args: string[], json: boolean): Promise<void> {
});
const record = presentAgentResult(result, json);
if (!record) return;
const receipt = presentAgentReceipt(record);
if (json) {
console.log(JSON.stringify(record, null, 2));
printJson(receipt);
return;
}
console.log(formatAgentLine(record));
console.log(formatAgentReceipt(receipt));
}

async function runAgentsContinue(args: string[], json: boolean): Promise<void> {
Expand All @@ -496,11 +508,12 @@ async function runAgentsContinue(args: string[], json: boolean): Promise<void> {
}, scope);
const record = presentAgentResult(result, json);
if (!record) return;
const receipt = presentAgentReceipt(record);
if (json) {
console.log(JSON.stringify(record, null, 2));
printJson(receipt);
return;
}
console.log(formatAgentLine(record));
console.log(formatAgentReceipt(receipt));
}

async function runAgentsShow(args: string[], json: boolean): Promise<void> {
Expand All @@ -522,23 +535,9 @@ async function runAgentsShow(args: string[], json: boolean): Promise<void> {
record = refreshed;
}

if (json) {
console.log(JSON.stringify(record, null, 2));
return;
}

console.log(formatAgentLine(record));
if (record.latestResponse) {
console.log(record.latestResponse);
return;
}
if (record.error) {
console.log(record.error);
return;
}
if (record.status === "starting" || record.status === "running") {
console.log(`No final response yet. Call \`devspace agents show ${record.id}\` again later.`);
}
const observation = presentAgentObservation(record);
if (json) printJson(observation);
else console.log(formatAgentObservation(observation));
}

async function runAgentsDaemon(args: string[], json: boolean): Promise<void> {
Expand All @@ -550,19 +549,21 @@ async function runAgentsDaemon(args: string[], json: boolean): Promise<void> {
case "status": {
const status = presentAgentResult(await client.status(), json);
if (!status) return;
console.log(JSON.stringify(status, null, 2));
printJson(status);
return;
}
case "stop": {
const status = presentAgentResult(await client.stop(), json);
if (!status) return;
console.log(json ? JSON.stringify(status, null, 2) : "Local agent daemon stop requested.");
if (json) printJson(status);
else console.log("Local agent daemon stop requested.");
return;
}
case "logs": {
const logs = presentAgentResult(await client.logs(), json);
if (logs === undefined) return;
console.log(json ? JSON.stringify({ logs }, null, 2) : (logs || "No local agent daemon logs found."));
if (json) printJson({ logs });
else console.log(logs || "No local agent daemon logs found.");
return;
}
default:
Expand All @@ -589,28 +590,23 @@ function extractJsonOption(args: string[]): { args: string[]; json: boolean } {
return { args: commandArgs, json };
}

function formatAgentLine(agent: Pick<
LocalAgentRecord,
"id" | "status" | "profileName" | "provider" | "model" | "effort"
>): string {
const model = agent.model ? ` ${agent.model}` : "";
const effort = agent.effort ? ` effort=${agent.effort}` : "";
return `${agent.id} ${agent.status} ${agent.profileName} ${agent.provider}${model}${effort}`;
}

function presentAgentResult<T, E extends LocalAgentError>(
result: BetterResult<T, E>,
json: boolean,
): T | undefined {
if (result.isOk()) return result.value;
if (json) {
console.log(JSON.stringify({ ok: false, error: toAgentErrorPayload(result.error) }, null, 2));
printJson({ error: toAgentErrorPayload(result.error) });
process.exitCode = 1;
return undefined;
}
throw new Error(result.error.message);
}

function printJson(value: unknown): void {
console.log(JSON.stringify(value));
}

function sleep(ms: number): Promise<void> {
return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
}
Expand Down
Loading
Loading