Skip to content

feat(sdk): runtime-extensible role catalog for marketplace plugins (#975)#976

Open
goldsziggy wants to merge 2 commits intobradygaster:devfrom
goldsziggy:feat/975-marketplace-plugin-roles
Open

feat(sdk): runtime-extensible role catalog for marketplace plugins (#975)#976
goldsziggy wants to merge 2 commits intobradygaster:devfrom
goldsziggy:feat/975-marketplace-plugin-roles

Conversation

@goldsziggy
Copy link
Copy Markdown

Summary

Closes #975. Lets marketplace plugins ship a roles/ directory whose definitions resolve through useRole() / listRoles() / searchRoles() / getCategories() identically to built-in roles.

  • New module: packages/squad-sdk/src/roles/registry.ts — runtime role registry. registerPluginRoles(plugin, roles) throws on collision with a built-in (additive-only guarantee) and skips duplicate-plugin ids with a structured skipped result.
  • New module: packages/squad-sdk/src/roles/loader.tsloadPluginRolesFromDir(<squadDir>/plugins) scans <plugin>/roles/*.json (single role or array) and registers each.
  • SDK re-exports: registerPluginRoles, unregisterPluginRole, clearPluginRoles, getPluginRoles, getPluginRoleRegistrations, getAllRoles, loadPluginRolesFromDir, plus types.
  • CLI: new loadPluginRolesForDest() helper is invoked from squad (interactive), squad roles, and squad init, so plugin roles appear in the catalog, hire wizard, and --sdk --roles scaffolding.
  • CLI UX: squad roles gains a "🔌 Plugin Roles" section grouped by plugin name.
  • Tests: test/plugin-roles.test.ts (15 new cases) covers collision, duplicate-skip, category contribution, useRole() resolution, loader edge cases (missing dir, invalid JSON, non-JSON entries, built-in shadow attempt). Existing role tests unchanged and passing.

Design notes

  • Plugin roles are appended after BASE_ROLES in getAllRoles() so iteration order stays deterministic. Collision with a built-in id is a hard error — an organization that wants a tweaked backend must use a namespaced id like @acme/backend.
  • The registry is process-global module state. loadPluginRolesForDest() is idempotent per-directory for a single process (pass { force: true } to reload after squad plugin marketplace add …).

Risks / call-outs

  1. Process-global registry state. Tests that register plugin roles and don't clean up can leak into later files. New tests use clearPluginRoles() in beforeEach/afterEach, but future contributors need the same discipline.
  2. Unvalidated plugin role shape. The loader JSON.parses whatever is on disk and hands it to registerPluginRoles(). Missing fields will blow up later in useRole() (TypeScript types aren't enforced at runtime). A follow-up could add a Zod-style schema check; issue scope didn't require it.
  3. Supply-chain trust boundary. A plugin with write access to .squad/plugins/ can register any non-built-in role id. We cannot shadow a built-in (guard), but a plugin could register @malicious/lead and socially engineer users into casting it. Same trust model as the existing plugin marketplace; worth naming.
  4. Init defaults are unchanged. SDK_ROLES_STARTER_TEAM in config/init.ts still points at built-in ids only. squad init --sdk --roles won't auto-select a plugin role; users opt in explicitly by passing the plugin role id (e.g. via the hire wizard or useRole('@acme/x', …) in squad.config.ts). Intentional — we don't want init to pick up arbitrary plugin-provided roles as the default team composition.
  5. CLI startup cost. Loader runs on every squad / squad roles / squad init invocation. If .squad/plugins is missing the cost is a single statSync. If populated, it reads every roles/*.json. Negligible for reasonable plugin counts but worth watching if a plugin ships hundreds of roles.
  6. Pre-existing build-state oddity. npm run lint / npm run build fails on dev today with TOKEN_PATH in platform/comms-teams.ts — unrelated to this PR but flagging so reviewers don't attribute it.

Test plan

  • npx vitest run test/roles.test.ts test/plugin-roles.test.ts test/init-base-roles.test.ts test/fact-checker-role.test.ts test/casting.test.ts test/casting-engine-integration.test.ts test/cast-parser.test.ts test/init-sdk.test.ts test/init-scaffolding.test.ts test/builders.test.ts test/consumer-imports.test.ts test/package-exports.test.ts test/shell.test.ts — 375/375 pass locally
  • tsc --noEmit clean on both packages/squad-sdk and packages/squad-cli for the changed files
  • Manual: drop a sample role JSON in .squad/plugins/demo/roles/x.json, run squad roles — plugin section appears
  • Manual: useRole('@demo/x', { name: 'foo' }) in squad.config.ts resolves without error

🤖 Generated with Claude Code

goldsziggy and others added 2 commits April 14, 2026 15:46
…radygaster#975)

Introduces a plugin role registry that `useRole()`, `listRoles()`,
`searchRoles()`, and `getCategories()` consult after the built-in
`BASE_ROLES`. Marketplace plugins can ship a `roles/` directory of
role definitions that `loadPluginRolesFromDir()` discovers from
`.squad/plugins/<name>/roles/*.json` and registers with the SDK.

Plugin roles are additive only — the registry throws on id collisions
with built-in roles, so a plugin cannot silently shadow a built-in.
Second-plugin duplicates are skipped (not fatal) and reported on the
result.

The `squad`, `squad roles`, and `squad init` entry points now load
`.squad/plugins/*/roles/*.json` before dispatching, so plugin roles
show up alongside built-ins in the roles listing, the hire wizard,
and the SDK scaffolding path (`squad init --sdk --roles`).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- features/built-in-roles.md — adds "Plugin-contributed roles" section
  with file format, useRole() resolution, and collision rules
- features/plugins.md — marketplace repo structure now shows roles/
  directory and links to the built-in-roles doc
- guide/building-extensions.md — extension structure shows optional
  roles/ directory

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@bradygaster
Copy link
Copy Markdown
Owner

@copilot resolve the merge conflicts in this pull request

@bradygaster
Copy link
Copy Markdown
Owner

🏗️ UX Risk Analysis — Deep Code Review

Overall verdict: Request changes on UX grounds. The feature works on the happy path but failure modes are nearly invisible to users.

🔴 High Severity

# Risk Detail
1 Silent failures loadPluginRolesFromDir() records errors internally, but the CLI helper (loadPluginRolesForDest()) drops the summaries. Malformed JSON, unreadable files, collisions — all fail with zero user-facing output.
2 Discoverable but unusable Plugin roles preload for squad interactive, squad roles, and squad init — but NOT for squad build. User sees a role in squad roles, puts it in config → "Unknown base role" at build time. Trust-breaking.
3 No runtime schema validation Parsed JSON is cast straight to BaseRole with no checks. A role with id but missing title/vibe/expertise registers fine, then crashes downstream in squad roles --search or useRole(). Users get cryptic runtime errors, not "invalid plugin role."

🟡 Medium Severity

# Risk Detail
4 Collision message swallowed The error text is actionable ("namespace like @acme/backend") — but it's captured in a summary object nobody reads. Delivery is broken.
5 Partial array load One bad role in a JSON array partially registers earlier entries, aborts later ones. Order-dependent and confusing.
6 Search has no provenance squad roles --search returns flat results — no built-in vs plugin marker, no source plugin name. Users can't tell where a match came from.
7 Lifecycle gaps No clean uninstall/update. force reload exists but nothing calls it. Removed roles linger in long-lived shells. Generic "Unknown base role" instead of "plugin no longer installed."
8 Performance cliff Synchronous scan. Fine for a handful of plugins; 100+ plugins × 50+ roles → thousands of sync reads per startup.

🟢 Low Severity

# Risk Detail
9 Ordering bias Built-ins always first, plugins append. Deterministic but may surprise users expecting override semantics.

Recommended Fixes (3 items to unblock merge)

  1. Surface load summaries — emit warnings to stderr when roles fail to load
  2. Preload in ALL useRole() consumers — especially squad build
  3. Basic schema validation — at minimum: id, title, expertise must be non-empty strings before registration

Great feature direction — just needs the failure paths to be as polished as the happy path. 🚀

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.

Feature Request: Allow marketplace plugins to extend default agent catalog

2 participants