Skip to content
Open
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
40 changes: 31 additions & 9 deletions .claude/skills/wizard-architecture/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@ The Confidence Wizard is a CLI tool for quickly setting up and integrating [Conf

The project is organized into decoupled top-level concerns. Each has its own subdirectory and must not depend on the others' internals.

### Shared Kernel (`src/shared-kernel/`)

Cross-domain vocabulary types that multiple domains depend on. Contains only type definitions — no runtime logic, no functions.

- Types here form the ubiquitous language of the project: identifiers and enums that appear in function signatures across domain boundaries (e.g. `IdeId`, `OnboardingGoal`, `PluginInstallationMethod`, `DetectedProvider`).
- If a type is used by three or more domains, it belongs here. If it's used by only one domain, it stays in that domain's `types.ts`.
- The shared kernel depends on nothing in `src/`. Every other domain may import from it.
- Adding a new shared type: define in `types.ts`, re-export from `index.ts`.

### 1. Commands (`src/commands/`)

CLI command definitions using yargs. Each command is a self-contained module exporting a `Command` object.
Expand Down Expand Up @@ -58,6 +67,16 @@ Terminal user interface built with Ink and React. Organized into:

UI modules must not import from `src/commands/`. They may import from `src/frameworks/` only to read framework metadata for display.

### 6. Features (`src/features/`)

Vertical feature slices that compose logic from multiple domains. Each feature gets its own subdirectory (e.g. `features/onboarding/`).

- Currently: `onboarding/` — prompt builder for the project onboarding flow.
- Features may import from `src/lib/`, `src/shared-kernel/`, and (via type-only imports) from other domains.
- Features must not import from `src/ui/` or `src/commands/`.
- Each feature exports through a barrel `index.ts` with a compact public API.
- Adding a new feature: create a subdir under `features/`, add a barrel, document in this skill and CLAUDE.md.

## Hard Constraints

### No product knowledge in the TUI
Expand All @@ -76,14 +95,17 @@ All of this belongs in the Claude Code Skill and is delivered via Confidence MCP
### Dependency direction

```
commands → ui, frameworks, lib
ui → lib, providers (and frameworks for display metadata only)
frameworks → lib
providers → lib
lib → nothing in src/
commands → ui, features, frameworks, lib, shared-kernel
features → lib, shared-kernel
ui → features, lib, providers, shared-kernel (and frameworks for display metadata only)
frameworks → lib, shared-kernel
providers → lib, shared-kernel
integrations → lib, shared-kernel
lib → shared-kernel
shared-kernel → nothing in src/
```

No circular dependencies. No upward imports. If two domains need to communicate, it flows through `src/lib/` shared types.
No circular dependencies. No upward imports. Cross-domain vocabulary types live in `src/shared-kernel/`. Runtime utilities and session infrastructure live in `src/lib/`.

Within the UI layer, the same principle applies at a finer grain:

Expand Down Expand Up @@ -218,7 +240,7 @@ function applyStatuses(updated: Record<string, Status>) {

### Path Aliases

Use path aliases (`@commands/`, `@frameworks/`, `@integrations/`, `@providers/`, `@ui/`, `@lib/`) for all imports that cross top-level domain boundaries under `src/`. Keep relative imports for references within the same domain.
Use path aliases (`@commands/`, `@features/`, `@frameworks/`, `@integrations/`, `@providers/`, `@shared-kernel/`, `@ui/`, `@lib/`) for all imports that cross top-level domain boundaries under `src/`. Keep relative imports for references within the same domain.

Aliases are configured in `tsconfig.build.json` (`paths`) and `vitest.config.ts` (`resolve.alias`). When adding a new top-level domain under `src/`, add its alias to both files.

Expand Down Expand Up @@ -249,7 +271,7 @@ import { ScreenId } from '@lib/session.js';
- In `useEffect`, use named functions instead of anonymous lambdas for the effect callback.
- Prefer `AbortController` for removing event listeners instead of manually calling `removeEventListener`. Pass `{ signal: controller.signal }` to `addEventListener` and call `controller.abort()` in cleanup. This avoids needing to keep a reference to the exact same handler function and scales cleanly when multiple listeners share a lifetime.
- Prefer "UI as a function of state" — derive values from state and props in the render body rather than stashing them in refs. Resort to `useRef` only when there is no pure-function alternative (e.g. holding a DOM node, a timer ID, or an instance that must survive re-renders without triggering one).
- No ad-hoc union extensions at call sites. When a function parameter or callback needs a union type (e.g. `ChosenIde | 'skip'`), define a named type in the slice's `actions.ts` and reference it — don't write inline unions like `value: SomeType | 'extra'` in function signatures. Composition is fine (`type DetectedSelectValue = IdeSelectValue | 'continue'`), but it must be named and exported from `actions.ts`.
- No ad-hoc union extensions at call sites. When a function parameter or callback needs a union type (e.g. `IdeId | 'skip'`), define a named type in the slice's `actions.ts` and reference it — don't write inline unions like `value: SomeType | 'extra'` in function signatures. Composition is fine (`type DetectedSelectValue = IdeSelectValue | 'continue'`), but it must be named and exported from `actions.ts`.
- In `switch` statements, the `default` case must use an exhaustive check via `satisfies never` to catch unhandled variants at compile time:
```ts
default: {
Expand Down Expand Up @@ -279,7 +301,7 @@ Never suppress, silence, or filter runtime warnings (e.g. `--no-warnings`, `--di
When the project grows, new top-level concerns (e.g. `src/agent/` for agent harness logic, `src/detection/` for project analysis) follow the same pattern:

- Own subdirectory under `src/`
- Shared types in `src/lib/` or own `types.ts`
- Cross-domain vocabulary types in `src/shared-kernel/types.ts`; domain-specific types in a local `types.ts`
- Exported through a barrel `index.ts`
- No circular dependencies with existing domains
- Documented in this skill and CLAUDE.md
Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/wizard-integrations/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ Each IDE owns the full implementation of all these methods. Shared helpers (`ver

### Shared types

`IdeId` is defined in `src/integrations/types.ts` — it belongs to the integrations module. `WizardSession` uses its own `ChosenIde` type (same string union, defined in `src/lib/session.ts`) to stay decoupled from the integrations module. This keeps the dependency direction clean: integrations never imports from lib/session for its own type definitions, and session never imports from integrations.
`IdeId` is defined in `src/integrations/types.ts` — it belongs to the integrations module. `WizardSession` uses its own `IdeId` type (same string union, defined in `src/lib/session.ts`) to stay decoupled from the integrations module. This keeps the dependency direction clean: integrations never imports from lib/session for its own type definitions, and session never imports from integrations.

### Orchestrators

Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ CLI wizard for setting up and integrating [Confidence](https://confidence.spotif

## Architecture

The project has five decoupled domains:
The project has five decoupled domains plus a shared kernel and a features layer:

- **`src/shared-kernel/`** — Cross-domain vocabulary types (`IdeId`, `OnboardingGoal`, `PluginInstallationMethod`, `DetectedProvider`). Type definitions only, no runtime logic.
- **`src/commands/`** — CLI command definitions (yargs-based). Currently: default (launches TUI) and help.
- **`src/features/`** — Vertical feature slices. Currently: `onboarding/` (prompt builder for the onboarding flow).
- **`src/frameworks/`** — Framework integration configs. One subdir per framework (react, nextjs, node). Each exports a `FrameworkConfig` with detection, SDK package, and docs URL.
- **`src/integrations/`** — IDE integration strategies. One subdir per supported IDE (claude, cursor, codex). Each exports an `IdeIntegration` object implementing the strategy pattern. Also contains IDE-agnostic MCP, plugin, and chat-session logic.
- **`src/providers/`** — Provider detection for competing feature flag platforms (Statsig, Eppo, PostHog, Optimizely). One subdir per provider.
Expand Down Expand Up @@ -69,7 +71,7 @@ The stable `node-pty` release (v1.1.0) doesn't ship prebuilt binaries for Node.j

## Conventions

- Use path aliases (`@commands/`, `@frameworks/`, `@integrations/`, `@providers/`, `@ui/`, `@lib/`) for cross-domain imports. Keep relative imports within the same domain.
- Use path aliases (`@commands/`, `@features/`, `@frameworks/`, `@integrations/`, `@providers/`, `@shared-kernel/`, `@ui/`, `@lib/`) for cross-domain imports. Keep relative imports within the same domain.
- Use `@inkjs/ui` components over standalone `ink-*` packages.
- Screens go in `src/ui/tui/screens/` (as slices or flat files), reusable components in `components/`.
- Shared modules (`hooks/`, `lib/`, `components/`) must never import from screen slices. If a type is needed by both, put it in `tui/lib/`.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildOnboardingPrompt } from '@lib/onboarding-prompt/index.js';
import { buildOnboardingPrompt } from '@features/onboarding/index.js';

describe('buildOnboardingPrompt', () => {
const baseOpts = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { buildReportTemplate } from '@lib/onboarding-prompt/report-templates.js';
import type { OnboardingGoal } from '@lib/session.js';
import { buildReportTemplate } from '@features/onboarding/report-templates.js';
import type { OnboardingGoal } from '@shared-kernel/types.js';

function fileEntries(goals: OnboardingGoal[]): string[] {
const { start } = buildReportTemplate(goals);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { skillInvocation, referenceInstruction } from '@lib/onboarding-prompt/tool-vars.js';
import type { ChosenIde } from '@lib/session.js';
import { skillInvocation, referenceInstruction } from '@features/onboarding/tool-vars.js';
import type { IdeId } from '@shared-kernel/types.js';

describe('skillInvocation', () => {
it.each<{ ide: ChosenIde; expected: string }>([
it.each<{ ide: IdeId; expected: string }>([
{ ide: 'claude', expected: '/confidence:analyze-project' },
{ ide: 'codex', expected: '$analyze-project' },
{ ide: 'cursor', expected: '/analyze-project' },
Expand All @@ -14,7 +14,7 @@ describe('skillInvocation', () => {

describe('referenceInstruction', () => {
describe('when method is cli', () => {
it.each<{ ide: ChosenIde; expected: string }>([
it.each<{ ide: IdeId; expected: string }>([
{
ide: 'claude',
expected: 'Invoke the `/confidence:analyze-project` skill as a **methodology reference**',
Expand All @@ -34,7 +34,7 @@ describe('referenceInstruction', () => {
});

describe('when method is download', () => {
it.each<{ ide: ChosenIde; expected: string }>([
it.each<{ ide: IdeId; expected: string }>([
{
ide: 'claude',
expected: 'Read `.claude/skills/analyze-project/SKILL.md` as a **methodology reference**',
Expand Down
6 changes: 3 additions & 3 deletions __tests__/ui/screens/ConnectToolsScreen.auth.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ import { ConnectToolsScreen } from '@ui/tui/screens/connect-tools/index.js';
import { ScreenId } from '@lib/session.js';
import { persistMcpPreference, clearMcpPreference, MCP_SERVERS } from '@integrations/index.js';
import type { McpServerName } from '@integrations/index.js';
import type { ChosenIde } from '@lib/session.js';
import type { IdeId } from '@shared-kernel/types.js';
import { server } from '../../msw/server.js';

type IntegrationTestCase = {
ide: ChosenIde;
ide: IdeId;
};

describe('ConnectToolsScreen', () => {
Expand Down Expand Up @@ -201,7 +201,7 @@ type McpConfigOpts = {
token?: string;
};

function writeMcpConfig(projectDir: string, ide: ChosenIde, opts?: McpConfigOpts): void {
function writeMcpConfig(projectDir: string, ide: IdeId, opts?: McpConfigOpts): void {
switch (ide) {
case 'claude':
return writeJsonMcpConfig(join(projectDir, '.mcp.json'), opts);
Expand Down
8 changes: 4 additions & 4 deletions __tests__/ui/testing-framework/ink/render.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { WizardRouter } from '@ui/tui/router.js';
import { SCREEN_TRANSITIONS } from '@ui/tui/screen-transitions.js';
import { RouterContext } from '@ui/tui/hooks/useRouter.js';
import { App } from '@ui/tui/App.js';
import type { AuthState, OnboardingGoal, ScreenId } from '@lib/session.js';
import type { ChosenIde } from '@lib/session.js';
import type { IdeId, OnboardingGoal } from '@shared-kernel/types.js';
import type { AuthState, ScreenId } from '@lib/session.js';

// ink-testing-library's Stdout provides columns (100) but not rows,
// so useTerminalSize defaults to 24 — below SHORT_THRESHOLD (28).
Expand All @@ -20,8 +20,8 @@ type RenderOptions = StoreOptions & {
screen?: ScreenId;
authState?: AuthState;

ide?: ChosenIde;
plugins?: ChosenIde[];
ide?: IdeId;
plugins?: IdeId[];

framework?: string;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { PluginInstallationMethod } from '@integrations/types.js';
import type { ChosenIde, OnboardingGoal } from '../session.js';
import { addIf } from '../prompt-utils.js';
import type { IdeId, OnboardingGoal, PluginInstallationMethod } from '@shared-kernel/types.js';
import { addIf } from '@lib/prompt-utils.js';
import { buildToolVars } from './tool-vars.js';
import { preflight } from './sections/preflight.js';
import { scaffold } from './sections/scaffold.js';
Expand All @@ -13,7 +12,7 @@ import { summary, rules } from './sections/summary.js';
type PromptOptions = {
framework: string;
projectDir: string;
ide?: ChosenIde;
ide?: IdeId;
isEmptyProject?: boolean;
goals?: OnboardingGoal[];
hasProviders?: boolean;
Expand Down
3 changes: 3 additions & 0 deletions src/features/onboarding/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { buildOnboardingPrompt } from './build-prompt.js';
export type { ReportTemplate } from './report-templates.js';
export { skillInvocation, referenceInstruction } from './tool-vars.js';
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { OnboardingGoal } from '../session.js';
import type { OnboardingGoal } from '@shared-kernel/types.js';

export type ReportTemplate = { start: string; end: string };

Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import type { PluginInstallationMethod } from '@integrations/types.js';
import type { ChosenIde } from '../../session.js';
import type { IdeId, PluginInstallationMethod } from '@shared-kernel/types.js';
import { loadStep } from '../steps/load.js';
import { referenceInstruction } from '../tool-vars.js';

export function instrumentEvents(
framework: string,
step: number,
isEmptyProject: boolean,
ide: ChosenIde,
ide: IdeId,
pluginInstallMethod?: PluginInstallationMethod | null,
): string {
return loadStep('instrument-events.md', {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import type { PluginInstallationMethod } from '@integrations/types.js';
import type { ChosenIde } from '../../session.js';
import type { IdeId, PluginInstallationMethod } from '@shared-kernel/types.js';
import { loadStep } from '../steps/load.js';
import { referenceInstruction } from '../tool-vars.js';

export function integrateViaSkill(
framework: string,
step: number,
isEmptyProject: boolean,
ide: ChosenIde,
ide: IdeId,
pluginInstallMethod?: PluginInstallationMethod | null,
): string {
const needsReactGotchas = /react|nextjs|next/i.test(framework);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CONFIDENCE_DOCS_URL } from '../../constants.js';
import { CONFIDENCE_DOCS_URL } from '@lib/constants.js';
import { loadStep } from '../steps/load.js';

export function preflight(toolVars: Record<string, string>): string {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CONFIDENCE_DOCS_URL } from '../../constants.js';
import { CONFIDENCE_DOCS_URL } from '@lib/constants.js';
import { loadStep } from '../steps/load.js';

export function determineRecordingSDK(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { OnboardingGoal } from '../../session.js';
import { CONFIDENCE_DOCS_URL } from '../../constants.js';
import type { OnboardingGoal } from '@shared-kernel/types.js';
import { CONFIDENCE_DOCS_URL } from '@lib/constants.js';
import { buildReportTemplate } from '../report-templates.js';
import { loadStep } from '../steps/load.js';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { interpolate } from '../../prompt-utils.js';
import { interpolate } from '@lib/prompt-utils.js';

const STEPS_DIR = dirname(fileURLToPath(import.meta.url));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,42 +1,41 @@
import type { PluginInstallationMethod } from '@integrations/types.js';
import type { ChosenIde } from '../session.js';
import { PLUGIN_NAME } from '../constants.js';
import type { IdeId, PluginInstallationMethod } from '@shared-kernel/types.js';
import { PLUGIN_NAME } from '@lib/constants.js';

type ToolFormatter = (server: string, tool: string) => string;

const TOOL_FORMATTERS: Record<ChosenIde, ToolFormatter> = {
const TOOL_FORMATTERS: Record<IdeId, ToolFormatter> = {
claude: (server, tool) => `mcp__${server}__${tool}`,
codex: (server, tool) => `${server}:${tool}`,
cursor: (server, tool) => `mcp__${server}__${tool}`,
};

const SKILL_INVOCATIONS: Record<ChosenIde, (skill: string) => string> = {
const SKILL_INVOCATIONS: Record<IdeId, (skill: string) => string> = {
claude: (skill) => `/${PLUGIN_NAME}:${skill}`,
codex: (skill) => `$${skill}`,
cursor: (skill) => `/${skill}`,
};

const SKILLS_DIRS: Record<ChosenIde, string> = {
const SKILLS_DIRS: Record<IdeId, string> = {
claude: '.claude/skills',
cursor: '.cursor/skills',
codex: '.agents/skills',
};

export function skillInvocation(skillName: string, ide: ChosenIde): string {
export function skillInvocation(skillName: string, ide: IdeId): string {
return SKILL_INVOCATIONS[ide](skillName);
}

export function referenceInstruction(
skillName: string,
ide: ChosenIde,
ide: IdeId,
method?: PluginInstallationMethod | null,
): string {
return method === 'cli'
? `Invoke the \`${skillInvocation(skillName, ide)}\` skill as a **methodology reference**`
: `Read \`${SKILLS_DIRS[ide]}/${skillName}/SKILL.md\` as a **methodology reference**`;
}

export function buildToolVars(ide: ChosenIde): Record<string, string> {
export function buildToolVars(ide: IdeId): Record<string, string> {
const fmt = TOOL_FORMATTERS[ide];
const flags = (tool: string) => fmt('confidence-flags', tool);
const docs = (tool: string) => fmt('confidence-docs', tool);
Expand Down
2 changes: 1 addition & 1 deletion src/integrations/chat.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IdeId } from './types.js';
import type { IdeId } from '@shared-kernel/types.js';
import type { WizardSession } from '@lib/session.js';
import { getIntegration } from './registry.js';

Expand Down
2 changes: 1 addition & 1 deletion src/integrations/claude/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { execFile as execFileCb } from 'node:child_process';
import { resolve } from 'node:path';
import { promisify } from 'node:util';
import { PLUGIN_NAME } from '@lib/constants.js';
import type { PluginInstallationMethod } from '../types.js';
import type { PluginInstallationMethod } from '@shared-kernel/types.js';
import { hasDownloadedSkills } from '../skills/local.js';
import { skillsDir } from './paths.js';

Expand Down
2 changes: 1 addition & 1 deletion src/integrations/codex/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { execFile as execFileCb } from 'node:child_process';
import { promisify } from 'node:util';
import { PLUGIN_MARKETPLACE_REPO, PLUGIN_MARKETPLACE_NAME, PLUGIN_NAME } from '@lib/constants.js';
import type { PluginInstallationMethod } from '../types.js';
import type { PluginInstallationMethod } from '@shared-kernel/types.js';
import { hasDownloadedSkills } from '../skills/local.js';
import { skillsDir } from './paths.js';

Expand Down
2 changes: 1 addition & 1 deletion src/integrations/cursor/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { execFile as execFileCb } from 'node:child_process';
import { promisify } from 'node:util';
import { PLUGIN_REPO_URL } from '@lib/constants.js';
import type { PluginInstallationMethod } from '../types.js';
import type { PluginInstallationMethod } from '@shared-kernel/types.js';
import { hasDownloadedSkills } from '../skills/local.js';
import { skillsDir } from './paths.js';

Expand Down
2 changes: 0 additions & 2 deletions src/integrations/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
export type {
IdeId,
IdeIntegration,
McpConnectOpts,
OnboardingOpts,
OnboardingCallbacks,
PluginInstallationMethod,
InstalledPlugin,
} from './types.js';

Expand Down
Loading