From af701e8c7e1b28a0c86c7bec143aafa12e047cf5 Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Mon, 11 May 2026 13:05:14 -0400 Subject: [PATCH 1/2] persona sources: rename display labels (built-in / repo / personal) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cascade has three conceptual tiers users should see: built-in — bundled with @agentworkforce/cli (e.g. persona-maker, persona-improver). Reserved for personas about Agent Workforce itself; intentionally small. personal — ~/.agentworkforce/workforce/personas/. One user, many repos. repo — /.agentworkforce/workforce/personas/. Codified in the working tree so the whole team gets them on checkout. Both installed library packs (the shadcn copy-and-own model) and hand-authored team overrides live here. Internally the cascade keys are still 'cwd' / 'user' / 'library' / 'dir:N' so --save-in-directory and the JSON outputs of `list` and `sources list` are unchanged for any tooling that pinned on them. Only the user-facing SOURCE column gets the new vocabulary: - `agentworkforce list` (table) - `agentworkforce sources list` (table) - the interactive picker A new formatPersonaSourceLabel() helper centralizes the mapping, with a test that locks the four cases. README gains a "Persona sources" section that explains the three categories and where library packs fit. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 32 +++++++++++++++++++++++++ packages/cli/src/cli.ts | 27 ++++++++++++++++----- packages/cli/src/local-personas.test.ts | 10 ++++++++ packages/cli/src/local-personas.ts | 26 ++++++++++++++++++++ 4 files changed, 89 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 1dbdf8e..e072f74 100644 --- a/README.md +++ b/README.md @@ -283,6 +283,38 @@ launcher metadata. AgentWorkforce records Use `--no-launch-metadata` or `AGENTWORKFORCE_LAUNCH_METADATA=0` to skip metadata writing and session-log refresh for that launch. +### Persona sources + +Three places a persona can live, surfaced as `SOURCE` in `agentworkforce list`, +`sources list`, and the interactive picker: + +- **`built-in`** — bundled with `@agentworkforce/cli`. Reserved for personas + that are directly about Agent Workforce itself, e.g. `persona-maker` and + `persona-improver`. Every user has these without installing anything. This + is intentionally a small set. +- **`personal`** — `~/.agentworkforce/workforce/personas/`. For things one + user wants on every repo on their machine, but doesn't want to commit. +- **`repo`** — `/.agentworkforce/workforce/personas/`. Personas codified + in the working tree so the whole team gets them on checkout. Two flavors + end up here: + - **library personas you've copied in** via `agentworkforce install ` — + generalized personas that can be extended to multiple codebases, similar + to the shadcn copy-and-own model. Example: the Relay team's + [`@agentrelay/personas`](https://github.com/AgentWorkforce/relay/tree/main/packages/personas) + pack. + - **repo-specific overrides** — hand-authored or `extends`-based personas + that encode rules unique to this codebase. Often extend a library persona + with project-specific auth, conventions, or skills. See + [AgentWorkforce/relay#839](https://github.com/AgentWorkforce/relay/pull/839) + for a worked example. + +Both flavors physically share the `repo` directory; the distinction is +conceptual — "did this persona come from a published pack, or did we write +it for this codebase?" + +Cascade order is `repo` → configured persona dirs → `personal` → `built-in`; +higher layers may override or `extends` lower ones field-by-field. + ### Persona pack installs Install a persona pack into the current project: diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index d685a7e..bf51438 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -67,6 +67,7 @@ import { import { buildPersonaSourceDirectories, defaultCwdPersonaDir, + formatPersonaSourceLabel, loadLocalPersonas, loadPersonaSourceConfig, normalizePersonaDir, @@ -81,7 +82,9 @@ const USAGE = `Usage: agentworkforce [args...] Run with no arguments inside a TTY to open an interactive persona picker — the top 3 most recently used personas are shown first, and typing fuzzy- -searches across persona names and descriptions. +searches across persona names and descriptions. Each row's SOURCE column +is one of: built-in (bundled), repo (./.agentworkforce/workforce/personas), +personal (~/.agentworkforce/workforce/personas), or dir:N (configured). Commands: create [flags] Opens persona-maker@best for creating a new @@ -1694,15 +1697,21 @@ function formatSourcesTable( dir: 'DIR' }; const cols = ['cascade', 'config', 'source', 'exists', 'dir'] as const; + // Display label only — the underlying `source` literal flows through to + // --json so tooling that pins on `'cwd'` / `'user'` / `'library'` is fine. + const display: readonly SourceDirRow[] = rows.map((r) => ({ + ...r, + source: formatPersonaSourceLabel(r.source) + })); const widths = Object.fromEntries( - cols.map((c) => [c, Math.max(headers[c].length, ...rows.map((r) => r[c].length))]) + cols.map((c) => [c, Math.max(headers[c].length, ...display.map((r) => r[c].length))]) ) as Record<(typeof cols)[number], number>; const line = (row: SourceDirRow) => cols.map((c) => row[c].padEnd(widths[c])).join(' ').trimEnd(); return [ `Config: ${configPath}`, `Default create target: ${defaultCreateTarget ?? '(auto)'}`, - [line(headers), ...rows.map(line)].join('\n'), + [line(headers), ...display.map(line)].join('\n'), '' ].join('\n'); } @@ -2032,7 +2041,9 @@ function formatPersonaTable( }; const rendered: RenderRow[] = rows.map((r) => ({ persona: r.persona, - source: r.source, + // Show the user-facing label (`built-in` / `repo` / `personal` / `dir:N`). + // The internal cascade key is still in `--json` output for tooling. + source: formatPersonaSourceLabel(r.source), harness: r.harness, model: r.model, rating: r.rating, @@ -3535,13 +3546,17 @@ function applyPatchInPlace(root: Record, patch: ImproverPatch): export function buildTuiCandidates(): TuiCandidate[] { const byId = new Map(); for (const spec of listBuiltInPersonas()) { - byId.set(spec.id, { id: spec.id, description: spec.description, source: 'library' }); + byId.set(spec.id, { + id: spec.id, + description: spec.description, + source: formatPersonaSourceLabel('library') + }); } for (const [id, spec] of local.byId.entries()) { byId.set(id, { id, description: spec.description, - source: local.sources.get(id) ?? 'library' + source: formatPersonaSourceLabel(local.sources.get(id) ?? 'library') }); } return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id)); diff --git a/packages/cli/src/local-personas.test.ts b/packages/cli/src/local-personas.test.ts index 55b79ab..08cea8f 100644 --- a/packages/cli/src/local-personas.test.ts +++ b/packages/cli/src/local-personas.test.ts @@ -6,6 +6,7 @@ import { join } from 'node:path'; import { __mergeOverrideForTests, + formatPersonaSourceLabel, loadLocalPersonas, loadPersonaSourceConfig, type LocalPersonaOverride @@ -1030,3 +1031,12 @@ test('override leaves channel alone: inherited claudeMdContent flows through', ( assert.equal(merged.claudeMdContent, '# keep me\n'); assert.equal(merged.claudeMd, undefined); }); + +test('formatPersonaSourceLabel maps internal cascade keys to display labels', () => { + assert.equal(formatPersonaSourceLabel('library'), 'built-in'); + assert.equal(formatPersonaSourceLabel('cwd'), 'repo'); + assert.equal(formatPersonaSourceLabel('user'), 'personal'); + // dir:N passes through unchanged so cascade position stays legible. + assert.equal(formatPersonaSourceLabel('dir:1'), 'dir:1'); + assert.equal(formatPersonaSourceLabel('dir:42'), 'dir:42'); +}); diff --git a/packages/cli/src/local-personas.ts b/packages/cli/src/local-personas.ts index 593e571..72c047f 100644 --- a/packages/cli/src/local-personas.ts +++ b/packages/cli/src/local-personas.ts @@ -88,6 +88,32 @@ export interface LocalPersonaOverride { export type PersonaSource = string; +/** + * Map an internal {@link PersonaSource} cascade label to the human-readable + * vocabulary surfaced in `agentworkforce list`, `sources list`, and the + * interactive picker: + * + * - `library` → `built-in` — bundled with `@agentworkforce/cli`, + * available to every user without an install step. + * - `cwd` → `repo` — `/.agentworkforce/workforce/personas/`, + * i.e. personas codified in the working tree (typically + * team-specific rules, or library packs installed via + * `agentworkforce install`). + * - `user` → `personal` — `~/.agentworkforce/workforce/personas/`, + * i.e. personas a single user keeps across all repos. + * - `dir:N` → `dir:N` — extra configurable persona dirs (passed + * through unchanged so position is still legible). + * + * Internal strings are left alone so `--save-in-directory ` and the + * JSON outputs of `list` / `sources list` keep their existing values. + */ +export function formatPersonaSourceLabel(source: PersonaSource): string { + if (source === 'library') return 'built-in'; + if (source === 'cwd') return 'repo'; + if (source === 'user') return 'personal'; + return source; +} + interface SourceLayer { key: string; source: PersonaSource; From ebc1cc078565e3ef19bc5e07710c53daa132bddc Mon Sep 17 00:00:00 2001 From: Will Washburn Date: Mon, 11 May 2026 13:24:59 -0400 Subject: [PATCH 2/2] persona sources: keep cwd label as `cwd` (not `repo`) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per feedback: `cwd` is a more precise pointer than `repo` — it names the actual directory the persona was loaded from. Only `library → built-in` and `user → personal` remain as display renames; `cwd` and `dir:N` pass through unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 6 +++--- packages/cli/src/cli.ts | 2 +- packages/cli/src/local-personas.test.ts | 3 ++- packages/cli/src/local-personas.ts | 9 ++++----- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index e072f74..c6496ec 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ Three places a persona can live, surfaced as `SOURCE` in `agentworkforce list`, is intentionally a small set. - **`personal`** — `~/.agentworkforce/workforce/personas/`. For things one user wants on every repo on their machine, but doesn't want to commit. -- **`repo`** — `/.agentworkforce/workforce/personas/`. Personas codified +- **`cwd`** — `/.agentworkforce/workforce/personas/`. Personas codified in the working tree so the whole team gets them on checkout. Two flavors end up here: - **library personas you've copied in** via `agentworkforce install ` — @@ -308,11 +308,11 @@ Three places a persona can live, surfaced as `SOURCE` in `agentworkforce list`, [AgentWorkforce/relay#839](https://github.com/AgentWorkforce/relay/pull/839) for a worked example. -Both flavors physically share the `repo` directory; the distinction is +Both flavors physically share the `cwd` directory; the distinction is conceptual — "did this persona come from a published pack, or did we write it for this codebase?" -Cascade order is `repo` → configured persona dirs → `personal` → `built-in`; +Cascade order is `cwd` → configured persona dirs → `personal` → `built-in`; higher layers may override or `extends` lower ones field-by-field. ### Persona pack installs diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index bf51438..3f93ca0 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -83,7 +83,7 @@ const USAGE = `Usage: agentworkforce [args...] Run with no arguments inside a TTY to open an interactive persona picker — the top 3 most recently used personas are shown first, and typing fuzzy- searches across persona names and descriptions. Each row's SOURCE column -is one of: built-in (bundled), repo (./.agentworkforce/workforce/personas), +is one of: built-in (bundled), cwd (./.agentworkforce/workforce/personas), personal (~/.agentworkforce/workforce/personas), or dir:N (configured). Commands: diff --git a/packages/cli/src/local-personas.test.ts b/packages/cli/src/local-personas.test.ts index 08cea8f..3883447 100644 --- a/packages/cli/src/local-personas.test.ts +++ b/packages/cli/src/local-personas.test.ts @@ -1034,8 +1034,9 @@ test('override leaves channel alone: inherited claudeMdContent flows through', ( test('formatPersonaSourceLabel maps internal cascade keys to display labels', () => { assert.equal(formatPersonaSourceLabel('library'), 'built-in'); - assert.equal(formatPersonaSourceLabel('cwd'), 'repo'); assert.equal(formatPersonaSourceLabel('user'), 'personal'); + // cwd passes through — it's already a precise pointer to a real dir. + assert.equal(formatPersonaSourceLabel('cwd'), 'cwd'); // dir:N passes through unchanged so cascade position stays legible. assert.equal(formatPersonaSourceLabel('dir:1'), 'dir:1'); assert.equal(formatPersonaSourceLabel('dir:42'), 'dir:42'); diff --git a/packages/cli/src/local-personas.ts b/packages/cli/src/local-personas.ts index 72c047f..286bb63 100644 --- a/packages/cli/src/local-personas.ts +++ b/packages/cli/src/local-personas.ts @@ -95,12 +95,12 @@ export type PersonaSource = string; * * - `library` → `built-in` — bundled with `@agentworkforce/cli`, * available to every user without an install step. - * - `cwd` → `repo` — `/.agentworkforce/workforce/personas/`, - * i.e. personas codified in the working tree (typically - * team-specific rules, or library packs installed via - * `agentworkforce install`). * - `user` → `personal` — `~/.agentworkforce/workforce/personas/`, * i.e. personas a single user keeps across all repos. + * - `cwd` → `cwd` — `/.agentworkforce/workforce/personas/`, + * the working-tree dir; both installed library packs and + * hand-authored team overrides live here. Kept as-is + * because it's a precise pointer to a real directory. * - `dir:N` → `dir:N` — extra configurable persona dirs (passed * through unchanged so position is still legible). * @@ -109,7 +109,6 @@ export type PersonaSource = string; */ export function formatPersonaSourceLabel(source: PersonaSource): string { if (source === 'library') return 'built-in'; - if (source === 'cwd') return 'repo'; if (source === 'user') return 'personal'; return source; }