Conversation
Introduce a new CLI (packages/workload-router/src/cli.ts) to run personas in interactive or one-shot modes and spawn external LLM harness CLIs. Add a package bin entry (agent-workforce -> dist/cli.js) and update the build script to chmod +x the generated CLI. Also update package-lock.json to reflect the new dependencies and dev-deps required by the CLI.
Introduce a new @agentworkforce/cli package with a command-line front end (agent-workforce). Adds package.json, README, and a TypeScript implementation (src/cli.ts) that resolves persona selectors across pwd/home/library layers, supports interactive and one-shot modes, installs skills, and spawns harness CLIs (claude/codex/opencode).
Also add env-ref utilities (src/env-refs.ts) with unit tests to resolve and leniently handle $VAR / ${VAR} references, and local persona loader tests (src/local-personas.test.ts) to validate cascade/extends behavior. Include a built-in persona (personas/posthog.json) and update the top-level README to document the CLI usage and local override semantics. Minor workload-router changes coordinate the new CLI package and generated personas.
There was a problem hiding this comment.
Pull request overview
Adds a new agent-workforce CLI for running personas directly (optionally from user-local overrides) and extends the workload-router persona model to support runtime env injection, MCP server config, and harness permission policies (with a new built-in posthog persona).
Changes:
- Introduce
@agentworkforce/cliwithagent-workforce agent <persona>[@<tier>] [task...], including local persona cascade + env-ref resolution and MCP/permissions wiring (interactive). - Extend
@agentworkforce/workload-routerpersona/spec/selection types to includeenv,mcpServers, andpermissions, adduseSelection(), and add aposthogpersona + routing updates. - Add tests for the CLI helpers (env refs + local persona cascading).
Reviewed changes
Copilot reviewed 14 out of 17 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds packages/cli workspace importer dependency on workload-router. |
| package-lock.json | Updates root lockfile for added dev dependency. |
| package.json | Adds agent-trajectories dev dependency used by the repo tooling. |
| personas/posthog.json | New built-in PostHog persona with MCP server + permissions. |
| packages/workload-router/src/index.ts | Adds env/MCP/permissions parsing & selection threading; introduces useSelection(); registers posthog. |
| packages/workload-router/src/index.test.ts | Updates routing profile test data to include posthog. |
| packages/workload-router/src/generated/personas.ts | Generated export for posthogAgent. |
| packages/workload-router/scripts/generate-personas.mjs | Wires posthog → posthogAgent mapping for generation. |
| packages/workload-router/routing-profiles/default.json | Adds default routing rule for posthog. |
| packages/cli/package.json | New publishable CLI package exposing agent-workforce bin. |
| packages/cli/tsconfig.json | TS build config for the CLI package. |
| packages/cli/src/cli.ts | Implements the CLI command: selection, cascade, env/MCP resolution, and harness spawning. |
| packages/cli/src/local-personas.ts | Implements pwd/home/library persona override cascade + merging rules. |
| packages/cli/src/local-personas.test.ts | Tests for cascade/merge behavior. |
| packages/cli/src/env-refs.ts | Implements $VAR / ${VAR} env reference resolution (strict + lenient). |
| packages/cli/src/env-refs.test.ts | Tests for env-ref resolution behavior. |
| packages/cli/README.md | CLI documentation and configuration guide. |
| README.md | Adds top-level CLI documentation and PostHog persona mention. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| The CLI ships as a `bin` in `@agentworkforce/workload-router`. From the repo | ||
| checkout: | ||
|
|
||
| ```sh | ||
| corepack pnpm -r build | ||
| corepack pnpm --filter @agentworkforce/workload-router link --global |
There was a problem hiding this comment.
This README says the CLI ships as a bin in @agentworkforce/workload-router and suggests linking that package globally, but this PR introduces @agentworkforce/cli as the package that defines the agent-workforce bin. Update the install instructions to reference @agentworkforce/cli (and its link/build commands) so users don’t install the wrong package.
| The CLI ships as a `bin` in `@agentworkforce/workload-router`. From the repo | |
| checkout: | |
| ```sh | |
| corepack pnpm -r build | |
| corepack pnpm --filter @agentworkforce/workload-router link --global | |
| The CLI ships as a `bin` in `@agentworkforce/cli`. From the repo checkout: | |
| ```sh | |
| corepack pnpm -r build | |
| corepack pnpm --filter @agentworkforce/cli link --global |
| "posthog": { | ||
| "type": "http", | ||
| "url": "https://mcp.posthog.com/mcp", | ||
| "headers": { "Authorization": "Bearer $POSTHOG_API_KEY" } |
There was a problem hiding this comment.
In this persona JSON example, "Authorization": "Bearer $POSTHOG_API_KEY" will not be interpolated by the documented resolver (only whole-string $VAR or braced ${VAR} inside longer strings). This example should use "Bearer ${POSTHOG_API_KEY}" (or adjust the docs/resolver) to avoid users copying a config that won’t authenticate.
| "headers": { "Authorization": "Bearer $POSTHOG_API_KEY" } | |
| "headers": { "Authorization": "Bearer ${POSTHOG_API_KEY}" } |
| ```json | ||
| { | ||
| "id": "my-posthog", | ||
| "extends": "posthog", | ||
| "env": { "POSTHOG_API_KEY": "$POSTHOG_API_KEY" }, | ||
| "permissions": { | ||
| "allow": ["mcp__posthog__insights-list", "mcp__posthog__events-query"] | ||
| } | ||
| } |
There was a problem hiding this comment.
This code block is labeled json but contains a trailing comma after the env object, which makes it invalid JSON for users to copy/paste into ./.agent-workforce/*.json. Either remove the trailing comma or change the fence to jsonc if you want to allow comments/trailing commas in examples.
There was a problem hiding this comment.
Verified this is a false positive — the block parses cleanly:
$ python3 -m json.tool <<'END'
{
"id": "my-posthog",
"extends": "posthog",
"env": { "POSTHOG_API_KEY": "$POSTHOG_API_KEY" },
"permissions": {
"allow": ["mcp__posthog__insights-list", "mcp__posthog__events-query"]
}
}
END
Every comma in the block separates key/value pairs; none trail before a closing brace. Leaving as json.
| * MCP server config, structured to match Claude Code's `--mcp-config` JSON | ||
| * verbatim so the whole object can be passed through untouched. Values inside | ||
| * `headers` / `env` may be literal strings or `$VAR` references resolved from | ||
| * `process.env` at spawn time (see resolveEnvRef). |
There was a problem hiding this comment.
The McpServerSpec docstring references resolveEnvRef, but there is no such symbol in this package (env-ref resolution happens in the CLI’s env-refs.ts). This comment is misleading—either rename the reference to the real helper or remove the pointer and describe the resolution responsibility at a higher level (e.g. “resolved by the runner/CLI at spawn time”).
| * `process.env` at spawn time (see resolveEnvRef). | |
| * `process.env` by the runner/CLI at spawn time. |
| const parsedSkills = parseSkills(skills, `persona[${expectedIntent}].skills`); | ||
| const parsedEnv = parseStringMap(env, `persona[${expectedIntent}].env`); | ||
| const parsedMcpServers = parseMcpServers(mcpServers, `persona[${expectedIntent}].mcpServers`); | ||
| const parsedPermissions = parsePermissions( | ||
| permissions, | ||
| `persona[${expectedIntent}].permissions` | ||
| ); |
There was a problem hiding this comment.
parsePersonaSpec now parses env, mcpServers, and permissions, and resolvePersona* now threads those through into PersonaSelection, but there are no unit tests asserting these fields are parsed and propagated correctly (or rejected on invalid shapes). Since this file already has comprehensive tests, please add coverage (e.g. resolvePersona('posthog') includes mcpServers.posthog, permissions.allow, and env).
| function parseOverride(value: unknown, context: string): LocalPersonaOverride { | ||
| if (!value || typeof value !== 'object' || Array.isArray(value)) { | ||
| throw new Error(`${context} must be a JSON object`); | ||
| } | ||
| const raw = value as Record<string, unknown>; | ||
| if (typeof raw.id !== 'string' || !raw.id.trim()) { | ||
| throw new Error(`${context}.id must be a non-empty string`); | ||
| } | ||
| if (raw.extends !== undefined && (typeof raw.extends !== 'string' || !raw.extends.trim())) { | ||
| throw new Error(`${context}.extends must be a non-empty string if provided`); | ||
| } | ||
| if (raw.systemPrompt !== undefined && typeof raw.systemPrompt !== 'string') { | ||
| throw new Error(`${context}.systemPrompt must be a string if provided`); | ||
| } | ||
| if (raw.description !== undefined && typeof raw.description !== 'string') { | ||
| throw new Error(`${context}.description must be a string if provided`); | ||
| } | ||
| return { | ||
| id: raw.id, | ||
| extends: raw.extends as string | undefined, | ||
| description: raw.description as string | undefined, | ||
| skills: raw.skills as PersonaSpec['skills'] | undefined, | ||
| env: raw.env as LocalPersonaOverride['env'], | ||
| mcpServers: raw.mcpServers as LocalPersonaOverride['mcpServers'], | ||
| permissions: raw.permissions as LocalPersonaOverride['permissions'], | ||
| systemPrompt: raw.systemPrompt as string | undefined, | ||
| tiers: raw.tiers as LocalPersonaOverride['tiers'] | ||
| }; |
There was a problem hiding this comment.
parseOverride only validates id/extends/systemPrompt/description and then blindly casts skills, env, mcpServers, permissions, and tiers. Because these values are later spread/merged, invalid JSON types (e.g. tiers.best as a string, env as an array) can produce confusing runtime errors or silent corruption instead of a clear per-file warning. Consider validating these optional fields (at least: object-vs-array checks and primitive type checks for nested values) before returning the override.
| runtime.harness === 'claude' | ||
| ? ` [mcp-strict: ${Object.keys(resolvedMcp ?? {}).join(', ') || '(none)'}]` | ||
| : ''; | ||
| process.stderr.write(`• spawning: ${spec.bin} ${spec.args.join(' ')}${promptNote}${mcpNote}\n`); | ||
|
|
||
| return new Promise((resolve) => { | ||
| let settled = false; | ||
| const finish = (code: number) => { | ||
| if (settled) return; | ||
| settled = true; | ||
| runCleanup(install.cleanupCommand, install.cleanupCommandString); | ||
| resolve(code); | ||
| }; | ||
|
|
||
| const child = spawn(spec.bin, finalArgs, { | ||
| stdio: 'inherit', | ||
| env: resolvedEnv ? { ...process.env, ...resolvedEnv } : process.env | ||
| }); |
There was a problem hiding this comment.
The spawned-command debug line prints spec.args.join(' '), which for claude includes --append-system-prompt <prompt> and --mcp-config <json>. After env interpolation, that JSON can contain secrets (e.g. Authorization bearer tokens), so this log line can leak credentials into terminal logs/CI logs. Please redact sensitive args (at minimum --mcp-config payload and possibly the system prompt) or log a sanitized summary (model + server names + permission mode) instead of raw argv.
The CLI package's tsc --noEmit resolves types for `@agentworkforce/workload-router` via its published `types` entry (`dist/index.d.ts`). On a fresh CI checkout nothing is built, so lint fails with "Cannot find module" and cascades into dozens of implicit-any errors downstream. Run `pnpm -r run build` before the checks so the router's .d.ts files exist when CLI lint runs.
- redact spawn debug line — stop printing the resolved --mcp-config
payload and full system prompt to stderr; print a sanitized summary
(harness, model, MCP server names, permission rule counts, mode)
so Bearer tokens can no longer leak into terminal or CI logs
- validate optional fields in local-personas parseOverride —
skills/env/mcpServers/permissions/tiers were cast blindly, letting
malformed JSON (e.g. env as an array, tiers.best as a string) reach
downstream spread/merge code. Added targeted shape checks so bad
files surface a clear per-file warning instead
- add tests covering env/mcpServers/permissions propagation through
resolvePersona and resolvePersonaByTier, plus a negative case on a
persona that declares none of them
- docs: packages/cli/README.md install now references
@agentworkforce/cli (not the router); bearer header example uses
braced ${POSTHOG_API_KEY} so it actually interpolates; fix stale
resolveEnvRef reference in McpServerSpec docstring
Copilot #3 (trailing comma in root README.md:88) is a false positive —
the JSON block parses cleanly via python json.tool; leaving that
block unchanged and replying on the PR.
defaultHomeDir checked override.trim() for truthiness but returned the original untrimmed string. A value like " /some/path " would then be passed to existsSync / readdirSync as an untrimmed path — the filesystem wouldn't find it, and the home layer would silently load zero personas with no warning. Trim once at the boundary. Regression test covers the exact failing input.
New library package containing the three things any persona spawner needs:
env-ref resolution ($VAR and ${VAR}), MCP server config resolution, and
per-harness argv translation. CLI now depends on this package instead of
carrying its own copies. External orchestrators on top of workload-router
can compose these helpers directly without reimplementing them.
API split:
- workload-router: models what a persona is (shape-only, harness-agnostic)
- harness-kit: models how to launch it on a given harness
- cli: argv parsing, local-persona cascade, process management
Notable change during extraction: buildInteractiveSpec now *returns*
warnings instead of writing to stderr, so library consumers route I/O
themselves. CLI emits them to stderr as before.
Tests: harness-kit ships with 21 tests (13 env-refs inherited from cli +
8 new covering buildInteractiveSpec's claude / codex / opencode branches
and the "warnings returned, not printed" contract).
publish.yml rewritten from router-only to workspace-wide: - package input (all | workload-router | harness-kit | cli) - version input adds pre* bumps and a `none` option for first-time or re-publish scenarios, plus optional custom_version and prerelease_id - uses `pnpm publish` to rewrite workspace:* → concrete versions at pack time (the CLI depends on harness-kit via workspace:*, so the previous `npm publish` path would have shipped a broken dep spec) - when package=all, publishes in dependency order: router → harness-kit → cli - commits all version bumps in a single chore(release): line and tags each package individually (`<pkg>-v<version>`) verify-publish.yml is new: a follow-up workflow that installs the just- published artifact from the registry and smoke tests it. - CLI: installs globally, asserts `agent-workforce --help` exits 0 with the usage block, bare invocation exits non-zero, unknown persona exits non-zero. - Libraries: installs into a clean tmp project, `import()`s the package, asserts the exports surface is non-empty. Run sequence: trigger Publish Package → trigger Verify Publish with the same package + version.
npm's OIDC trusted-publisher flow exchanges the GitHub workflow's OIDC
identity (the `id-token: write` permission) for a short-lived publish
token — no long-lived NPM_TOKEN secret required. Matches the agent-relay
publish pattern.
Flow: `pnpm pack --pack-destination $PACK_DIR` rewrites workspace:* deps
to concrete versions inside the tarball's package.json, then
`npm publish <tarball> --provenance` uploads via npm's native auth. This
decouples workspace-aware packing from publish-time auth (pnpm's publish
would re-implement auth rather than delegate to npm).
First-publish requirement: each of @agentworkforce/{workload-router,
harness-kit,cli} needs to be registered as a trusted publisher on
npmjs.com pointing at AgentWorkforce/workforce + this workflow path.
Enables the ability to run a persona without workflows. i.e
The usecase that came to mind is running a "posthog" agent, that is already preconfigured with mcp/skills/keys in an isolated way.
So I configure a ~/.agent-workforce/posthog.json with the right shape and I can just run and be authed and have it not have other context i don't want.