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
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ pnpm dev # dev mode
- `docs/internal/CLI_SPEC_v0.2.md` → v0.2 specification (COMPLETE)
- `docs/internal/CLI_SPEC_v0.2.1.md` → v0.2.1 UX polish specification (COMPLETE)
- `docs/internal/DOCS_SPEC.md` → Mintlify documentation spec (COMPLETE)
- `docs/internal/WATCH_SPEC.md` → v0.3 watch mode specification (ACTIVE — implement this)
- `docs/internal/WATCH_SPEC.md` → v0.3 watch mode specification (COMPLETE)
- `docs/internal/PULL_SPEC.md` → v0.4 Pull rules specification (IMPLEMENT THIS)
- `docs/internal/DECISIONS.md` → accepted decisions (source of truth if conflict)
- `docs/internal/` is gitignored — internal specs not published

Expand Down
49 changes: 49 additions & 0 deletions content/rules/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Official Rules

Rules for AI coding agents, distributed via `devw pull`.

## Available Rules

| Rule | Category | Description | Command |
|------|----------|-------------|---------|
| `typescript/strict` | TypeScript | Strict TypeScript conventions | `devw pull typescript/strict` |
| `javascript/react` | JavaScript | React conventions and best practices | `devw pull javascript/react` |
| `javascript/nextjs` | JavaScript | Next.js App Router patterns and RSC | `devw pull javascript/nextjs` |
| `css/tailwind` | CSS | Utility-first Tailwind conventions | `devw pull css/tailwind` |
| `testing/vitest` | Testing | Vitest testing patterns | `devw pull testing/vitest` |
| `security/supabase-rls` | Security | Supabase RLS enforcement | `devw pull security/supabase-rls` |

## Usage

```bash
# List all available rules
devw pull --list

# Pull a specific rule
devw pull typescript/strict

# Preview without writing
devw pull typescript/strict --dry-run

# Force overwrite
devw pull typescript/strict --force
```

## Rule Format

Each rule file uses YAML frontmatter and Markdown bullets:

```markdown
---
name: rule-name
description: "Short description"
version: "0.1.0"
scope: conventions
tags: [tag1, tag2]
---

## Section

- Rule text as a bullet.
Continuation indented.
```
28 changes: 28 additions & 0 deletions content/rules/css/tailwind.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
name: tailwind
description: "Utility-first Tailwind CSS conventions and design tokens"
version: "0.1.0"
scope: conventions
tags: [tailwind, css, styling]
---

## Utilities

- Use Tailwind utility classes for all styling. Do not write
custom CSS unless absolutely necessary (e.g. complex
animations or third-party overrides).

- Avoid `@apply` in CSS files. Extract reusable patterns into
React components instead of creating CSS abstractions.

- Keep className strings readable. Break long class lists across
multiple lines and group related utilities together.

## Design Tokens

- Use Tailwind's design tokens (spacing, colors, typography)
from the theme config. Avoid arbitrary values like
`w-[137px]`; prefer the closest token.

- Extend the theme in `tailwind.config` for project-specific
tokens. Do not hardcode colors or spacing outside the config.
39 changes: 39 additions & 0 deletions content/rules/javascript/nextjs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
name: nextjs
description: "Next.js App Router patterns and React Server Components"
version: "0.1.0"
scope: architecture
tags: [nextjs, react, app-router, rsc]
---

## Server Components

- Minimize `"use client"` directives. Default to Server Components.
Only add `"use client"` when the component needs browser APIs,
event handlers, or React hooks that require client state.

- Fetch data in Server Components or server actions, not in
client components with `useEffect`. Use React Suspense for
loading states.

- Keep Server Components free of side effects. Data fetching
and rendering only; mutations belong in server actions.

## Routing

- Follow the App Router file conventions: `page.tsx`, `layout.tsx`,
`loading.tsx`, `error.tsx`, `not-found.tsx`. Do not create custom
routing abstractions.

- Use route groups `(group)` to organize routes without affecting
the URL structure. Use parallel routes and intercepting routes
when needed.

## Server Actions

- Prefer server actions for form submissions and data mutations.
Define them with `"use server"` in a separate file or at the
top of an async function.

- Validate all inputs in server actions. Never trust data coming
from the client even in server-side code.
43 changes: 43 additions & 0 deletions content/rules/javascript/react.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
name: react
description: "React conventions and best practices for AI coding agents"
version: "0.1.0"
scope: conventions
tags: [react, frontend, components, hooks]
---

## Components

- Always use named exports. Never use default exports.
This applies to all files: components, utilities, hooks, and types.

- Use PascalCase for component names and their files
(`UserProfile.tsx`). Use camelCase for hook files prefixed
with `use` (`useAuth.ts`).

- Prefer composition over prop drilling. Use children,
render props, or context for shared behavior rather than
deeply nested prop chains.

- Colocate related files: component, hook, utils, and types
in the same feature folder.

## Hooks

- Follow the Rules of Hooks: only call hooks at the top level,
never inside conditions or loops. Custom hooks must start
with `use`.

- Extract complex logic into custom hooks. A component should
primarily handle rendering; business logic belongs in hooks.

- Use `useMemo` and `useCallback` only when there is a measured
performance problem. Premature memoization adds complexity.

## Styling

- Avoid inline styles. Use CSS modules, Tailwind classes,
or styled-components for styling.

- Keep className logic simple. Extract complex conditional
classes into a helper or use a utility like `clsx`.
30 changes: 30 additions & 0 deletions content/rules/security/supabase-rls.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
name: supabase-rls
description: "Supabase Row-Level Security enforcement and auth patterns"
version: "0.1.0"
scope: security
tags: [supabase, rls, security, database]
---

## RLS Policies

- Every new table must have RLS policies before merging.
Enable RLS with `ALTER TABLE ... ENABLE ROW LEVEL SECURITY`
and create at least one policy per operation (SELECT, INSERT,
UPDATE, DELETE) as needed.

- Always use `auth.uid()` in RLS policies to scope data to
the authenticated user. Never rely on client-provided
user IDs in queries.

- Test RLS policies in isolation. Write SQL tests that verify
access is denied for unauthorized users before merging.

## Auth Keys

- Never expose the `service_role` key to the client.
Use the anon key in browser code and the `service_role` key
only in server-side or admin contexts.

- Store Supabase keys in environment variables. Never hardcode
keys in source files or commit them to version control.
38 changes: 38 additions & 0 deletions content/rules/testing/vitest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
name: vitest
description: "Vitest testing patterns and best practices"
version: "0.1.0"
scope: testing
tags: [vitest, testing, unit-tests]
---

## Test Structure

- Use descriptive test names that explain the expected behavior.
Follow the pattern: `should [expected behavior] when [condition]`.

- Structure tests with Arrange-Act-Assert (AAA) pattern.
Separate setup, execution, and verification into distinct
sections for readability.

- Group related tests with `describe` blocks. One `describe`
per function or feature under test.

## Assertions

- Test behavior, not implementation details. Avoid asserting
on internal state, private methods, or specific function calls
unless testing integration points.

- Prefer specific assertions (`toEqual`, `toContain`, `toThrow`)
over generic ones (`toBeTruthy`). Specific assertions give
better failure messages.

## Mocking

- Only mock at system boundaries: network requests, databases,
file system, and third-party services. Do not mock internal
modules or utility functions.

- Use `vi.fn()` for function spies and `vi.mock()` for module
mocks. Restore all mocks after each test with `afterEach`.
37 changes: 37 additions & 0 deletions content/rules/typescript/strict.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
name: strict
description: "Strict TypeScript conventions for professional codebases"
version: "0.1.0"
scope: conventions
tags: [typescript, strict, types]
---

## Type Safety

- Never use `any`. Use `unknown` when the type is truly unknown,
then narrow with type guards.

- Always declare explicit return types on exported functions.
Inferred types are fine for internal/private functions.

- Never use non-null assertion (`!`). Handle null/undefined explicitly
with optional chaining, nullish coalescing, or type guards.

## Types and Enums

- Prefer union types over enums.
Use `as const` objects when you need runtime values.

- Prefer `interface` for object shapes that may be extended.
Use `type` for unions, intersections, and mapped types.

- Use `satisfies` to validate object literals against a type
while preserving the narrowest inferred type.

## Generics

- Name generic parameters descriptively when the meaning
is not obvious. Prefer `TItem` over `T` in complex signatures.

- Constrain generic parameters with `extends` to communicate
the expected shape and catch misuse at compile time.
8 changes: 8 additions & 0 deletions packages/cli/src/bridges/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ export interface Rule {
tags?: string[];
enabled: boolean;
sourceBlock?: string;
source?: string;
}

export interface PulledEntry {
path: string;
version: string;
pulled_at: string;
}

export interface ProjectConfig {
Expand All @@ -17,6 +24,7 @@ export interface ProjectConfig {
tools: string[];
mode: 'copy' | 'link';
blocks: string[];
pulled: PulledEntry[];
}

export interface Bridge {
Expand Down
34 changes: 32 additions & 2 deletions packages/cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { cursorBridge } from '../bridges/cursor.js';
import { geminiBridge } from '../bridges/gemini.js';
import { windsurfBridge } from '../bridges/windsurf.js';
import { copilotBridge } from '../bridges/copilot.js';
import type { Bridge, ProjectConfig, Rule } from '../bridges/types.js';
import type { Bridge, ProjectConfig, PulledEntry, Rule } from '../bridges/types.js';
import { fileExists } from '../utils/fs.js';
import { isValidScope } from '../core/schema.js';
import * as ui from '../utils/ui.js';
Expand Down Expand Up @@ -184,6 +184,32 @@ export async function checkSymlinks(cwd: string, config: ProjectConfig): Promise
return { passed: true, message: 'Symlinks are valid' };
}

export async function checkPulledFilesExist(cwd: string, pulled: PulledEntry[]): Promise<CheckResult> {
if (pulled.length === 0) {
return { passed: true, message: 'Pulled files check skipped (no pulled rules)', skipped: true };
}

const missing: string[] = [];

for (const entry of pulled) {
const slug = entry.path.replace(/\//g, '-');
const fileName = `pulled-${slug}.yml`;
const filePath = join(cwd, '.dwf', 'rules', fileName);
if (!(await fileExists(filePath))) {
missing.push(fileName);
}
}

if (missing.length > 0) {
return {
passed: false,
message: `Missing pulled rule files: ${missing.join(', ')}`,
};
}

return { passed: true, message: `Pulled rule files exist (${String(pulled.length)} entries)` };
}

export async function checkHashSync(cwd: string, rules: Rule[]): Promise<CheckResult> {
const storedHash = await readStoredHash(cwd);
if (storedHash === null) {
Expand Down Expand Up @@ -273,7 +299,11 @@ async function runDoctor(): Promise<void> {
const symlinkResult = await checkSymlinks(cwd, config!);
results.push(symlinkResult);

// Check 8: Hash sync (conditional on compiled files existing)
// Check 8: Pulled files exist
const pulledResult = await checkPulledFilesExist(cwd, config!.pulled);
results.push(pulledResult);

// Check 9: Hash sync (conditional on compiled files existing)
const hashResult = await checkHashSync(cwd, rules);
results.push(hashResult);

Expand Down
9 changes: 8 additions & 1 deletion packages/cli/src/commands/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,14 @@ async function listRules(): Promise<void> {
for (const rule of active) {
const severityIcon = rule.severity === 'error' ? chalk.red(ICONS.error) : rule.severity === 'warning' ? chalk.yellow(ICONS.warn) : chalk.dim(ICONS.dot);
const severityColor = rule.severity === 'error' ? chalk.red : rule.severity === 'warning' ? chalk.yellow : chalk.dim;
const source = rule.sourceBlock ? chalk.dim(` [${rule.sourceBlock}]`) : '';
let source = '';
if (rule.source) {
source = chalk.dim(` (pulled: ${rule.source})`);
} else if (rule.sourceBlock) {
source = chalk.dim(` [${rule.sourceBlock}]`);
} else {
source = chalk.dim(` ${ICONS.arrow} manual`);
}
console.log(` ${severityIcon} ${severityColor(rule.severity.padEnd(8))}${chalk.cyan(rule.scope.padEnd(15))}${rule.id}${source}`);
}
}
Expand Down
Loading