From e5a59c0e531d2abf1418a730f2f5b5c4f4aa8cef Mon Sep 17 00:00:00 2001
From: haosenwang1018 <1293965075@qq.com>
Date: Mon, 27 Apr 2026 16:54:17 +0800
Subject: [PATCH] feat: add run workspace option
---
README.md | 3 +++
docs.md | 6 ++++++
src/commands/run.ts | 26 ++++++++++++++++----------
src/runners/claude.ts | 8 ++++++--
src/runners/crewai.ts | 13 ++++++++++---
src/runners/gemini.ts | 5 +++++
src/runners/git.ts | 20 ++++++++++++--------
src/runners/gitclaw.ts | 5 +++++
src/runners/github.ts | 1 +
src/runners/lyzr.ts | 1 +
src/runners/nanobot.ts | 8 ++++++--
src/runners/openai.ts | 13 ++++++++++---
src/runners/openclaw.ts | 10 +++++++---
src/runners/opencode.ts | 5 +++++
14 files changed, 93 insertions(+), 31 deletions(-)
diff --git a/README.md b/README.md
index 30ac8d3..cf289e7 100644
--- a/README.md
+++ b/README.md
@@ -374,6 +374,9 @@ gapman export --format system-prompt
# Run an agent directly
gapman run ./my-agent --adapter lyzr
+
+# Run an agent definition against a separate target workspace
+gapman run --dir ./agents/reviewer --workspace ~/code/my-app --adapter claude -p "Review this repository"
```
## Inheritance & Composition
diff --git a/docs.md b/docs.md
index 1551619..f5447b8 100644
--- a/docs.md
+++ b/docs.md
@@ -629,12 +629,15 @@ gitagent run [options]
| `-d, --dir
` | — | Local directory (alternative to `--repo`) |
| `-a, --adapter ` | `claude` | Adapter (see table below) |
| `-b, --branch ` | `main` | Git branch or tag to clone |
+| `-w, --workspace ` | Agent directory | Working directory for spawned agent process |
| `--refresh` | `false` | Force re-clone (pull latest) |
| `--no-cache` | `false` | Clone to temp dir, delete on exit |
| `-p, --prompt ` | — | Initial prompt (non-interactive for some adapters) |
Either `--repo` or `--dir` is required.
+`--workspace` lets an agent definition live separately from the repository it operates on. It is honored by adapters that can safely set the spawned process working directory directly, including `claude`, `openai`, `crewai`, `openclaw`, and `nanobot`. Adapters that generate an isolated runtime workspace, such as `opencode`, `gemini`, and `gitclaw`, continue to run from that prepared workspace to avoid overwriting files such as `AGENTS.md`, `GEMINI.md`, or `agent.yaml` in the target repository.
+
**Available adapters:**
| Adapter | Mode | Requirements |
@@ -668,6 +671,9 @@ gitagent run -r https://github.com/user/agent -a git -p "Hello"
# One-shot prompt mode
gitagent run -d ./my-agent -p "Review my authentication code"
+# Run an agent definition against a separate target workspace
+gitagent run -d ./agents/reviewer --workspace ~/code/my-app -a claude -p "Review this repository"
+
# Run a specific branch, force refresh
gitagent run -r https://github.com/user/agent -b develop --refresh
diff --git a/src/commands/run.ts b/src/commands/run.ts
index bdc8480..733f017 100644
--- a/src/commands/run.ts
+++ b/src/commands/run.ts
@@ -24,6 +24,7 @@ interface RunOptions {
cache: boolean;
prompt?: string;
dir?: string;
+ workspace?: string;
}
export const runCommand = new Command('run')
@@ -35,6 +36,7 @@ export const runCommand = new Command('run')
.option('--no-cache', 'Clone to temp dir, delete on exit')
.option('-p, --prompt ', 'Initial prompt to send to the agent')
.option('-d, --dir ', 'Use local directory instead of git URL')
+ .option('-w, --workspace ', 'Working directory for the spawned agent process')
.action(async (options: RunOptions) => {
let agentDir: string;
let cleanup: (() => void) | undefined;
@@ -89,40 +91,43 @@ export const runCommand = new Command('run')
label('Model', manifest.model.preferred);
}
label('Adapter', options.adapter);
+ if (options.workspace) {
+ label('Workspace', resolve(options.workspace));
+ }
divider();
// Run with selected adapter
try {
switch (options.adapter) {
case 'claude':
- runWithClaude(agentDir, manifest, { prompt: options.prompt });
+ runWithClaude(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'openai':
- runWithOpenAI(agentDir, manifest);
+ runWithOpenAI(agentDir, manifest, { workspace: options.workspace });
break;
case 'crewai':
- runWithCrewAI(agentDir, manifest);
+ runWithCrewAI(agentDir, manifest, { workspace: options.workspace });
break;
case 'openclaw':
- runWithOpenClaw(agentDir, manifest, { prompt: options.prompt });
+ runWithOpenClaw(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'nanobot':
- runWithNanobot(agentDir, manifest, { prompt: options.prompt });
+ runWithNanobot(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'lyzr':
- await runWithLyzr(agentDir, manifest, { prompt: options.prompt });
+ await runWithLyzr(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'github':
- await runWithGitHub(agentDir, manifest, { prompt: options.prompt });
+ await runWithGitHub(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'opencode':
- runWithOpenCode(agentDir, manifest, { prompt: options.prompt });
+ runWithOpenCode(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'gemini':
- runWithGemini(agentDir, manifest, { prompt: options.prompt });
+ runWithGemini(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'gitclaw':
- runWithGitclaw(agentDir, manifest, { prompt: options.prompt });
+ runWithGitclaw(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'git':
if (!options.repo) {
@@ -135,6 +140,7 @@ export const runCommand = new Command('run')
refresh: options.refresh,
noCache: !options.cache,
prompt: options.prompt,
+ workspace: options.workspace,
});
break;
case 'prompt':
diff --git a/src/runners/claude.ts b/src/runners/claude.ts
index 5c5d6b3..63be9ec 100644
--- a/src/runners/claude.ts
+++ b/src/runners/claude.ts
@@ -1,5 +1,5 @@
import { writeFileSync, unlinkSync, existsSync, readdirSync, readFileSync } from 'node:fs';
-import { join } from 'node:path';
+import { join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { spawnSync } from 'node:child_process';
import { randomBytes } from 'node:crypto';
@@ -11,6 +11,7 @@ import { error, info, warn } from '../utils/format.js';
export interface ClaudeRunOptions {
prompt?: string;
+ workspace?: string;
}
export function runWithClaude(agentDir: string, manifest: AgentManifest, options: ClaudeRunOptions = {}): void {
@@ -80,7 +81,10 @@ export function runWithClaude(agentDir: string, manifest: AgentManifest, options
// from interfering with argument parsing of other flags
args.push('--append-system-prompt', systemPrompt);
+ const runCwd = resolve(options.workspace ?? agentDir);
+
info(`Launching Claude Code with agent "${manifest.name}"...`);
+ info(`Working directory: ${runCwd}`);
// Resolve the real Claude Code binary, skipping any node_modules/.bin/claude
// that may shadow it (e.g. when running via npx)
@@ -90,7 +94,7 @@ export function runWithClaude(agentDir: string, manifest: AgentManifest, options
try {
const result = spawnSync(claudePath, args, {
stdio: 'inherit',
- cwd: agentDir,
+ cwd: runCwd,
});
if (result.error) {
diff --git a/src/runners/crewai.ts b/src/runners/crewai.ts
index 8947331..c3dc2c4 100644
--- a/src/runners/crewai.ts
+++ b/src/runners/crewai.ts
@@ -1,5 +1,5 @@
import { writeFileSync, unlinkSync } from 'node:fs';
-import { join } from 'node:path';
+import { join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { spawnSync } from 'node:child_process';
import { randomBytes } from 'node:crypto';
@@ -7,18 +7,25 @@ import { exportToCrewAI } from '../adapters/crewai.js';
import { AgentManifest } from '../utils/loader.js';
import { error, info } from '../utils/format.js';
-export function runWithCrewAI(agentDir: string, _manifest: AgentManifest): void {
+export interface CrewAIRunOptions {
+ workspace?: string;
+}
+
+export function runWithCrewAI(agentDir: string, _manifest: AgentManifest, options: CrewAIRunOptions = {}): void {
const config = exportToCrewAI(agentDir);
const tmpFile = join(tmpdir(), `gitagent-${randomBytes(4).toString('hex')}.yaml`);
writeFileSync(tmpFile, config, 'utf-8');
+ const runCwd = resolve(options.workspace ?? agentDir);
+
info(`Running CrewAI agent from "${agentDir}"...`);
+ info(`Working directory: ${runCwd}`);
try {
const result = spawnSync('crewai', ['kickoff', '--config', tmpFile], {
stdio: 'inherit',
- cwd: agentDir,
+ cwd: runCwd,
env: { ...process.env },
});
diff --git a/src/runners/gemini.ts b/src/runners/gemini.ts
index 645a0f9..c2f9aca 100644
--- a/src/runners/gemini.ts
+++ b/src/runners/gemini.ts
@@ -9,6 +9,7 @@ import { error, info } from '../utils/format.js';
export interface GeminiRunOptions {
prompt?: string;
+ workspace?: string;
}
/**
@@ -24,6 +25,10 @@ export interface GeminiRunOptions {
* Supports both interactive mode (no prompt) and single-shot mode (`gemini -p`).
*/
export function runWithGemini(agentDir: string, manifest: AgentManifest, options: GeminiRunOptions = {}): void {
+ if (options.workspace) {
+ info('--workspace is not applied to Gemini because it reads GEMINI.md and .gemini/settings.json from the prepared temporary workspace.');
+ }
+
const exp = exportToGemini(agentDir);
// Create a temporary workspace
diff --git a/src/runners/git.ts b/src/runners/git.ts
index cc9f421..f0fafec 100644
--- a/src/runners/git.ts
+++ b/src/runners/git.ts
@@ -21,6 +21,7 @@ export interface GitRunOptions {
noCache?: boolean;
adapter?: string;
prompt?: string;
+ workspace?: string;
}
/**
@@ -85,33 +86,36 @@ export async function runWithGit(
label('Model', manifest.model.preferred);
}
label('Adapter', adapter);
+ if (options.workspace) {
+ label('Workspace', resolve(options.workspace));
+ }
divider();
try {
switch (adapter) {
case 'claude':
- runWithClaude(agentDir, manifest, { prompt: options.prompt });
+ runWithClaude(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'openai':
- runWithOpenAI(agentDir, manifest);
+ runWithOpenAI(agentDir, manifest, { workspace: options.workspace });
break;
case 'crewai':
- runWithCrewAI(agentDir, manifest);
+ runWithCrewAI(agentDir, manifest, { workspace: options.workspace });
break;
case 'openclaw':
- runWithOpenClaw(agentDir, manifest, { prompt: options.prompt });
+ runWithOpenClaw(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'nanobot':
- runWithNanobot(agentDir, manifest, { prompt: options.prompt });
+ runWithNanobot(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'opencode':
- runWithOpenCode(agentDir, manifest, { prompt: options.prompt });
+ runWithOpenCode(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'lyzr':
- await runWithLyzr(agentDir, manifest, { prompt: options.prompt });
+ await runWithLyzr(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'github':
- await runWithGitHub(agentDir, manifest, { prompt: options.prompt });
+ await runWithGitHub(agentDir, manifest, { prompt: options.prompt, workspace: options.workspace });
break;
case 'prompt':
console.log(exportToSystemPrompt(agentDir));
diff --git a/src/runners/gitclaw.ts b/src/runners/gitclaw.ts
index 3d2d7af..f50e067 100644
--- a/src/runners/gitclaw.ts
+++ b/src/runners/gitclaw.ts
@@ -9,6 +9,7 @@ import { error, info } from '../utils/format.js';
export interface GitclawRunOptions {
prompt?: string;
+ workspace?: string;
}
/**
@@ -23,6 +24,10 @@ export interface GitclawRunOptions {
* Supports both interactive mode (no prompt) and single-shot mode (`gitclaw run -p`).
*/
export function runWithGitclaw(agentDir: string, manifest: AgentManifest, options: GitclawRunOptions = {}): void {
+ if (options.workspace) {
+ info('--workspace is not applied to gitclaw because it reads agent.yaml and related files from the prepared temporary workspace.');
+ }
+
const exp = exportToGitclaw(agentDir);
// Create a temporary workspace
diff --git a/src/runners/github.ts b/src/runners/github.ts
index 4ee9334..44a5037 100644
--- a/src/runners/github.ts
+++ b/src/runners/github.ts
@@ -13,6 +13,7 @@ const DEFAULT_MODEL = 'openai/gpt-4.1';
export interface GitHubRunOptions {
prompt?: string;
token?: string;
+ workspace?: string;
}
/**
diff --git a/src/runners/lyzr.ts b/src/runners/lyzr.ts
index 390f25d..b81ca89 100644
--- a/src/runners/lyzr.ts
+++ b/src/runners/lyzr.ts
@@ -12,6 +12,7 @@ export interface LyzrRunOptions {
prompt?: string;
apiKey?: string;
userId?: string;
+ workspace?: string;
}
/**
diff --git a/src/runners/nanobot.ts b/src/runners/nanobot.ts
index 713737f..99b4163 100644
--- a/src/runners/nanobot.ts
+++ b/src/runners/nanobot.ts
@@ -1,5 +1,5 @@
import { writeFileSync, mkdirSync, rmSync } from 'node:fs';
-import { join } from 'node:path';
+import { join, resolve } from 'node:path';
import { tmpdir, homedir } from 'node:os';
import { spawnSync } from 'node:child_process';
import { randomBytes } from 'node:crypto';
@@ -10,6 +10,7 @@ import { ensureNanobotAuth } from '../utils/auth-provision.js';
export interface NanobotRunOptions {
prompt?: string;
+ workspace?: string;
}
export function runWithNanobot(agentDir: string, manifest: AgentManifest, options: NanobotRunOptions = {}): void {
@@ -38,7 +39,10 @@ export function runWithNanobot(agentDir: string, manifest: AgentManifest, option
args.push('--message', options.prompt);
}
+ const runCwd = resolve(options.workspace ?? agentDir);
+
info(`Launching Nanobot agent "${manifest.name}"...`);
+ info(`Working directory: ${runCwd}`);
if (!options.prompt) {
info('Starting interactive mode. Type your messages to chat.');
}
@@ -46,7 +50,7 @@ export function runWithNanobot(agentDir: string, manifest: AgentManifest, option
try {
const result = spawnSync('nanobot', args, {
stdio: 'inherit',
- cwd: agentDir,
+ cwd: runCwd,
env: {
...process.env,
NANOBOT_CONFIG: configFile,
diff --git a/src/runners/openai.ts b/src/runners/openai.ts
index 7f289e7..b9067da 100644
--- a/src/runners/openai.ts
+++ b/src/runners/openai.ts
@@ -1,5 +1,5 @@
import { writeFileSync, unlinkSync } from 'node:fs';
-import { join } from 'node:path';
+import { join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { spawnSync } from 'node:child_process';
import { randomBytes } from 'node:crypto';
@@ -8,7 +8,11 @@ import { AgentManifest } from '../utils/loader.js';
import { error, info } from '../utils/format.js';
import { resolveOpenAIKey } from '../utils/auth-provision.js';
-export function runWithOpenAI(agentDir: string, _manifest: AgentManifest): void {
+export interface OpenAIRunOptions {
+ workspace?: string;
+}
+
+export function runWithOpenAI(agentDir: string, _manifest: AgentManifest, options: OpenAIRunOptions = {}): void {
if (!resolveOpenAIKey()) {
error('OPENAI_API_KEY environment variable is not set');
info('Set it with: export OPENAI_API_KEY="sk-..."');
@@ -20,12 +24,15 @@ export function runWithOpenAI(agentDir: string, _manifest: AgentManifest): void
writeFileSync(tmpFile, script, 'utf-8');
+ const runCwd = resolve(options.workspace ?? agentDir);
+
info(`Running OpenAI agent from "${agentDir}"...`);
+ info(`Working directory: ${runCwd}`);
try {
const result = spawnSync('python3', [tmpFile], {
stdio: 'inherit',
- cwd: agentDir,
+ cwd: runCwd,
env: { ...process.env },
});
diff --git a/src/runners/openclaw.ts b/src/runners/openclaw.ts
index b38e180..76eb662 100644
--- a/src/runners/openclaw.ts
+++ b/src/runners/openclaw.ts
@@ -1,5 +1,5 @@
import { writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
-import { join } from 'node:path';
+import { join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
import { spawnSync } from 'node:child_process';
import { randomBytes } from 'node:crypto';
@@ -10,6 +10,7 @@ import { ensureOpenClawAuth } from '../utils/auth-provision.js';
export interface OpenClawRunOptions {
prompt?: string;
+ workspace?: string;
}
export function runWithOpenClaw(agentDir: string, manifest: AgentManifest, options: OpenClawRunOptions = {}): void {
@@ -58,6 +59,8 @@ export function runWithOpenClaw(agentDir: string, manifest: AgentManifest, optio
info(` Sub-agent workspace: workspace-${sub.name}/`);
}
+ const runCwd = resolve(options.workspace ?? workspaceDir);
+
// Write openclaw.json config, pointing workspaces to temp dirs
const config = exp.config as Record>;
if (hasSubAgents) {
@@ -72,7 +75,7 @@ export function runWithOpenClaw(agentDir: string, manifest: AgentManifest, optio
}
} else {
config.agent = config.agent ?? {};
- config.agent.workspace = workspaceDir;
+ config.agent.workspace = runCwd;
}
const configFile = join(workspaceDir, 'openclaw.json');
@@ -104,11 +107,12 @@ export function runWithOpenClaw(agentDir: string, manifest: AgentManifest, optio
}
info(`Launching OpenClaw agent "${manifest.name}"...`);
+ info(`Working directory: ${runCwd}`);
try {
const result = spawnSync('openclaw', args, {
stdio: 'inherit',
- cwd: workspaceDir,
+ cwd: runCwd,
env: {
...process.env,
OPENCLAW_CONFIG: configFile,
diff --git a/src/runners/opencode.ts b/src/runners/opencode.ts
index 87a7d53..55f9956 100644
--- a/src/runners/opencode.ts
+++ b/src/runners/opencode.ts
@@ -9,6 +9,7 @@ import { error, info } from '../utils/format.js';
export interface OpenCodeRunOptions {
prompt?: string;
+ workspace?: string;
}
/**
@@ -24,6 +25,10 @@ export interface OpenCodeRunOptions {
* Supports both interactive mode (no prompt) and single-shot mode (`opencode run ""`).
*/
export function runWithOpenCode(agentDir: string, manifest: AgentManifest, options: OpenCodeRunOptions = {}): void {
+ if (options.workspace) {
+ info('--workspace is not applied to OpenCode because it reads AGENTS.md and opencode.json from the prepared temporary workspace.');
+ }
+
const exp = exportToOpenCode(agentDir);
// Create a temporary workspace