Skip to content

Consume @agentworkforce/persona-kit; rewrite spawnPersona to honor full persona schema #832

@willwashburn

Description

@willwashburn

Replaces the framing in #824. This consumes the new @agentworkforce/persona-kit package being published in AgentWorkforce/workforce#71 (tracking sequence: workforce issues #64#71).

Written as if persona-kit already exists at @agentworkforce/persona-kit@^1.0.0 — depends on the workforce-side publish landing first.

Why the original #824 framing was wrong

Issue #824 proposed adding skills install behind an opt-in installSkills: true flag. That's a band-aid. A persona's DNA is the full bundle — skills + MCP servers + mount policy + sidecar markdown + inputs + system prompt. AgentRelay.spawnPersona today silently drops most of that bundle. The header comment of packages/sdk/src/personas.ts literally says:

Skills installation, mount policy, sidecar markdown, input rendering, and routing profiles are deliberately not handled here

A persona stripped of skills and mounts isn't a persona — it's a system prompt. That's not what relay should ship. Skills install (and mount, sidecar, input render) is mandatory, not opt-in. The legitimate "I want to look without running" use case is satisfied by a separate dry-run API.

Goal

  • Drop @agentworkforce/harness-kit and @agentworkforce/workload-router deps; replace with @agentworkforce/persona-kit.
  • Gut relay/packages/sdk/src/personas.ts — most of it duplicates work persona-kit now owns.
  • Rewrite AgentRelay.spawnPersona (in relay/packages/sdk/src/relay.ts:771–829) to honor the full persona schema via persona-kit's buildPersonaSpawnPlan + executePersonaSpawnPlan.
  • Add a new public method AgentRelay.getPersonaSpawnPlan(personaId, options?) returning the dry-run plan for authoring tools (persona-maker, validators).
  • Update both the MDX and MD docs (per .claude/rules/docs-sync.md).

File-by-file work

relay/packages/sdk/package.json

  • Remove @agentworkforce/harness-kit.
  • Remove @agentworkforce/workload-router (relay's SDK doesn't use routing profiles or persona catalog).
  • Add @agentworkforce/persona-kit@^1.0.0.

relay/packages/sdk/src/personas.ts

This file currently spans ~550 lines because it duplicates parsing + tier resolution + spec building that persona-kit now owns. Gut it. After this PR it should be ~80 lines:

import {
  loadPersonas,
  resolvePersonaTier,
  buildPersonaSpawnPlan,
  executePersonaSpawnPlan,
  type PersonaSpec,
  type ResolvedPersona,
  type PersonaSpawnPlan,
  type Harness,
  type PersonaTier,
  type PersonaSkill,
  type PersonaMount,
  type PersonaInputSpec,
  type SkillMaterializationPlan,
  HARNESS_VALUES,
  PERSONA_TIERS,
} from '@agentworkforce/persona-kit';

// Re-export the canonical types for SDK consumers
export {
  loadPersonas,
  resolvePersonaTier,
  buildPersonaSpawnPlan,
  HARNESS_VALUES,
  PERSONA_TIERS,
};
export type {
  PersonaSpec,
  ResolvedPersona,
  PersonaSpawnPlan,
  Harness,
  PersonaTier,
  PersonaSkill,
  PersonaMount,
  PersonaInputSpec,
  SkillMaterializationPlan,
};

export interface PersonaLoadOptions {
  cwd?: string;
  searchDirs?: string[];
  extraDirs?: string[];
  tier?: PersonaTier;
}

/** Default search-dir cascade for relay (cwd → user home → AgentWorkforce home). */
function defaultRelaySearchDirs(cwd?: string): string[] {
  // Match the existing relay cascade: agentworkforce/personas, .agentworkforce/workforce/personas,
  // ~/.agentworkforce/workforce/personas, $AGENT_WORKFORCE_HOME/personas
  // [...]
}

export function loadPersona(id: string, options: PersonaLoadOptions = {}): ResolvedPersona {
  const searchDirs = options.searchDirs ?? defaultRelaySearchDirs(options.cwd);
  const dirs = [...searchDirs, ...(options.extraDirs ?? [])];
  const loaded = loadPersonas({ cwd: options.cwd, searchDirs: dirs });
  const spec = loaded.byId.get(id);
  if (!spec) throw new Error(`Persona '${id}' not found in: ${loaded.paths.join(', ')}`);
  return resolvePersonaTier(spec, options.tier ?? 'best');
}

export interface PersonaSpawnOptions {
  cwd?: string;
  installRoot?: string;
  envOverrides?: Record<string, string>;
}

export function getPersonaSpawnPlan(
  personaId: string,
  options: PersonaLoadOptions & PersonaSpawnOptions = {},
): PersonaSpawnPlan {
  const persona = loadPersona(personaId, options);
  return buildPersonaSpawnPlan(persona, {
    cwd: options.cwd,
    installRoot: options.installRoot,
    envOverrides: options.envOverrides,
  });
}

The relay-specific concerns that stay: search-dir cascade defaults, PersonaLoadOptions shape relay consumers expect.

relay/packages/sdk/src/relay.ts (lines 771–829)

Rewrite AgentRelay.spawnPersona:

async spawnPersona(personaId: string, options: SpawnPersonaOptions): Promise<Agent> {
  const spawnCwd = options.cwd ?? process.cwd();
  const persona = loadPersona(personaId, { cwd: spawnCwd, tier: options.tier });
  const plan = buildPersonaSpawnPlan(persona, {
    cwd: spawnCwd,
    installRoot: options.skillsInstallRoot,
    envOverrides: options.env,
  });

  // Always materialize the full persona — skills, MCPs, mount, sidecars, inputs.
  // No opt-in flag. A persona without these is just a system prompt, which isn't
  // what relay ships.
  const handle = await executePersonaSpawnPlan(plan, { cwd: spawnCwd });

  try {
    const task = composePersonaTask(plan, options.task);
    const agent = await this.spawnPty({
      name: options.name ?? persona.id,
      cli: plan.cli,
      args: plan.args,
      env: plan.env,
      task,
      ...options,
    });
    agent.waitForExit().finally(() => handle.dispose());
    return agent;
  } catch (err) {
    await handle.dispose();
    throw err;
  }
}

Add a new public method:

/**
 * Build a `PersonaSpawnPlan` for the persona without executing it. Useful for
 * authoring tools that want to validate a persona's skill sources, mount
 * policy, and harness argv before committing the JSON.
 *
 * Calls under the hood: `loadPersona` → `buildPersonaSpawnPlan`. No filesystem
 * writes, no subprocesses.
 */
getPersonaSpawnPlan(
  personaId: string,
  options?: PersonaLoadOptions & PersonaSpawnOptions,
): PersonaSpawnPlan {
  return getPersonaSpawnPlan(personaId, options);
}

relay/packages/sdk/src/personas.test.ts (or wherever persona tests live)

Replace tests for the gutted code with tests against the persona-kit-backed surface:

  • loadPersona(id) — happy path against a fixture; throws with cascade paths in the error when missing.
  • getPersonaSpawnPlan(id) — returns a plan matching what workforce CLI builds for the same persona on the same harness. Assert by snapshot.
  • spawnPersona(id) end-to-end with a fake skill source (test-only resolver mapped to echo) — verify execution order, verify cleanup runs after agent.waitForExit().
  • Failure-path: persona declares a hallucinated skill source → spawnPersona throws before spawnPty is called; no orphan process; handle.dispose() reverses partial state.
  • JSON serialization: JSON.parse(JSON.stringify(plan)) round-trips byte-for-byte.

Bundle check

  • Verify relay/packages/sdk/dist/workers.js (the workerd entry) doesn't pull node: modules transitively from persona-kit. If it does, switch to @agentworkforce/persona-kit/plan (pure entry) for the workers build and @agentworkforce/persona-kit (full) only for the node entry.

Docs sync

Per .claude/rules/docs-sync.md, every MDX change mirrors to MD.

  • web/content/docs/reference-sdk.mdx — document AgentRelay.spawnPersona's new contract (always installs skills/mount/sidecars/inputs), document new getPersonaSpawnPlan method, link to persona-kit's README for the canonical persona schema reference. Remove/update the old caveat about skills being deferred.
  • docs/reference-sdk.md — same content, MDX components stripped per the sync rule.
  • If web/content/docs/personas.mdx (or similar) exists — update there too.
  • Update packages/sdk/README.md if it lists persona behavior; refresh.

Migration notes for SDK consumers

This is a breaking change for anyone calling spawnPersona against a persona that declares skills/mount/sidecars and was relying on those being silently dropped. Before this change, relay's spawn was always cheap (no installs, no fs writes). After this change, spawn does everything the workforce CLI does.

For the rare consumer who wants the old "just spawn the harness, ignore the rest of the schema" behavior:

  • Option A: Use AgentRelay.spawnPty directly with the cli/args from getPersonaSpawnPlan(id). They get the harness invocation without the side effects.
  • Option B: Author personas without skills/mount/sidecars. The DNA is opt-in at authoring time, not opt-in at spawn time.

Document both in reference-sdk.mdx.

Constraints

  • Persona-kit's API is the contract; relay should not re-implement parsing or tier-resolution. If something feels missing in persona-kit, file an issue against AgentWorkforce/workforce instead of working around it in relay.
  • The new contract closes Handle persona skills installation in SDK #824 — link it as 'Closes Handle persona skills installation in SDK #824' in the PR description.
  • Don't add an opt-in flag for skills install. The right opt-in is getPersonaSpawnPlan (don't run anything; just look). Adding a installSkills: false option re-creates the original problem.
  • No back-compat shim for the dropped harness-kit / workload-router imports. Per the workforce-side migration, those are gone.

Verification

  • pnpm install resolves cleanly with the new dep set; @agentworkforce/harness-kit and @agentworkforce/workload-router are absent from the lockfile.
  • pnpm test in relay/packages/sdk passes.
  • End-to-end test (manual or CI):
    1. Pick a persona that exercises the full schema: skills (vercel-labs/skills#find-skills or similar), mount policy, claudeMdContent sidecar, inputs with env-substitution.
    2. Run agentworkforce <persona>. Record everything: skill installs, sidecar files, mount apply, harness argv, env at spawn.
    3. Run AgentRelay.spawnPersona(<persona>) against the same persona. Assert byte-equality with the workforce-side recording.
    4. Run the agent for ~10 seconds; verify .claude/skills/find-skills/SKILL.md (or harness equivalent) exists and is non-empty during the session.
    5. Failure test: persona with a hallucinated skill source → spawnPersona throws before spawnPty; no orphan process; no half-installed .claude/skills/<name> left behind.
  • AgentRelay.getPersonaSpawnPlan(<persona>) returns a plan whose skills.installs[*].argv matches step 2's recorded npx commands byte-for-byte.
  • Docs render correctly: visit the local docs build (pnpm --filter web dev or equivalent), confirm reference-sdk page reflects the new API.
  • The dist/personas.d.ts exports the right types from persona-kit.

Out of scope

  • Routing profiles / personaCatalog. Workforce-CLI internals; relay doesn't need them. The dropped @agentworkforce/workload-router dep reflects that.
  • Persona authoring tool UX. getPersonaSpawnPlan gives them what they need; how they render a plan is their problem.
  • Cross-repo schema drift CI. Once persona-kit is the single source of truth (which it is after this PR), this is unnecessary.

Closes

Closes #824.

Depends on

This issue can start drafting in parallel with the workforce side, but cannot land until #71 publishes @agentworkforce/persona-kit@1.0.0.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions