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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions docs.md
Original file line number Diff line number Diff line change
Expand Up @@ -629,12 +629,15 @@ gitagent run [options]
| `-d, --dir <dir>` | — | Local directory (alternative to `--repo`) |
| `-a, --adapter <name>` | `claude` | Adapter (see table below) |
| `-b, --branch <branch>` | `main` | Git branch or tag to clone |
| `-w, --workspace <dir>` | 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 <query>` | — | 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 |
Expand Down Expand Up @@ -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

Expand Down
26 changes: 16 additions & 10 deletions src/commands/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ interface RunOptions {
cache: boolean;
prompt?: string;
dir?: string;
workspace?: string;
}

export const runCommand = new Command('run')
Expand All @@ -35,6 +36,7 @@ export const runCommand = new Command('run')
.option('--no-cache', 'Clone to temp dir, delete on exit')
.option('-p, --prompt <query>', 'Initial prompt to send to the agent')
.option('-d, --dir <dir>', 'Use local directory instead of git URL')
.option('-w, --workspace <dir>', 'Working directory for the spawned agent process')
.action(async (options: RunOptions) => {
let agentDir: string;
let cleanup: (() => void) | undefined;
Expand Down Expand Up @@ -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) {
Expand All @@ -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':
Expand Down
8 changes: 6 additions & 2 deletions src/runners/claude.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand All @@ -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) {
Expand Down
13 changes: 10 additions & 3 deletions src/runners/crewai.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
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';
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 },
});

Expand Down
5 changes: 5 additions & 0 deletions src/runners/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { error, info } from '../utils/format.js';

export interface GeminiRunOptions {
prompt?: string;
workspace?: string;
}

/**
Expand All @@ -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
Expand Down
20 changes: 12 additions & 8 deletions src/runners/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface GitRunOptions {
noCache?: boolean;
adapter?: string;
prompt?: string;
workspace?: string;
}

/**
Expand Down Expand Up @@ -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));
Expand Down
5 changes: 5 additions & 0 deletions src/runners/gitclaw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { error, info } from '../utils/format.js';

export interface GitclawRunOptions {
prompt?: string;
workspace?: string;
}

/**
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/runners/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const DEFAULT_MODEL = 'openai/gpt-4.1';
export interface GitHubRunOptions {
prompt?: string;
token?: string;
workspace?: string;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/runners/lyzr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface LyzrRunOptions {
prompt?: string;
apiKey?: string;
userId?: string;
workspace?: string;
}

/**
Expand Down
8 changes: 6 additions & 2 deletions src/runners/nanobot.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -38,15 +39,18 @@ 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.');
}

try {
const result = spawnSync('nanobot', args, {
stdio: 'inherit',
cwd: agentDir,
cwd: runCwd,
env: {
...process.env,
NANOBOT_CONFIG: configFile,
Expand Down
13 changes: 10 additions & 3 deletions src/runners/openai.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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-..."');
Expand All @@ -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 },
});

Expand Down
Loading