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
39 changes: 23 additions & 16 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# agentworkforce CLI

A thin command-line front end for the workload-router. Spawns the harness CLI
(`claude`, `codex`, `opencode`, `grok`) configured by a selected **persona** from the
(`claude`, `codex`, `opencode`, `grok`, `cursor`) configured by a selected **persona** from the
project-local layer, configured source directories, or the small internal
built-in system catalog.

Expand Down Expand Up @@ -41,7 +41,7 @@ agentworkforce --version
selector accepts agent id, compact agent id, deployed name, persona slug, or
persona id, and posts to the same cloud trigger endpoint used by the dashboard.
- `sources` — list, add, or remove persona source directories.
- `harness check` — probe which harnesses (`claude`, `codex`, `opencode`, `grok`)
- `harness check` — probe which harnesses (`claude`, `codex`, `opencode`, `grok`, `cursor`)
are installed. See [`## Harness check`](#harness-check) below.
- `destroy` — tear down a deployed cloud agent: cancels all relaycron
schedules and marks the agent as destroyed. Accepts either a persona
Expand Down Expand Up @@ -338,7 +338,7 @@ combination; by default only the **recommended tier per intent** is shown
| `--all` | off | Show every tier of every persona. Alias: `--no-recommended`. |
| `--recommended` | on | Only show the recommended tier per intent. Implicit default; mostly useful for undoing `--all` earlier in a wrapper script. |
| `--filter-rating <tier>` | — | Restrict to a single tier (`best` \| `best-value` \| `minimum`). **Implicitly turns off the recommended-only default**, so filtering by `best` shows every persona's `best` row even when that's not the recommended tier. |
| `--filter-harness <harness>` | — | Restrict to a single harness (`claude` \| `codex` \| `opencode` \| `grok`). Composable with `--filter-rating` and `--all`. |
| `--filter-harness <harness>` | — | Restrict to a single harness (`claude` \| `codex` \| `opencode` \| `grok` \| `cursor`). Composable with `--filter-rating` and `--all`. |
| `--no-display-description` | off | Hide the `DESCRIPTION` column. `--display-description` re-enables it. |
| `--json` | off | Emit `{ "personas": [...] }` with one object per row. Same field set as the table, useful for scripting. |
| `-h`, `--help` | — | Print a one-line usage string and exit. |
Expand Down Expand Up @@ -430,7 +430,7 @@ agentworkforce harness check
```

Probes your PATH for each supported harness binary (`claude`, `codex`,
`opencode`, `grok`) and prints a table with status (`ok` / `missing`), resolved
`opencode`, `grok`, `cursor-agent`) and prints a table with status (`ok` / `missing`), resolved
version, and the resolved path (or the error, for missing ones). Exit
code is always `0` — this command is diagnostic, not a gate.

Expand Down Expand Up @@ -785,7 +785,7 @@ persona JSON remains commit-safe as long as you only use references.

## Relayfile mount rules

Interactive harness sessions (`claude`, `opencode`, `grok`, `codex`) run inside
Interactive harness sessions (`claude`, `opencode`, `grok`, `codex`, `cursor`) run inside
a Relayfile mount by default. File visibility and writability are controlled by
the persona's `mount` block plus project-level dotfiles:

Expand Down Expand Up @@ -832,9 +832,10 @@ mount rules (`.agentignore` / `.agentreadonly`) for that.
from that server), `mcp__<server>__<tool>` (specific tool).
- **Harness support today:** `claude` is wired for allow/deny/mode flags
(`--allowedTools`, `--disallowedTools`, `--permission-mode`). `grok` maps
`mode: "bypassPermissions"` to `--always-approve`; other Grok permission
fields warn and are ignored. `codex` and `opencode` emit a warning and fall
back to their defaults when `permissions` is set.
`mode: "bypassPermissions"` to `--always-approve`; `cursor` maps it to
`--force`. Other Grok/Cursor permission fields warn and are ignored.
`codex` and `opencode` emit a warning and fall back to their defaults when
`permissions` is set.
- **Cascade merge:** `allow` and `deny` are unions across layers (deduped on
merge); `mode` is replaced by the topmost layer that sets it. So the
library can declare the minimum-viable allow list, a user or configured
Expand Down Expand Up @@ -920,6 +921,7 @@ verbatim. Three transport types:
| codex | yes (via `--config mcp_servers.<name>...`) | not yet — SDK workflow path doesn't thread MCP |
| opencode | not yet — warns and proceeds without MCP | not yet |
| grok | not yet — warns and proceeds without MCP | not yet |
| cursor | not yet — warns and proceeds without MCP | not yet |

For a persona that needs MCP today, pick `claude` or `codex` as the harness
for that tier.
Expand All @@ -946,10 +948,10 @@ By default, interactive harness sessions run inside a sandbox mount — see

1. Resolves the persona, walks the cascade, resolves `$VAR` refs.
2. **Stages skills outside the repo by default** (claude interactive only —
see **Skill staging** below). For codex / opencode / grok, or when
see **Skill staging** below). For codex / opencode / grok / cursor, or when
`--install-in-repo` is passed, falls back to the legacy repo-relative
install path (`.claude/skills/`, `.agents/skills/`, `.skills/`,
`.grok/skills/`).
`.grok/skills/`, `.cursor/rules/`).
3. Runs skill install (`prpm install …`) if the persona declares any skills,
using the computed target (stage dir or repo).
4. Execs the harness binary with stdio inherited:
Expand All @@ -966,6 +968,8 @@ By default, interactive harness sessions run inside a sandbox mount — see
`opencode.json` in the sandbox carrying the persona model and prompt.
- `grok`: `grok --no-auto-update --model <model>`. In one-shot paths, the
CLI uses Grok Build's `--single` mode and passes cwd/output flags.
- `cursor`: `cursor-agent --model <model>`. In one-shot paths, the CLI
uses Cursor Agent's `--print --output-format text` mode.
5. Runs the skill cleanup command on exit, regardless of exit status. In
stage-dir mode this is a single `rm -rf <stage-dir>`.
6. Records launch metadata for the session and refreshes harness session logs
Expand Down Expand Up @@ -1052,7 +1056,7 @@ stage dir conflicts with something else (network filesystem, read-only

**Caveats for V1:**

- **Claude harness only.** codex, opencode, and grok continue to install into their
- **Claude harness only.** codex, opencode, grok, and cursor continue to install into their
conventional repo-relative directories. The SDK throws if `installRoot` is
passed with a non-claude harness.
- **No cache layer yet.** Every interactive session runs a fresh prpm install
Expand Down Expand Up @@ -1096,10 +1100,13 @@ back to the repo):

| Pattern | Rationale |
| --- | --- |
| `.agents`, `.claude/skills`, `.factory/skills`, `.grok/skills`, `.kiro/skills`, `skills` | skill.sh universal install root + per-harness symlink farms |
| `.agents`, `.claude/skills`, `.cursor/rules`, `.factory/skills`, `.grok/skills`, `.kiro/skills`, `skills` | skill.sh universal install root + per-harness symlink farms |
| `.opencode`, `.skills` | prpm `--as <harness>` output roots |
| `prpm.lock`, `skills-lock.json` | provider lockfiles |

For `cursor`, the mount also hides root `CLAUDE.md` / `CLAUDE.local.md`
because Cursor Agent reads root Claude guidance alongside `AGENTS.md`.

**What's preserved:**

- **User-level context** under `~/.claude/` — `CLAUDE.md`, skills, etc.
Expand All @@ -1108,7 +1115,7 @@ back to the repo):
- **Persona skills.** For claude, the `--plugin-dir` passed to the harness
resolves to an absolute path *outside* the mount, so staged skills from
`~/.agentworkforce/workforce/sessions/<id>/claude/plugin/` load normally. For
codex, opencode, and grok, the install runs inside the mount so the writes
codex, opencode, grok, and cursor, the install runs inside the mount so the writes
land in the sandbox.
- **Keychain auth.** The mount does not pass `--bare`; it only hides
files. Claude Code's macOS keychain login stays active.
Expand Down Expand Up @@ -1161,7 +1168,7 @@ stage dir is cleaned up by the existing `rm -rf` cleanup command.
A persona's three tiers can use different harnesses.

If a persona uses MCP, use `claude` or `codex` tiers.
`opencode` still does not inject persona `mcpServers` at spawn time.
`opencode`, `grok`, and `cursor` still do not inject persona `mcpServers` at spawn time.

## Troubleshooting

Expand All @@ -1175,9 +1182,9 @@ If a persona uses MCP, use `claude` or `codex` tiers.
auth interactively (e.g. Claude Code's MCP OAuth flow).

- **`Failed to spawn "claude": binary not found on PATH.`** — Install the
harness CLI (`claude`, `codex`, `opencode`, or `grok`) and ensure it's on your PATH.
harness CLI (`claude`, `codex`, `opencode`, `grok`, or `cursor-agent`) and ensure it's on your PATH.

- **`warning: persona declares mcpServers but the opencode harness is not yet
- **`warning: persona declares mcpServers but the <harness> harness is not yet
wired …`** — Switch that tier's `harness` to `claude` or `codex`, or drop the
MCP requirement.

Expand Down
72 changes: 52 additions & 20 deletions packages/cli/src/cli-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,8 @@ Commands:
install skills into the repo's
harness-conventional directory
(.claude/skills, .opencode/skills,
.agents/skills, .grok/skills, etc.).
.agents/skills, .grok/skills,
.cursor/rules, etc.).
By default, interactive harness
sessions run inside a
@relayfile/local-mount sandbox so
Expand Down Expand Up @@ -1141,6 +1142,7 @@ export const SKILL_INSTALL_IGNORED_PATTERNS = [
// skill.sh universal install root + per-harness symlink farms
'.agents',
'.claude/skills',
'.cursor/rules',
'.factory/skills',
'.grok/skills',
'.kiro/skills',
Expand All @@ -1157,6 +1159,14 @@ export const SKILL_INSTALL_IGNORED_PATTERNS = [
'AGENTS.md'
] as const;

const CURSOR_IGNORED_PATTERNS = [
...SKILL_INSTALL_IGNORED_PATTERNS,
// Cursor CLI reads root CLAUDE.md alongside AGENTS.md; hide repo-level
// Claude guidance so persona AGENTS.md remains the only root memory file.
'CLAUDE.md',
'CLAUDE.local.md'
] as const;

export interface RelayfileMountPatterns {
ignoredPatterns: string[];
readonlyPatterns: string[];
Expand All @@ -1175,7 +1185,9 @@ export function buildRelayfileMountPatterns(input: {
const builtInIgnored =
input.harness === 'claude'
? CLEAN_IGNORED_PATTERNS
: SKILL_INSTALL_IGNORED_PATTERNS;
: input.harness === 'cursor'
? CURSOR_IGNORED_PATTERNS
: SKILL_INSTALL_IGNORED_PATTERNS;

return {
ignoredPatterns: [
Expand Down Expand Up @@ -1275,7 +1287,7 @@ export function configureGitForMount(mountDir: string, patterns: readonly string
* harness has no mount (`--install-in-repo`).
*/
export interface ResolvedSidecar {
/** Filename inside the mount: `CLAUDE.md` (claude) or `AGENTS.md` (opencode/codex/grok). */
/** Filename inside the mount: `CLAUDE.md` (claude) or `AGENTS.md` (opencode/codex/grok/cursor). */
mountFile: 'CLAUDE.md' | 'AGENTS.md';
/** Persona-author content. Already inlined for built-ins; read from disk for local. */
personaContent: string;
Expand All @@ -1298,7 +1310,8 @@ export function loadSidecarForSelection(
harness !== 'claude' &&
harness !== 'opencode' &&
harness !== 'codex' &&
harness !== 'grok'
harness !== 'grok' &&
harness !== 'cursor'
) {
return {};
}
Expand Down Expand Up @@ -1328,7 +1341,7 @@ export function loadSidecarForSelection(
}
return {};
}
// opencode, codex, and grok all read AGENTS.md from cwd. The resolution
// opencode, codex, grok, and cursor all read AGENTS.md from cwd. The resolution
// rule is identical for these harnesses here.
if (selection.agentsMdContent) {
return {
Expand Down Expand Up @@ -1390,7 +1403,7 @@ export function buildSidecarBody(
*
* All interactive harnesses default to the mount.
* The mount hides CLAUDE.md / .claude / .mcp.json (claude) or the
* skill-install patterns + AGENTS.md (codex / opencode / grok) so
* skill-install patterns + AGENTS.md (codex / opencode / grok / cursor) so
* persona-supplied sidecars and any per-session writes stay sandboxed and
* don't leak into the user's real repo. `--install-in-repo` is the single
* opt-out that disengages the mount across all harnesses.
Expand All @@ -1401,7 +1414,13 @@ export function decideCleanMode(
harness: Harness,
installInRepo = false
): { useClean: boolean } {
if (harness === 'claude' || harness === 'opencode' || harness === 'codex' || harness === 'grok') {
if (
harness === 'claude' ||
harness === 'opencode' ||
harness === 'codex' ||
harness === 'grok' ||
harness === 'cursor'
) {
return { useClean: !installInRepo };
}
return { useClean: false };
Expand Down Expand Up @@ -2211,24 +2230,37 @@ async function runInteractive(
// - Mount path (default): write each configFile into the
// mount dir via onBeforeLaunch, so it lives only in the sandbox and is
// torn down with the session.
// - Non-mount path: today the only configFile producer is opencode
// (opencode.json for the --agent wiring), and the non-mount opencode
// path only engages under --install-in-repo. Writing opencode.json
// into the user's real repo would pollute the working tree, so we
// degrade: drop --agent from the argv, warn, and launch opencode with
// its default agent. The persona's prompt will not be applied in that
// mode; users who want it should drop --install-in-repo (the mount
// default handles this cleanly).
// - Non-mount path: opencode materializes opencode.json for its --agent
// wiring; cursor materializes AGENTS.md for the persona prompt. Writing
// either into the user's real repo would pollute the working tree, so we
// degrade under --install-in-repo: launch opencode without --agent and
// pass Cursor's system prompt as its initial prompt. Users who want the
// normal config-file behavior should drop --install-in-repo.
const hasConfigFiles = spec.configFiles.length > 0;
const degradeConfigFiles = hasConfigFiles && !useClean;
let effectiveArgs: readonly string[] = spawnArgs;
let effectiveInitialPrompt = spec.initialPrompt;
if (degradeConfigFiles) {
process.stderr.write(
'warning: --install-in-repo cannot safely materialize the persona agent config (would write opencode.json into your repo); launching without --agent. Drop --install-in-repo to apply the persona prompt.\n'
);
effectiveArgs = stripAgentFlag(spawnArgs);
if (harness === 'opencode') {
process.stderr.write(
'warning: --install-in-repo cannot safely materialize the persona agent config (would write opencode.json into your repo); launching without --agent. Drop --install-in-repo to apply the persona prompt.\n'
);
effectiveArgs = stripAgentFlag(spawnArgs);
} else if (harness === 'cursor') {
process.stderr.write(
'warning: --install-in-repo cannot safely materialize Cursor AGENTS.md (would write AGENTS.md into your repo); launching with the persona system prompt as Cursor\'s initial prompt. Drop --install-in-repo to apply the persona prompt via AGENTS.md.\n'
);
effectiveInitialPrompt = systemPrompt || null;
} else {
const files = spec.configFiles.map((file) => file.path).join(', ');
process.stderr.write(
`warning: --install-in-repo cannot safely materialize persona config file(s) ${files} into your repo; launching without those config files. Drop --install-in-repo to apply them.\n`
);
}
}
const finalArgs = spec.initialPrompt ? [...effectiveArgs, spec.initialPrompt] : [...effectiveArgs];
const finalArgs = effectiveInitialPrompt
? [...effectiveArgs, effectiveInitialPrompt]
: [...effectiveArgs];

// Print a sanitized summary rather than raw argv: spec.args for the claude
// harness contains the resolved --mcp-config JSON and the full system
Expand Down
Loading
Loading