Architecture Guidelines
This skill defines the structural rules, domain boundaries, and constraints that govern all work on the Confidence Wizard CLI. Follow these when adding features, refactoring, or reviewing changes.
Purpose
The Confidence Wizard is a CLI tool for quickly setting up and integrating Confidence with users' projects. It works together with a Claude Code Skill backed by Confidence MCP tools (confidence-ai-plugins) — the Skill handles product knowledge, the CLI handles user interaction.
Domain Separation
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 fromindex.ts.
1. Commands (src/commands/)
CLI command definitions using yargs. Each command is a self-contained module exporting a Command object.
- Currently:
default(launches TUI) andhelp. - Commands orchestrate — they call into
src/ui/orsrc/frameworks/but never contain UI rendering or framework detection logic themselves. - New commands get their own file, are re-exported from
src/commands/index.ts.
2. Framework Integrations (src/frameworks/)
One subdirectory per supported framework (e.g. react/, nextjs/, node/). Each exports a FrameworkConfig implementing a shared interface from src/frameworks/types.ts.
- Framework configs define: id, name, docs URL, SDK package, and a
detect()function. - The registry (
src/frameworks/index.ts) exposesgetFrameworks()anddetectFramework(). - Adding a new framework means adding a new subdir with an
index.ts— no changes to other framework dirs. - Framework modules must not import from
src/ui/orsrc/commands/.
4. Providers (src/providers/)
Provider detection and configuration for competing feature flag platforms (e.g. Statsig, Eppo, PostHog, Optimizely). One subdirectory per provider, plus shared types and dependency-reading utilities.
- The registry (
src/providers/index.ts) exposesdetectProviders(). - Each provider exports a
ProviderConfigimplementing the shared interface fromsrc/providers/types.ts. - Adding a new provider means adding a new subdir — no changes to other provider dirs.
- Provider modules must not import from
src/ui/,src/commands/, orsrc/integrations/.
5. UI (src/ui/)
Terminal user interface built with Ink and React. Organized into:
tui/screens/— Every screen is organized as a slice: a subdirectory (e.g.screens/system-check/,screens/welcome/) containing the screen component, a barrelindex.ts, and collocatedlog-messages.ts,telemetry-events.ts, andactions.tsfiles for screen-specific debug log factories, telemetry event factories, and typed prompt-option constants. Slices with side-effect hooks also contain their hooks (e.g.useSystemCheck.ts). Slices that need initial state computed at mount time have a dedicateduseInitial*hook (e.g.useInitialAuth.ts) — see Initialization hooks below. Slices that need sub-components (presentational pieces too large to keep inline in the screen file) place them in a collocatedcomponents/subdirectory within the slice (e.g.screens/select-goal/components/LeftPanel.tsx). These sub-components are private to the slice — they import from sibling files within the slice and from sharedtui/modules, but must not be imported by other slices. To add a screen: create a subdir inscreens/, add the component + barrel +log-messages.ts+telemetry-events.ts+actions.ts, register inscreen-registry.tsx, add transitions inscreen-transitions.ts.tui/components/— Reusable building blocks and composites (TextBlock,Divider,ScreenContainer,KeyboardHintsBar,TitleBar, etc.). Barrel-exported.tui/styles.ts— Theme constants:Colors,Icons,HAlign,VAlign. All visual styling imports from here.tui/hooks/— Shared hooks used across screens (useStore,useRouter,useLog,useTerminalSize, etc.). Screen-specific hooks live inside their slice, not here.tui/lib/— Shared utilities and types used across the TUI (status-line.ts,wizard-tasks.ts,logger.ts,layout-budget.ts,tips.ts). Also containslog-messages.ts(sharedLogMessagetype and cross-screen helpers likeskipped()) andtelemetry-events.ts(shared telemetry factories likescreenEntered()used by theuseTelemetryhook). Screen-specific log and telemetry factories live in each screen's slice, not here.tui/store.ts— Reactive state via nanostores. All mutations through explicit setters that callemitChange().tui/router.ts— State-machine navigation viaWizardRouter. Uses aTransitionMap(screen → event → target screen) with a history stack for back navigation. Screens navigate by callingnav.to(event)ornav.back()through the type-safeuseNavigationhook.tui/screen-transitions.ts— Transition map defining all valid screen-to-screen navigation edges. Adding a branch (e.g., a warehouse sub-flow) means adding new edges here — no structural changes needed.tui/screen-registry.tsx— MapsScreenId→ React component. Single place to wire up new screens.
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/orsrc/commands/. - Each feature exports through a barrel
index.tswith 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
The TUI is a generic wizard shell. It must not contain Confidence-specific domain logic such as:
- How to create or configure feature flags
- SDK initialization code or snippets
- API interaction with Confidence services
- Migration logic from other platforms
All of this belongs in the Claude Code Skill and is delivered via Confidence MCP tools (confidence-flags, confidence-docs). The TUI only renders what the Skill provides.
Why: The CLI tool and the Skill are separate products with different release cycles. Embedding product knowledge in the TUI creates coupling that makes both harder to evolve. The Skill has access to live documentation and flag state via MCP — the TUI does not.
Dependency direction
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. 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:
screen slices → hooks/, lib/, components/
components/ → lib/, hooks/
hooks/ → lib/
lib/ → nothing in tui/
Shared modules (hooks/, lib/, components/) must never import from screen slices. If a type or utility is needed by both a slice and a shared module, it belongs in tui/lib/ — not re-exported from the slice.
Screen identification
Always use ScreenId enum values from src/lib/session.ts. Never use raw strings for screen identification or navigation.
State mutations
All session state changes go through WizardStore setters. Never mutate the session object directly — the reactivity system depends on emitChange() being called.
UI component sourcing
Use @inkjs/ui components (Select, TextInput, Spinner, ConfirmInput, MultiSelect, Badge, StatusMessage, ProgressBar) over standalone ink-* packages. The standalone packages are unmaintained and @inkjs/ui supersedes them.
Framework extensibility
Each framework gets its own subdirectory. Framework-specific logic stays inside that subdir. The shared FrameworkConfig interface in types.ts is the contract — adding a framework must not require changes to existing framework dirs.
Coding Conventions
Initialization Hooks
Slices that compute initial state at mount time (e.g. reading persisted tokens, detecting installed plugins, checking the filesystem) use a dedicated useInitial* hook collocated in the same slice directory. This separates the one-time "resolve initial state + sync side effects to the store" concern from the ongoing interaction logic in the main hook.
Each init hook follows the same shape:
- A pure
resolve*function (defined outside the hook) that computes initial values fromdryRunand session data — called once viauseState(() => resolve*(...)). - A
useEffectthat syncs any store side effects (e.g.store.setAuthState,store.setInstalledPlugins) derived from the resolved values. - Returns the resolved values for the parent hook to seed its own
useStatecalls.
The parent hook consumes it as const initial = useInitial*() and initializes its own state: useState<Phase>(initial.phase).
Async variant. When resolution requires async work (e.g. shelling out to a CLI), seed useState with a pending phase and run the async operation in a useEffect named resolveInitial*. The parent hook overlays its own phase: installPhase ?? initial.phase.
Existing init hooks:
useInitialAuth— resolves persisted credentials, syncs auth state to storeuseInitialDetection— async; detects installed plugins via CLI, syncs to storeuseInitialOnboarding— resolves project emptiness / framework, syncs empty-project flaguseInitialMcpDetection— resolves dry-run phase for MCP detectionuseInitialSystemCheck— resolves dry-run checks, syncs check results to store
Dry Run Separation
Hooks that support dry-run mode must keep dry-run logic in a separate function from the real implementation. Never interleave them with conditionals, ternaries, or early returns inside a shared function body.
Pattern — the dispatch function checks dryRun and delegates:
function startAuth(mode: 'signup' | 'login') {
setPhase('waiting-browser');
// shared setup...
if ($session.get().dryRun) return startDryRunAuth();
startRealAuth(mode);
}
function startDryRunAuth() {
// simulated behavior only
}
function startRealAuth(mode: 'signup' | 'login') {
// real implementation only
}
When the dispatch function is inside useCallback, define the dry-run and real functions as nested function declarations within the callback body:
const start = useCallback(
function start(fwName: string | null) {
// shared setup...
if (s.dryRun) return startDryRun();
startReal();
function startDryRun() {
/* ... */
}
function startReal() {
/* ... */
}
},
[deps],
);
Init hooks (useInitial*) handle the dry-run initial state separately via the resolve* function — see Initialization Hooks. Effects that only need to skip real work in dry-run mode (e.g. if (session.dryRun) return; before runAllChecks()) are fine as simple guards when the init hook already set up the dry-run state.
React Hooks Rules
The project uses eslint-plugin-react-hooks with the recommended-latest rule set (React Compiler rules). All rules are set to error — no warnings. Key rules enforced:
rules-of-hooks— Hooks must be called at the top level. Functions returned from hooks must not start withuse(the linter treats them as hooks and flags calls in callbacks as violations).exhaustive-deps— All values referenced insideuseEffect/useMemo/useCallbackmust appear in the dependency array. Wrap unstable functions inuseCallbackwhen they're needed as effect deps.set-state-in-effect— Never callsetStatesynchronously in an effect body. For mount-time initialization, use lazyuseState(() => computeInitial())and theuseInitial*hook pattern instead. Async callbacks (.then(), event handlers) inside effects are fine.immutability— Never reference a function or variable before its declaration inside a hook body. Either move the declaration above the call site, inline it into the effect, or useuseCallback.
Guarding Redundant State Updates
When syncing derived React state to the store (or vice versa), always compare values before calling a store setter. Store setters call emitChange(), which can trigger re-renders through subscriptions. Without a guard, setting the same value repeatedly creates render loops — especially when Ink re-mounts components that re-process buffered input.
Pattern:
useEffect(
function syncToStore() {
const current = store.session.someField;
if (derived.length === current.length && derived.every((v, i) => v === current[i])) return;
store.setSomeField(derived);
},
[derived, store],
);
Similarly, applyStatuses-style functions that merge new data into existing state should bail out when nothing actually changed:
function applyStatuses(updated: Record<string, Status>) {
if (Object.entries(updated).every(([k, v]) => current[k] === v)) return;
// ...proceed with setState
}
Path Aliases
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.
// Cross-domain — use alias
import { ScreenId } from '@lib/session.js';
import { getIntegration } from '@integrations/index.js';
import { detectFramework } from '@frameworks/index.js';
// Within-domain — use relative
import { store } from '../../store.js';
import { Colors } from '../styles.js';
Test files also use aliases for imports from src/:
import { WelcomeScreen } from '@ui/tui/screens/welcome/index.js';
import { ScreenId } from '@lib/session.js';
TypeScript Style
- Sort imports by groups: system (e.g., node:*), react, other external deps, aliases (
@lib/,@integrations/, etc.), neighboring modules. - Use
typeinstead ofinterfacefor all type definitions. - Use latest TypeScript syntax:
satisfies,using, etc. - When a function has more than 3 parameters, group them into a single object parameter instead of listing them as positional arguments.
- In
useEffect, use named functions instead of anonymous lambdas for the effect callback. - Prefer
AbortControllerfor removing event listeners instead of manually callingremoveEventListener. Pass{ signal: controller.signal }toaddEventListenerand callcontroller.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
useRefonly 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.
IdeId | 'skip'), define a named type in the slice'sactions.tsand reference it — don't write inline unions likevalue: SomeType | 'extra'in function signatures. Composition is fine (type DetectedSelectValue = IdeSelectValue | 'continue'), but it must be named and exported fromactions.ts. - In
switchstatements, thedefaultcase must use an exhaustive check viasatisfies neverto catch unhandled variants at compile time:default: { const _exhaustive: never = value satisfies never; throw new Error(`Unhandled: ${_exhaustive}`); }
Module Exports
- Keep the public API surface of each module compact. Only export what is intended to be consumed by other modules.
- Functions, types, and constants that are internal to a module must not be exported — leave them unexported so the boundary is clear.
- Barrel files (
index.ts) re-export only the public API; they are not a place to dump everything the module contains.
Linting
- Strict linting — all rules are errors, never warnings.
- Fix lint errors immediately, do not suppress or downgrade them.
- ESLint config includes
eslint-plugin-react-hooks(recommended-latestflat config) with all warning-level rules upgraded to errors. See React Hooks Rules above for the key rules.
No Warning Suppression
Never suppress, silence, or filter runtime warnings (e.g. --no-warnings, --disable-warning, NODE_NO_WARNINGS) or compiler/linter diagnostics without explicit consent from the developers. Warnings exist to surface real issues — fix the root cause instead of hiding the symptom.
Adding New Concerns
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/ - Cross-domain vocabulary types in
src/shared-kernel/types.ts; domain-specific types in a localtypes.ts - Exported through a barrel
index.ts - No circular dependencies with existing domains
- Documented in this skill and CLAUDE.md
Confidence MCP Tools
The wizard works alongside two Confidence MCP servers from confidence-ai-plugins:
confidence-flags— Feature flag management: create, list, resolve, target, archive flags; manage clients, metrics, context schema, and warehouses.confidence-docs— Documentation access: search docs, get SDK integration guides, code snippets, and deployment guides.
These are accessed via the Claude Code Skill, never directly from the TUI or CLI code.