Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
0b672c8
feat(user-experience): add the plugin skeleton with shape, plan-user-…
kyle-sexton Oct 9, 2026
2ef8b98
refactor(user-interface): read installed() from the shared scripts/li…
kyle-sexton Oct 9, 2026
55c71e5
feat(user-experience): add the route rows and their contract tests
kyle-sexton Oct 9, 2026
c90b959
feat(user-experience): read the team file with merge, the #6642 limit…
kyle-sexton Oct 9, 2026
b900883
feat(user-experience): add the reference files
kyle-sexton Oct 9, 2026
e710867
feat(user-experience): add the four job skills and the evaluator agent
kyle-sexton Oct 9, 2026
be3b322
feat(planning): point /planning:design at the user-experience and use…
kyle-sexton Oct 9, 2026
5a2873b
docs(conventions): record user-experience as a routing-as-data adopter
kyle-sexton Oct 9, 2026
7ac3113
feat(user-experience): complete shape and add setup and prerequisites
kyle-sexton Oct 9, 2026
1ced6d6
test(user-experience): add the plugin eval suite
kyle-sexton Oct 9, 2026
6c8ac50
Merge remote-tracking branch 'origin/main' into feat/user-experience
kyle-sexton Oct 9, 2026
c9897a2
chore(release): convert the user-interface bump to a changelog fragment
kyle-sexton Oct 9, 2026
7635e06
fix(user-experience): describe every shipped skill in the manifest an…
kyle-sexton Oct 9, 2026
adfa43f
fix(user-experience): reject team-file nesting deeper than 64 levels …
kyle-sexton Oct 9, 2026
04ce457
test(user-experience): use en-US spelling in the embedded-instruction…
kyle-sexton Oct 9, 2026
be1f68c
fix(user-experience): fall back to the session skill listing in the j…
kyle-sexton Oct 9, 2026
1d5a3fc
fix(user-experience): harden the team file and job skills after local…
kyle-sexton Oct 9, 2026
99fdb37
test(user-experience): keep the team-file-degrade baseline inside the…
kyle-sexton Oct 9, 2026
e7d5e20
fix(user-experience): put nothing before the record header and calibr…
kyle-sexton Oct 9, 2026
8688593
test(user-experience): make the uc3 calibration samples load and fix …
kyle-sexton Oct 9, 2026
5ede5c4
Merge remote-tracking branch 'origin/main' into feat/user-experience
kyle-sexton Oct 9, 2026
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
7 changes: 7 additions & 0 deletions .changes/planning/feat-user-experience-9c9cb80e.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
bump: patch
---

### Changed

- `/planning:design` points UI/UX work at `/user-experience:shape` and `/user-interface:design` (when enabled), and `/planning:brainstorm` names `/user-experience:shape` under Next for who the users are and what they need.
8 changes: 8 additions & 0 deletions .changes/user-interface/feat-user-experience-a16b8aa2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
bump: patch
---

### Changed

- Detect's installed-plugin logic moves to `scripts/lib/installed.mjs`, a generated copy shared with
user-experience; output unchanged.
6 changes: 6 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,12 @@
"category": "presentation",
"tags": ["user-interface", "ui", "terminal", "cli", "tui", "design-system", "accessibility", "skill"]
},
{
"name": "user-experience",
"source": "./plugins/user-experience",
"category": "design",
"tags": ["user-experience", "ux", "user-research", "personas", "jobs-to-be-done", "skill"]
},
{
"name": "developer-experience",
"source": "./plugins/developer-experience",
Expand Down
1 change: 1 addition & 0 deletions docs/catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ plugin manifests and kept in sync by CI. Never hand-edit it; the category vocabu
- [`naming`](../plugins/naming): Generates and evaluates fresh name candidates for an identifier, file, module, skill, repo, or domain term (naming:name-it-better). It distills a context brief, runs blind fresh-context generators from distinct lenses (responsibility-literal, moment-of-use, domain-lore), and scores a shortlist against the org's naming criteria. The human always picks; it never locks a name. An optional tournament mode adds elimination rounds with independent judges for high-stakes names.
- [`event-storming`](../plugins/event-storming): EventStorming for domain discovery: a methodology skill (Big Picture / Process Modeling / Design-Level facilitation reference, notation, patterns) and a simulation skill (agentic multi-persona workshops that produce a structured-markdown model by default; a live Miro-board rendering path is available when the first-party miro plugin is enabled).
- [`miro`](../plugins/miro): Miro board management over the Model Context Protocol: create and manage boards, sticky notes, shapes, frames, connectors, and tags for EventStorming, brainstorming, and diagramming. Ships a local stdio MCP server that installs its pinned npm dependencies on first launch (needs Node.js 24+ and npm); installs disabled, so opt in. The server starts without a Miro API token; until one is set, each tool call explains how to set it.
- [`user-experience`](../plugins/user-experience): Front door for user experience on the app being built, at any stage from idea to legacy: shape detects the app's stage and the project's own research, personas and analytics, then chains user research planning, synthesis, flows and IA, and evaluation, routing to the tools installed, with every deliverable labeled evidence-based, assumption-based or mixed.

## Development

Expand Down
5 changes: 5 additions & 0 deletions docs/conventions/routing-as-data/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ Notable changes to the routing-as-data contract (SemVer). Changing a required ro
rule, the team-file operations or the degradation rule is a major bump; additive guidance is a minor
bump; docs-only clarification is a patch.

## [1.0.1] - 2026-10-09

- Adopters: `user-experience` moves from Planned to Adopts (group field `job`, first team-layer
implementer).

## [1.0.0] - 2026-10-09

- Route rows: versioned file with a `note`, closed schema, ranks 1..n per group with the project as
Expand Down
2 changes: 1 addition & 1 deletion docs/conventions/routing-as-data/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ user-writable layer.
| Plugin | Status |
|---|---|
| `user-interface` (`/user-interface:design`) | Adopts: bare own-marketplace detects, slash skill ids, no pointer on own-skill rows. Exception: the deferred `axe-accessibility` row keeps a bare `id` and `detect` until it is qualified |
| `user-experience` | Planned |
| `user-experience` (`/user-experience:shape`) | Adopts: group field `job`, the first team-layer implementer; applies #6642's two limits narrower than this doc |
| Developer-experience plugin | Planned |

An adopter with a switch for account-bound tools names it `account_tools_enabled`, a boolean, as
Expand Down
22 changes: 13 additions & 9 deletions docs/plugin-philosophy.md
Original file line number Diff line number Diff line change
Expand Up @@ -1440,16 +1440,17 @@ same depth on two models):
**Pinned agents.** Every named agent in this repository pins its effort, so a session tuned down for
cost does not silently cheapen a worker. Each pins the level that model config's task rows give its
kind of work, and never below `medium` for work that changes code or verifies a change (the
[effort floor](#effort-floor)). Ten pin `effort: high`: `implementation` `phase-verifier`;
[effort floor](#effort-floor)). Twelve pin `effort: high`: `implementation` `phase-verifier`;
`discovery` `researcher`, `intent-tracer`, and `research-verifier`; `review` `code-reviewer`,
`architecture-guardian`, `security-reviewer`, `ci-log-auditor`, and `doc-drift-detector`;
`plugin-quality` `auditor`. Six pin `effort: medium`: `planning` `plan-reviewer` by its
[recorded exception](#named-agent-bar); `implementation` `implementer`, because a phase brief is
scoped feature work and its verifier runs at `high`; `implementation` `scoped-implementer`, because
a plan routes only well-scoped work to it; `songwriting` `object-writer`, because creative
generation is not verification; and `review` `ecosystem-specialist` and `discovery` `explorer`,
because their work is clearly scoped tool use, running a repository's declared commands and reading
and indexing a scope. No pin goes below `medium`, because a low-effort executor stops detecting
`architecture-guardian`, `security-reviewer`, `ci-log-auditor`, `doc-drift-detector`, and
`brief-reviewer`; `plugin-quality` `auditor`; `user-experience` `evaluator`. Seven pin
`effort: medium`: `planning` `plan-reviewer` by its [recorded exception](#named-agent-bar);
`implementation` `implementer`, because a phase brief is scoped feature work and its verifier runs
at `high`; `implementation` `scoped-implementer`, because a plan routes only well-scoped work to
it; `songwriting` `object-writer`, because creative generation is not verification; and `review`
`ecosystem-specialist`, `discovery` `explorer` and `performance` `go-faster-sweeper`, because their
work is clearly scoped tool use, running a repository's declared commands, reading and indexing a
scope, and checking named areas of the development process. No pin goes below `medium`, because a low-effort executor stops detecting
that it is stuck. A frontmatter pin is what holds a named agent's lane, since an Agent-tool dispatch
passes no effort.

Expand Down Expand Up @@ -1485,15 +1486,18 @@ and verdict lanes follow the `high` row; well-specified mechanical work follows
| `discovery` `intent-tracer` | `opus` | `high` | `high` | Reconstructed rationale feeds decisions |
| `discovery` `research-verifier` | `opus` | `high` | `high` | Verdict on a research artifact |
| `discovery` `researcher` | `opus` | `high` | `high` | Research that feeds decisions |
| `performance` `go-faster-sweeper` | `opus` | `medium` | `medium` | Checks each named area of the development process and records findings |
| `planning` `plan-reviewer` | `opus` | `medium` | `medium` | A review lane held to `medium` by its [recorded exception](#named-agent-bar), not by the review rule |
| `plugin-quality` `auditor` | `opus` | `high` | `high` | Audit verdict |
| `review` `architecture-guardian` | `opus` | `high` | `high` | Review verdict |
| `review` `brief-reviewer` | `opus` | `high` | `high` | Review verdict on the brief it is handed |
| `review` `ci-log-auditor` | `opus` | `high` | `high` | Audit verdict on a CI run |
| `review` `code-reviewer` | `opus` | `high` | `high` | Review verdict |
| `review` `doc-drift-detector` | `opus` | `high` | `high` | Drift verdict |
| `review` `ecosystem-specialist` | `sonnet` | `medium` | `medium` | Runs a repository's declared build, test and lint commands |
| `review` `security-reviewer` | `opus` | `high` | `high` | Security verdict |
| `songwriting` `object-writer` | `opus` | `medium` | `medium` | Creative generation, which no row names; the `medium` choice is our judgment |
| `user-experience` `evaluator` | `opus` | `high` | `high` | Verdict on a UX artifact |

- **Pointer:** for the rows, see
[model config: choose an effort level](https://code.claude.com/docs/en/model-config#choose-an-effort-level);
Expand Down
5 changes: 5 additions & 0 deletions docs/skill-cheat-sheet.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,11 @@ owned by [docs/catalog-taxonomy.md](catalog-taxonomy.md).
| [`/testing:check`](../plugins/testing/skills/check/SKILL.md) | `testing` | Report whether node and jq resolve for the testing hooks. Never installs. |
| [`/toolchain:check-prerequisites`](../plugins/toolchain/skills/check-prerequisites/SKILL.md) | `toolchain` | Report whether the tools toolchain declares resolve. Never installs. |
| [`/typos-format:check`](../plugins/typos-format/skills/check/SKILL.md) | `typos-format` | Report whether typos and node are installed. Never installs. |
| [`/user-experience:evaluate`](../plugins/user-experience/skills/evaluate/SKILL.md) | `user-experience` | Plan and run UX evaluations, fair-choice checks and measure selection, evidence-labeled |
| [`/user-experience:plan-user-research`](../plugins/user-experience/skills/plan-user-research/SKILL.md) | `user-experience` | Plan user research and write its instruments, evidence-labeled |
| [`/user-experience:shape`](../plugins/user-experience/skills/shape/SKILL.md) | `user-experience` | Detect the app's stage and the project's own evidence, then chain the UX jobs in order |
| [`/user-experience:structure`](../plugins/user-experience/skills/structure/SKILL.md) | `user-experience` | Shape user flows, information architecture and content structure, evidence-labeled |
| [`/user-experience:synthesize`](../plugins/user-experience/skills/synthesize/SKILL.md) | `user-experience` | Synthesize research data into labeled themes, insights, needs, personas or jobs |
| [`/user-interface:design`](../plugins/user-interface/skills/design/SKILL.md) | `user-interface` | Design interfaces from the project's system and installed tools; terminal guidance built in |
| [`/visualization:present`](../plugins/visualization/skills/present/SKILL.md) | `visualization` | Slide deck through the claude.ai Slides Artifact type, outline markdown as the record |
| [`/visualization:visualize`](../plugins/visualization/skills/visualize/SKILL.md) | `visualization` | Pick the best visual form for what is in the conversation and render it |
Expand Down
104 changes: 104 additions & 0 deletions lib/installed.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// Which routing rows are installed and reachable here, under the routing-as-data detection contract
// (docs/conventions/routing-as-data/README.md, Detection). Pure: every input arrives as an argument.
//
// installed(rows, {pluginRoot, home, projectDir, projectMcpServers, pluginList, mcpList})
// -> {installed: [row ids], reachable: {id: true | false | null}, reason?, uncertain?}
// | {installed: null, reason}
//
// pluginList and mcpList are lazy readers returning the text of `claude plugin list --json` and
// `claude mcp list`, or an Error. The MCP reader runs only after the plugin list parses.
// A bare plugin detect means this plugin's own marketplace: the one in the plugin-list record whose
// installPath is pluginRoot. Without that record, or when its marketplace is inline, skills-dir or
// synced, a bare name matches in any marketplace and `reason` says so; a bare name some row also
// detects qualified is then left out of installed and listed in `uncertain`.
import { existsSync, realpathSync, statSync } from "node:fs";
import { join, resolve } from "node:path";

// Marketplace names that do not identify a marketplace a sibling plugin could come from.
const NO_MARKETPLACE = ["inline", "skills-dir", "synced"];

const isDir = (p) => existsSync(p) && statSync(p).isDirectory();

/** Plugin ids in effect here: a project or local record enabled for this project, or any other scope
* (user, managed) enabled. A fresh local install reads `enabled: true, projectEnabled: false` with this
* project as `projectPath`; the CLI may emit `projectPath: null`. */
function enabledPlugins(records, projectDir) {
const here = resolve(projectDir);
const forHere = (r) => r.projectEnabled || (r.enabled && r.projectPath != null && resolve(r.projectPath) === here);
return new Set(records.filter((r) => (["project", "local"].includes(r.scope) ? forHere(r) : r.enabled)).map((r) => r.id));
}

/** The marketplace of the record installed at pluginRoot, or null. Each record resolves in its own
* try, so a missing or unreadable installPath skips only that record. */
function ownMarketplace(records, pluginRoot) {
const real = (p) => (process.platform === "win32" ? realpathSync.native(p).toLowerCase() : realpathSync.native(p));
const root = real(pluginRoot);
for (const r of records) {
try {
if (r.installPath && real(r.installPath) === root) return r.id.slice(r.id.lastIndexOf("@") + 1);
} catch {
// skip this record
}
}
return null;
}

/** Server name to connected (true or false) from `claude mcp list` lines shaped `name: target - status`. */
function mcpStatus(text) {
const status = new Map();
for (const line of text.split("\n")) {
const m = line.match(/^(.+?): .* - (.*)$/);
if (m) status.set(m[1], /Connected$/.test(m[2]) && !/Not connected$/i.test(m[2]));
}
return status;
}

export function installed(rows, { pluginRoot, home, projectDir, projectMcpServers, pluginList, mcpList }) {
const pluginText = pluginList();
if (pluginText instanceof Error) return { installed: null, reason: pluginText.message };
let plugins, own;
try {
const records = JSON.parse(pluginText);
plugins = enabledPlugins(records, projectDir);
own = ownMarketplace(records, pluginRoot);
} catch (e) {
return { installed: null, reason: `unreadable plugin list: ${e.message}` };
}
const mcpText = mcpList();
if (mcpText instanceof Error) return { installed: null, reason: mcpText.message };
const listed = mcpStatus(mcpText);
const servers = [...listed.keys(), ...projectMcpServers];
const server = (row) => servers.find((s) => s === row.detect || s.endsWith(`:${row.detect}`));
const skillDirs = [join(home, ".claude/skills"), join(projectDir, ".claude/skills")];
const resolved = own && !NO_MARKETPLACE.includes(own);
const names = new Set([...plugins].filter((id) => typeof id === "string").map((id) => id.slice(0, id.lastIndexOf("@"))));
const qualified = new Map(rows.filter((r) => r.detect.includes("@")).map((r) => [r.detect.slice(0, r.detect.lastIndexOf("@")), r.detect]));
const uncertain = {};
const barePlugin = (row) => row.kind === "plugin" || (row.kind === "skill" && /^\/[^:]+:/.test(row.id));
const byName = (row) => {
if (!names.has(row.detect)) return false;
if (!qualified.has(row.detect)) return true;
uncertain[row.id] = `${row.detect} also used by ${qualified.get(row.detect)}`;
return false;
};

const present = (row) => {
if (row.detect.includes("@")) return plugins.has(row.detect);
if (barePlugin(row)) return resolved ? plugins.has(`${row.detect}@${own}`) : byName(row);
if (row.kind === "mcp") return server(row) !== undefined;
if (row.kind === "skill") return skillDirs.some((d) => isDir(join(d, row.detect)));
return false; // kind tool: only the session's own tool listing can tell
};
// true or false when this module can tell; null when only an account, a key or the session can.
const reach = (row) => {
if (row.kind === "mcp") return listed.get(server(row)) ?? null;
return row.account === "none" ? true : null;
};
const found = rows.filter(present);
return {
installed: [...new Set(found.map((r) => r.id))],
reachable: Object.fromEntries(found.map((r) => [r.id, reach(r)])),
...(!resolved && { reason: `own marketplace unresolved (${own ?? "no record at this plugin's path"}); matched by name` }),
...(Object.keys(uncertain).length && { uncertain }),
};
}
66 changes: 66 additions & 0 deletions lib/installed.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { strict as assert } from "node:assert";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { after, test } from "node:test";
import { installed } from "./installed.mjs";

const scratch = mkdtempSync(join(tmpdir(), "installed-lib-"));
after(() => rmSync(scratch, { recursive: true, force: true }));

const ROWS = [{ id: "dovetail", kind: "mcp", detect: "dovetail", account: "login" }];
/** installed() over ROWS with the given plugin reader; returns the result and how often the MCP reader ran. */
function run(pluginList, mcpText = "dovetail: https://mcp.example.test (HTTP) - ✔ Connected\n") {
let mcpCalls = 0;
const result = installed(ROWS, {
pluginRoot: scratch,
home: scratch,
projectDir: scratch,
projectMcpServers: [],
pluginList,
mcpList: () => {
mcpCalls++;
return mcpText;
},
});
return { result, mcpCalls };
}

test("the MCP reader is not called when the plugin reader returns an Error", () => {
const { result, mcpCalls } = run(() => new Error("claude plugin list --json failed: exit 1"));
assert.equal(mcpCalls, 0);
assert.equal(result.installed, null);
assert.equal(result.reason, "claude plugin list --json failed: exit 1");
});

test("the MCP reader is not called when the plugin list is unparsable", () => {
const { result, mcpCalls } = run(() => "{not json");
assert.equal(mcpCalls, 0);
assert.equal(result.installed, null);
assert.match(result.reason, /^unreadable plugin list/);
});

test("a parsed plugin list reads the MCP list once and detects a connected server", () => {
const { result, mcpCalls } = run(() => "[]");
assert.equal(mcpCalls, 1);
assert.deepEqual(result.installed, ["dovetail"]);
assert.deepEqual(result.reachable, { dovetail: true });
});

test("an MCP reader Error nulls installed with its message", () => {
let mcpCalls = 0;
const result = installed(ROWS, {
pluginRoot: scratch,
home: scratch,
projectDir: scratch,
projectMcpServers: [],
pluginList: () => "[]",
mcpList: () => {
mcpCalls++;
return new Error("claude mcp list failed: exit 2");
},
});
assert.equal(mcpCalls, 1);
assert.equal(result.installed, null);
assert.equal(result.reason, "claude mcp list failed: exit 2");
});
4 changes: 4 additions & 0 deletions lib/installed.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
# Owns installed.test.mjs; run-plugin-tests.sh discovers only .test.sh files.
set -euo pipefail
exec node --test "$(dirname "${BASH_SOURCE[0]}")/installed.test.mjs"
1 change: 1 addition & 0 deletions plugins/planning/skills/brainstorm/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Session output has no persisted artifact by default (ideation is conversation ou
- Engineering contract: /planning:interview.
- Product intent: /planning:prd.
- Type and module decisions: /planning:design.
- Who the users are and what they need: /user-experience:shape.

## What this skill does NOT do

Expand Down
2 changes: 1 addition & 1 deletion plugins/planning/skills/design/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ This plugin ships the step-2 resolver at `bash "${CLAUDE_PLUGIN_ROOT}/lib/resolv
- **Code writing**. That's the implementation stage
- **External research**. That's the research capability (this skill synthesizes research results into design decisions)
- **Diagram craft**. This skill selects the dialect a typed artifact is written in; it teaches no dialect. Layout, readability, and syntax idiom route to the visualization capability (`/visualization:visualize` if that plugin is enabled); without it, the plainest correct form of the dialect is emitted
- **UI/UX design**. Use dedicated frontend design and UI/UX tooling
- **UI/UX design**. User research, flows and information architecture are `/user-experience:shape` (if enabled); look and response are `/user-interface:design` (if enabled)
- **Domain event workshops**. A dedicated EventStorming-style capability covers that methodology; this skill covers broader design and may suggest it within module design
- **Product intent**. That's `/planning:prd` (problem, users, success metrics)
- **Intent contract**. That's `/planning:interview` (goal, constraints, acceptance criteria)
Expand Down
Loading
Loading