Skip to content

Workforce cli#16

Merged
willwashburn merged 8 commits intomainfrom
workforce-cli
Apr 17, 2026
Merged

Workforce cli#16
willwashburn merged 8 commits intomainfrom
workforce-cli

Conversation

@willwashburn
Copy link
Copy Markdown
Member

@willwashburn willwashburn commented Apr 17, 2026

Enables the ability to run a persona without workflows. i.e

agent-workforce agent <persona>[@<tier>] [task...]

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.


Open with Devin

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.
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/cli with agent-workforce agent <persona>[@<tier>] [task...], including local persona cascade + env-ref resolution and MCP/permissions wiring (interactive).
  • Extend @agentworkforce/workload-router persona/spec/selection types to include env, mcpServers, and permissions, add useSelection(), and add a posthog persona + 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 posthogposthogAgent 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.

Comment thread packages/cli/README.md Outdated
Comment on lines +17 to +22
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
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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

Copilot uses AI. Check for mistakes.
Comment thread packages/cli/README.md Outdated
"posthog": {
"type": "http",
"url": "https://mcp.posthog.com/mcp",
"headers": { "Authorization": "Bearer $POSTHOG_API_KEY" }
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
"headers": { "Authorization": "Bearer $POSTHOG_API_KEY" }
"headers": { "Authorization": "Bearer ${POSTHOG_API_KEY}" }

Copilot uses AI. Check for mistakes.
Comment thread README.md
Comment on lines +80 to +88
```json
{
"id": "my-posthog",
"extends": "posthog",
"env": { "POSTHOG_API_KEY": "$POSTHOG_API_KEY" },
"permissions": {
"allow": ["mcp__posthog__insights-list", "mcp__posthog__events-query"]
}
}
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread packages/workload-router/src/index.ts Outdated
* 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).
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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”).

Suggested change
* `process.env` at spawn time (see resolveEnvRef).
* `process.env` by the runner/CLI at spawn time.

Copilot uses AI. Check for mistakes.
Comment on lines 1133 to +1139
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`
);
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Comment on lines +102 to +129
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']
};
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment thread packages/cli/src/cli.ts Outdated
Comment on lines +379 to +396
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
});
Copy link

Copilot AI Apr 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 6 additional findings.

Open in Devin Review

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.
devin-ai-integration[bot]

This comment was marked as resolved.

- 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.
@willwashburn willwashburn merged commit e7cbe09 into main Apr 17, 2026
1 check passed
@willwashburn willwashburn deleted the workforce-cli branch April 17, 2026 20:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants