Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
- **`cwd`** — `<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 <pkg>` —
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 `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 `cwd` → 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:
Expand Down
27 changes: 21 additions & 6 deletions packages/cli/src/cli.ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 show command outputs raw internal source label instead of user-facing label

The formatPersonaShow function at packages/cli/src/cli.ts:2288 prints the raw internal source value (e.g., library, cwd, user) directly in the human-readable output. All other human-facing displays in this PR — formatPersonaTable (packages/cli/src/cli.ts:2046), formatSourcesTable (packages/cli/src/cli.ts:1704), and buildTuiCandidates (packages/cli/src/cli.ts:3552) — now call formatPersonaSourceLabel() to map these to the new vocabulary (built-in, repo, personal). This means agentworkforce show code-reviewer will display SOURCE library while agentworkforce list displays built-in for the same persona.

(Refers to line 2288)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import {
import {
buildPersonaSourceDirectories,
defaultCwdPersonaDir,
formatPersonaSourceLabel,
loadLocalPersonas,
loadPersonaSourceConfig,
normalizePersonaDir,
Expand All @@ -81,7 +82,9 @@ const USAGE = `Usage: agentworkforce <command> [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), cwd (./.agentworkforce/workforce/personas),
personal (~/.agentworkforce/workforce/personas), or dir:N (configured).

Commands:
create [flags] Opens persona-maker@best for creating a new
Expand Down Expand Up @@ -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');
}
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -3535,13 +3546,17 @@ function applyPatchInPlace(root: Record<string, unknown>, patch: ImproverPatch):
export function buildTuiCandidates(): TuiCandidate[] {
const byId = new Map<string, TuiCandidate>();
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));
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/src/local-personas.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { join } from 'node:path';

import {
__mergeOverrideForTests,
formatPersonaSourceLabel,
loadLocalPersonas,
loadPersonaSourceConfig,
type LocalPersonaOverride
Expand Down Expand Up @@ -1030,3 +1031,13 @@ 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('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');
});
25 changes: 25 additions & 0 deletions packages/cli/src/local-personas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,31 @@ 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.
* - `user` → `personal` — `~/.agentworkforce/workforce/personas/`,
* i.e. personas a single user keeps across all repos.
* - `cwd` → `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).
*
* Internal strings are left alone so `--save-in-directory <target>` 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 === 'user') return 'personal';
return source;
}

interface SourceLayer {
key: string;
source: PersonaSource;
Expand Down
Loading