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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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 DownExpand 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