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
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ The repository owns the workflow. A committed project contract defines setup com
- Repository-owned setup, check, Git, and deployment policy
- Isolated local ports, process metadata, logs, and clean shutdown
- Optional Vercel Development environment injection without persisted secret values
- Agent-aware launch context for Codex, Claude Code, and OpenClaw
- Portable instructions for app-based, remote, and unsupported agent clients
- Machine-readable project and workspace state

## Install from source
Expand All @@ -41,10 +43,23 @@ pagent doctor
pagent context
pagent enter dashboard-feature --agent openclaw
pagent enter dashboard-polish --agent codex
pagent launch dashboard-polish
pagent status
```

`pagent enter` prints the isolated worktree path. Start the selected coding agent from that directory.
`pagent launch` starts the selected coding agent in the isolated worktree and supplies the committed project workflow as its initial context. It does not create, replace, or modify `AGENTS.md`, `CLAUDE.md`, or other repository instruction files.

Include the first task when launching an agent:

```bash
pagent launch dashboard-polish --prompt "Polish the dashboard navigation"
```

For ChatGPT, a remote OpenClaw agent, or another client that Parallel Agent CLI cannot launch directly, print a portable handoff and paste it into that client:

```bash
pagent instructions dashboard-polish --agent chatgpt
```

To register a worktree that already exists:

Expand Down Expand Up @@ -109,6 +124,8 @@ pagent stop dashboard-feature

Parallel Agent CLI allocates a local port, records the process and log path, and checks the configured health URL. Running `pagent dev` from inside a registered worktree infers its workspace name.

Agents started through `pagent launch` are explicitly instructed to use `pagent bootstrap`, `pagent dev`, `pagent logs`, and `pagent stop` rather than bypassing the managed runtime lifecycle. Codex and Claude Code run with the worktree as their current project. OpenClaw starts in local TUI mode with the worktree supplied through `OPENCLAW_WORKSPACE_DIR` and in the handoff instructions. An explicit OpenClaw workspace in the user's configuration can take precedence over that environment variable; use `pagent instructions` with an already configured OpenClaw agent in that case.

For a Vercel-backed development environment:

```yaml
Expand Down Expand Up @@ -137,6 +154,8 @@ pagent doctor
pagent bootstrap [--apply]
pagent enter <name> --agent <agent> [--base <git-ref>]
pagent adopt <path> --agent <agent> [--name <name>] [--base <git-ref>]
pagent launch [name] [--agent <agent>] [--prompt <task>] [--dry-run]
pagent instructions [name] [--agent <agent>] [--prompt <task>]
pagent remove <name>
pagent dev [name] [--port <port>] [--no-wait]
pagent logs <name> [--lines <count>]
Expand Down
23 changes: 23 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { doctorCommand } from "./commands/doctor.js";
import { devCommand } from "./commands/dev.js";
import { enterCommand } from "./commands/enter.js";
import { initCommand } from "./commands/init.js";
import { instructionsCommand, launchCommand } from "./commands/launch.js";
import { logsCommand } from "./commands/logs.js";
import { removeCommand } from "./commands/remove.js";
import { statusCommand } from "./commands/status.js";
Expand Down Expand Up @@ -53,6 +54,28 @@ program
devCommand(process.cwd(), name, options),
);

program
.command("launch")
.argument("[name]", "workspace name; inferred inside a registered workspace")
.description("Launch a coding agent with the workspace's operational context")
.option("--agent <agent>", "override the workspace's recorded agent launcher")
.option("--prompt <prompt>", "include an initial user task")
.option("--dry-run", "print the launch context without starting the agent")
.action(
(name: string | undefined, options: { agent?: string; prompt?: string; dryRun?: boolean }) =>
launchCommand(process.cwd(), name, options),
);

program
.command("instructions")
.argument("[name]", "workspace name; inferred inside a registered workspace")
.description("Print portable workspace instructions for any coding agent")
.option("--agent <agent>", "name the receiving agent in the instructions")
.option("--prompt <prompt>", "include an initial user task")
.action((name: string | undefined, options: { agent?: string; prompt?: string }) =>
instructionsCommand(process.cwd(), name, options),
);

program
.command("stop")
.argument("<name>", "workspace name")
Expand Down
11 changes: 10 additions & 1 deletion src/commands/enter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,14 @@ export async function enterCommand(
);
}
console.log(`Worktree: ${worktree}`);
console.log(`\nNext: cd ${JSON.stringify(worktree)} && ${agent}`);
console.log("\nNext:");
console.log(` pagent launch ${name}`);
console.log(
"\nThe launched agent is instructed to use pagent bootstrap for dependencies and pagent dev for the development server.",
);
console.log(`\nFor an app, remote agent, or unsupported client:`);
console.log(` pagent instructions ${name}`);
console.log(
"\nUse pagent dev instead of starting the development command directly so ports, environment variables, logs, and cleanup remain managed.",
);
}
217 changes: 217 additions & 0 deletions src/commands/launch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { access } from "node:fs/promises";
import { loadConfig, type ProjectConfig } from "../config.js";
import { CliError } from "../errors.js";
import { gitRoot } from "../git.js";
import { slugify } from "../format.js";
import { commandExists, runInteractiveCommand } from "../process.js";
import { readState, type WorkspaceRecord } from "../state.js";

type SupportedAgent = "codex" | "claude" | "openclaw";

export interface LaunchOptions {
agent?: string;
prompt?: string;
dryRun?: boolean;
}

export interface AgentLaunchSpec {
agent: SupportedAgent;
command: string;
args: string[];
environment: NodeJS.ProcessEnv;
}

const agentAliases: Record<string, SupportedAgent> = {
codex: "codex",
"codex-cli": "codex",
claude: "claude",
"claude-code": "claude",
openclaw: "openclaw",
};

export async function launchCommand(
cwd: string,
requestedName: string | undefined,
options: LaunchOptions,
): Promise<void> {
const resolved = await resolveWorkspace(cwd, requestedName);
const agent = normalizeAgent(options.agent ?? resolved.workspace.agent);
const instructions = buildAgentInstructions(
resolved.config,
resolved.workspace,
agent,
options.prompt,
);
const specification = buildLaunchSpec(agent, resolved.workspace, instructions, process.env);

if (options.dryRun) {
printLaunchPreview(resolved.workspace, specification, instructions);
return;
}

if (!(await commandExists(specification.command))) {
throw new CliError(
`Agent executable is not installed or not on PATH: ${specification.command}. ` +
`Run "pagent instructions ${resolved.workspace.name}" to hand the context to another client.`,
);
}

console.log(`Launching ${agent} in workspace ${resolved.workspace.name}`);
console.log(`Worktree: ${resolved.workspace.worktree}`);
console.log(
"Parallel Agent will provide the repository workflow as the agent's initial instructions.\n",
);
await runInteractiveCommand(
specification.command,
specification.args,
resolved.workspace.worktree,
specification.environment,
);
}

export async function instructionsCommand(
cwd: string,
requestedName: string | undefined,
options: Pick<LaunchOptions, "agent" | "prompt">,
): Promise<void> {
const resolved = await resolveWorkspace(cwd, requestedName);
const agent = options.agent?.trim()
? options.agent.trim()
: (agentAliases[slugify(resolved.workspace.agent)] ?? resolved.workspace.agent);
console.log(buildAgentInstructions(resolved.config, resolved.workspace, agent, options.prompt));
}

export function buildLaunchSpec(
agent: SupportedAgent,
workspace: WorkspaceRecord,
instructions: string,
baseEnvironment: NodeJS.ProcessEnv,
): AgentLaunchSpec {
const environment: NodeJS.ProcessEnv = {
...baseEnvironment,
PARALLEL_AGENT_WORKSPACE: workspace.name,
PARALLEL_AGENT_WORKTREE: workspace.worktree,
};

switch (agent) {
case "codex":
return {
agent,
command: "codex",
args: ["--cd", workspace.worktree, instructions],
environment,
};
case "claude":
return {
agent,
command: "claude",
args: [instructions],
environment,
};
case "openclaw":
return {
agent,
command: "openclaw",
args: [
"tui",
"--local",
"--session",
`pagent-${workspace.name}`,
"--message",
instructions,
],
environment: {
...environment,
OPENCLAW_WORKSPACE_DIR: workspace.worktree,
},
};
}
}

export function buildAgentInstructions(
config: ProjectConfig,
workspace: WorkspaceRecord,
agent: string,
prompt?: string,
): string {
const install = config.runtime.install ?? "not configured";
const development = config.runtime.development ?? "not configured";
const checks = config.checks.length > 0 ? config.checks.join(", ") : "none configured";
const task = prompt?.trim();

return `Parallel Agent workspace instructions

You are the ${agent} agent assigned to the isolated workspace "${workspace.name}" for project "${config.project.name}".

Workspace: ${workspace.worktree}
Branch: ${workspace.branch}
Recorded base: ${workspace.baseRef} (${workspace.baseSha})
Install command in project contract: ${install}
Development command in project contract: ${development}
Required checks: ${checks}

Operating rules:
1. Work only in the workspace path above. Do not edit another checkout of this repository.
2. Run "pagent context" before making changes so you have the current repository workflow and safety policy.
3. When dependencies are missing, run "pagent bootstrap" first and then "pagent bootstrap --apply" if installation is needed. Do not bypass the configured bootstrap command with a package-manager install.
4. Start the project with "pagent dev". Do not run the development command directly; Pagent must allocate the port, inject the configured environment, and track the process.
5. Inspect the server with "pagent logs ${workspace.name}" and stop it with "pagent stop ${workspace.name}".
6. Repository instructions such as AGENTS.md or CLAUDE.md still apply to implementation work. The committed .parallel-agent/project.yaml contract is authoritative for workspace, runtime, Git, and deployment operations.
7. Direct pushes to ${config.repository.default_branch} are ${config.git.direct_push_to_main ? "allowed by project policy" : "forbidden"}. Force pushes are ${config.git.force_push ? "allowed by project policy" : "forbidden"}.
8. ${config.production.requires_approval ? "Do not deploy or promote to production without explicit user approval." : "Follow the project contract before any production operation."}

${task ? `User task:\n${task}` : "Load the project context, then ask the user what they want to work on."}`;
}

function normalizeAgent(rawAgent: string): SupportedAgent {
const normalized = agentAliases[slugify(rawAgent)];
if (!normalized) {
throw new CliError(
`No interactive launcher is configured for agent "${rawAgent}". ` +
"Supported launchers: codex, claude, openclaw. Use pagent instructions for other clients.",
);
}
return normalized;
}

async function resolveWorkspace(cwd: string, requestedName: string | undefined) {
const { root, config } = await loadConfig(cwd);
const state = await readState(root);
let currentRoot: string | undefined;
if (!requestedName) {
try {
currentRoot = await gitRoot(cwd);
} catch {
currentRoot = undefined;
}
}
const workspace = requestedName
? state.workspaces.find((candidate) => candidate.name === requestedName)
: state.workspaces.find((candidate) => candidate.worktree === currentRoot);
if (!workspace) {
throw new CliError(
requestedName
? `Unknown workspace: ${requestedName}`
: "Could not infer a workspace from this directory. Pass a workspace name.",
);
}
try {
await access(workspace.worktree);
} catch {
throw new CliError(`Workspace worktree is missing: ${workspace.worktree}`);
}
return { config, workspace };
}

function printLaunchPreview(
workspace: WorkspaceRecord,
specification: AgentLaunchSpec,
instructions: string,
): void {
console.log(`Agent: ${specification.agent}`);
console.log(`Workspace: ${workspace.name}`);
console.log(`Worktree: ${workspace.worktree}`);
console.log(`Executable: ${specification.command}`);
console.log("\nInitial instructions:\n");
console.log(instructions);
}
32 changes: 32 additions & 0 deletions src/process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,35 @@ export async function runShellCommand(command: string, cwd: string): Promise<voi
});
});
}

export async function runInteractiveCommand(
command: string,
args: string[],
cwd: string,
environment: NodeJS.ProcessEnv = process.env,
): Promise<void> {
await new Promise<void>((resolve, reject) => {
const child = spawn(command, args, {
cwd,
env: environment,
stdio: "inherit",
});

child.on("error", (error: NodeJS.ErrnoException) => {
if (error.code === "ENOENT") {
reject(new CliError(`Agent executable is not installed or not on PATH: ${command}`));
return;
}
reject(error);
});
child.on("exit", (code, signal) => {
if (code === 0) {
resolve();
} else if (signal) {
reject(new CliError(`${command} exited after receiving ${signal}.`));
} else {
reject(new CliError(`${command} exited with code ${code ?? "unknown"}.`));
}
});
});
}
Loading