From 09b200426aea4567d1fcd70d2a3993da6387937d Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Sat, 18 Apr 2026 23:02:10 -0400 Subject: [PATCH 1/3] Add --clean and out-of-repo skill staging Introduce interactive Claude session staging and a clean sandbox mode. Features: - New CLI flags: --install-in-repo (legacy) and --clean (mount sandbox). They are mutually exclusive. - By default interactive claude sessions stage skills under ~/.agent-workforce/sessions//claude/plugin and pass --plugin-dir to Claude so the repo's .claude/ is not touched. --install-in-repo restores legacy in-repo installs. - --clean launches the harness inside a @relayfile/local-mount symlink mount that hides repo-level Claude config (CLAUDE.md, CLAUDE.local.md, .claude, .mcp.json) while preserving persona skills and user-level ~/.claude/. One-shot runs and non-claude harnesses print notes/warnings and ignore --clean. Code changes: - CLI: parseAgentArgs, AgentFlags, session path generation, install root plumbing, decideCleanMode, launchOnMount integration, and wiring of pluginDirs to interactive spec. Added runtime checks and messaging for one-shot and non-claude behavior. - workload-router: add SkillMaterializationOptions.installRoot, materializeSkills/materializeSkillsFor/useSelection/usePersona support for installRoot, scaffold generation, session-mode install/cleanup behavior, and related tests. - harness-kit: support pluginDirs (emit --plugin-dir for claude, warn for others). - Tests added/updated across packages (cli, harness-kit, workload-router) exercising flag parsing, clean behavior, pluginDirs, and materialization. - package.json: add @relayfile/local-mount dependency and update README docs describing skill staging and clean mode. This change isolates interactive Claude sessions from repo artifacts and provides an opt-out legacy path, while leaving one-shot and other harnesses unchanged for v1. --- README.md | 66 ++++++ packages/cli/README.md | 173 ++++++++++++++- packages/cli/package.json | 3 +- packages/cli/src/cli.test.ts | 235 ++++++++++++++++++++ packages/cli/src/cli.ts | 238 +++++++++++++++++++-- packages/harness-kit/src/harness.test.ts | 54 +++++ packages/harness-kit/src/harness.ts | 26 ++- packages/workload-router/src/index.test.ts | 92 +++++++- packages/workload-router/src/index.ts | 168 +++++++++++++-- 9 files changed, 1017 insertions(+), 38 deletions(-) create mode 100644 packages/cli/src/cli.test.ts diff --git a/README.md b/README.md index 82471239..163dba3f 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,72 @@ The full docs — cascade rules, `${VAR}` interpolation, MCP transport options, permission grammar, troubleshooting — live in **[packages/cli/README.md](./packages/cli/README.md)**. +### Skill staging (interactive claude only) + +Interactive `claude` sessions stage skills **outside the repo** by default. +The CLI materializes a Claude Code plugin under the user's home directory +and passes it via `--plugin-dir`, so the session sees exactly the persona's +declared skills — and nothing the repo happens to have in `.claude/skills/`. + +``` +~/.agent-workforce/ +└── sessions/--/ + └── claude/ + └── plugin/ ← passed as --plugin-dir + ├── .claude-plugin/plugin.json ← generated scaffold + ├── skills → .claude/skills ← relative symlink + └── .claude/skills//SKILL.md ← prpm install output +``` + +On exit the whole stage dir is removed with a single `rm -rf`. To fall back +to the legacy behavior and install into the repo's `.claude/skills/`, pass +`--install-in-repo`: + +```bash +agent-workforce agent --install-in-repo code-reviewer@best +``` + +V1 scope: claude interactive only. codex, opencode, and one-shot runs still +use the repo-relative install path. A content-addressed +`~/.agent-workforce/cache/` layer for reusing installs across sessions is +planned but not yet wired up. See +**[packages/cli/README.md#skill-staging](./packages/cli/README.md#skill-staging)** +for the full mechanics. + +### Clean mode (`--clean`) + +For interactive `claude` sessions, `--clean` launches the harness inside a +[`@relayfile/local-mount`](https://www.npmjs.com/package/@relayfile/local-mount) +sandbox that hides repo-level Claude Code configuration from the model: + +| Hidden in `--clean` mode | Still visible | +| --- | --- | +| `CLAUDE.md` (at any depth) | `~/.claude/CLAUDE.md` (user-level) | +| `CLAUDE.local.md` | `~/.claude/skills/` (user-level skills) | +| `.claude/` | persona's staged skills (via `--plugin-dir`) | +| `.mcp.json` | persona's own `mcpServers` block | +| | your keychain auth (unchanged) | + +```bash +agent-workforce agent --clean posthog@best +``` + +The repo tree is mirrored into +`~/.agent-workforce/sessions//mount/` via symlinks; claude sees the +mount as its cwd. Writes inside the mount sync back to the real repo on +exit. Ignore semantics follow gitignore — `.claude` hides nested variants +like `packages/foo/.claude/` too. + +**Scope:** interactive claude only. Pass it to codex/opencode or to a +one-shot run and the flag is ignored with a note. `--clean` and +`--install-in-repo` are mutually exclusive — they ask for opposite things. + +**Caveat:** user-level Claude Code config in `~/.claude/` still loads +inside the session. `--clean` hides the *repo's* context, not the user's. +If you need to hide user-level config too, launch under a scratch +`$HOME`. See **[packages/cli/README.md#clean-mode](./packages/cli/README.md#clean-mode)** +for the full mount layout and semantics. + ## Packages - `packages/workload-router` — TypeScript SDK for typed persona + routing profile resolution (harness-agnostic). diff --git a/packages/cli/README.md b/packages/cli/README.md index 93470707..6fb94efc 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -380,22 +380,34 @@ persona session, add it to the persona's `mcpServers` block. ### Interactive ```sh -agent-workforce agent [@] +agent-workforce agent [--install-in-repo] [--clean] [@] ``` +`--install-in-repo` and `--clean` are mutually exclusive — see +[**Clean mode**](#clean-mode) below. + 1. Resolves the persona, walks the cascade, resolves `$VAR` refs. -2. Runs skill install (`prpm install …`) if the persona declares any skills. -3. Execs the harness binary with stdio inherited: +2. **Stages skills outside the repo by default** (claude interactive only — + see **Skill staging** below). For codex / opencode, or when + `--install-in-repo` is passed, falls back to the legacy repo-relative + install path (`.claude/skills/`, `.agents/skills/`, `.skills/`). +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: - `claude`: `claude --model --append-system-prompt - --mcp-config '' --strict-mcp-config`. Both flags are always passed - so the session only sees the persona's declared MCP servers — see - **MCP isolation** above. + --mcp-config '' --strict-mcp-config [--plugin-dir ]`. The + `--plugin-dir` flag is appended when the session uses out-of-repo + staging so Claude Code loads exactly the staged skills — and nothing + the repo happens to carry. Both MCP flags are always passed so the + session only sees the persona's declared MCP servers (see **MCP + isolation** above). - `codex`: `codex -m ` with the system prompt as the initial positional `[PROMPT]`. (codex has no `--system-prompt` flag today.) - `opencode`: `opencode --model ` with the system prompt as the initial argument. -4. Runs the skill cleanup command on exit, regardless of exit status. -5. Propagates the harness's exit code. +5. Runs the skill cleanup command on exit, regardless of exit status. In + stage-dir mode this is a single `rm -rf `. +6. Propagates the harness's exit code. Signals (SIGINT, SIGTERM) are forwarded to the child. @@ -414,6 +426,151 @@ Env from the persona is passed through via `ExecuteOptions.env`. `mcpServers` is currently **ignored with a warning** in one-shot mode — the SDK workflow path doesn't thread MCP config yet. +One-shot mode still installs into the repo regardless of `--install-in-repo`; +out-of-repo staging only applies to the interactive path today because the +agent-relay workflow SDK doesn't yet thread `--plugin-dir` into the claude +agent adapter. `--install-in-repo` passed to a one-shot run prints a `note:` +and is a no-op. + +## Skill staging + +By default, interactive `claude` sessions stage skills under the user's home +directory instead of the current repo. Nothing gets written to `.claude/` in +the working tree, and the session only sees the skills the persona declares +— never whatever skills the repo happens to carry on disk. + +**Layout:** + +``` +~/.agent-workforce/ +└── sessions/--/ + └── claude/ + └── plugin/ ← passed as --plugin-dir + ├── .claude-plugin/plugin.json ← generated scaffold + ├── skills → .claude/skills ← relative symlink + └── .claude/skills//SKILL.md ← prpm install output +``` + +- **Stage dir** — `~/.agent-workforce/sessions//claude/plugin/`. + `` is `--` so parallel + sessions never collide. +- **Plugin wrapper** — the CLI writes a minimal `.claude-plugin/plugin.json` + (`{"name":"agent-workforce-session","version":"0.0.0",…}`) and a relative + symlink `skills → .claude/skills` so Claude Code's plugin layout + (`skills//SKILL.md`) resolves to prpm's actual output + (`.claude/skills//SKILL.md`) without moving any files. +- **prpm install** — still runs `npx -y prpm install --as claude`, + but inside `cd ` so the harness-conventional `.claude/skills/` + lands in the stage dir, not the repo. +- **Cleanup** — on exit, a single `rm -rf ` removes the whole + session directory. The provider lockfile (`prpm.lock`) is inside the stage + dir and goes with it — no repeat-run resolution cache today. Restart cost + is one prpm install per session. + +**Opt-out — `--install-in-repo`:** + +Pass `--install-in-repo` to fall back to the legacy behavior (skills land in +the repo's `.claude/skills/` directory, cleaned on exit): + +```sh +agent-workforce agent --install-in-repo code-reviewer@best +``` + +Useful when you want to inspect the installed skills on disk, or when the +stage dir conflicts with something else (network filesystem, read-only +`$HOME`, etc.). + +**Caveats for V1:** + +- **Claude harness only.** codex and opencode 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 + into a new stage dir. A `~/.agent-workforce/cache/` content-addressed cache + is planned but not wired up. +- **One-shot path unchanged.** See the paragraph under "One-shot" above. + +## Clean mode + +`--clean` launches an interactive claude session inside a +[`@relayfile/local-mount`](https://www.npmjs.com/package/@relayfile/local-mount) +symlink mount that hides the repo's Claude Code configuration from the +session — so the model sees persona context + user-level context, and +nothing the repo itself declares. + +```sh +agent-workforce agent --clean [@] +``` + +**What's hidden (gitignore semantics, at any depth):** + +| Pattern | Rationale | +| --- | --- | +| `CLAUDE.md` | Repo-level project memory | +| `CLAUDE.local.md` | Developer-local project memory | +| `.claude` | Repo Claude Code config dir (settings, agents, skills, commands) | +| `.mcp.json` | Repo-declared MCP servers | + +**What's preserved:** + +- **User-level context** under `~/.claude/` — `CLAUDE.md`, skills, etc. + still load. `--clean` scrubs the *project*, not the user. To exclude + user-level context too, launch under a scratch `$HOME`. +- **Persona skills.** The `--plugin-dir` passed to claude resolves to an + absolute path *outside* the mount, so staged skills from + `~/.agent-workforce/sessions//claude/plugin/` load normally. +- **Keychain auth.** `--clean` does NOT pass `--bare`; it only hides + files via the mount. Claude Code's macOS keychain login stays active. +- **Persona `mcpServers`.** Still passed via `--mcp-config` — unaffected + by the mount. The repo's `.mcp.json` is hidden regardless. + +### Session layout + +Both the skill install root and the clean mount live under a single +session directory. The session id (`--`) +is generated once and both paths are derived from it: + +``` +~/.agent-workforce/ +└── sessions/--/ + ├── claude/ + │ └── plugin/ ← passed as --plugin-dir + │ ├── .claude-plugin/plugin.json + │ ├── skills → .claude/skills + │ └── .claude/skills//SKILL.md + └── mount/ ← --clean: claude's cwd + └──