code

SkillFiles & storage

Guides your agent to write TypeScript and React code with proper types, component patterns, and file organization.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the code skill

About this capability

Use when writing TypeScript/React code - covers type safety, component patterns, and file organization

What this skill tells your AI

The instructions your AI receives, as published by trycompai/comp in .agents/skills/code/SKILL.md and read by ahel’s review.

Source Cursor rule: .cursor/rules/code.mdc. Original Cursor alwaysApply: false.

Code Standards

TypeScript

No any, No Unsafe Casts

// ✅ Validate with zod
const TaskSchema = z.object({ id: z.string(), title: z.string() });
const task = TaskSchema.parse(response.data);

// ✅ Use unknown and narrow
const parseResponse = (data: unknown): Task => {
  if (!isTask(data)) throw new Error('Invalid');
  return data;
};

// ❌ Never
const data: any = fetchData();
const task = response as Task;
const name = user!.name;
// @ts-ignore

Generics Over Any

// ✅ Generic
const first = <T>(items: T[]): T | undefined => items[0];

// ❌ Any
const first = (items: any[]): any => items[0];

React Patterns

Named Exports, PascalCase

// ✅ Named export, PascalCase file
// TaskCard.tsx
export function TaskCard({ task }: TaskCardProps) { ... }

// ❌ Default export, lowercase
export default function taskCard() { ... }

Derive State, Avoid useEffect

// ✅ Derived
const completedCount = tasks.filter(t => t.completed).length;

// ❌ Synced state
const [count, setCount] = useState(0);
useEffect(() => {
  setCount(tasks.filter(t => t.completed).length);
}, [tasks]);

When useEffect IS Appropriate

// External subscriptions
useEffect(() => {
  const sub = eventSource.subscribe(handler);
  return () => sub.unsubscribe();
}, []);

// DOM measurements
useEffect(() => {
  setHeight(ref.current?.getBoundingClientRect().height);
}, []);

Toasts with Sonner

import { toast } from 'sonner';

toast.success('Task created');
toast.error('Failed to save');
toast.promise(saveTask(), {
  loading: 'Saving...',
  success: 'Saved!',
  error: 'Failed',
});

File Structure

Colocate at Route Level

app/(app)/[orgId]/tasks/
├── page.tsx              # Server component
├── components/
│   └── TaskList.tsx      # Client component
├── hooks/
│   └── useTasks.ts       # SWR hook
└── data/
    └── queries.ts        # Server queries

Share Only When Reused 3+ Times

src/components/shared/    # Cross-page components
src/hooks/                # Shared hooks (useApiSWR, useDebounce)

Code Quality

File Size Limit: 300 Lines

Split large files into focused components.

Named Parameters for 2+ Args

// ✅ Named
const createTask = ({ title, assigneeId }: CreateTaskParams) => { ... };
createTask({ title: 'Review PR', assigneeId: user.id });

// ❌ Positional
const createTask = (title: string, assigneeId: string) => { ... };
createTask('Review PR', user.id); // What's the 2nd param?

Early Returns

// ✅ Early return
function processTask(task: Task | null) {
  if (!task) return null;
  if (task.deleted) return null;
  return <TaskCard task={task} />;
}

// ❌ Nested
function processTask(task) {
  if (task) {
    if (!task.deleted) {
      return <TaskCard task={task} />;
    }
  }
  return null;
}

Event Handler Naming

// ✅ Prefix with "handle"
const handleClick = () => { ... };
const handleSubmit = (e: FormEvent) => { ... };
const handleTaskCreate = (task: Task) => { ... };

Accessibility

// Interactive elements need keyboard support
<div
  role="button"
  tabIndex={0}
  onClick={handleClick}
  onKeyDown={(e) => e.key === 'Enter' && handleClick()}
  aria-label="Delete task"
>
  <TrashIcon />
</div>

// Form inputs need labels
<label htmlFor="task-name">Task Name</label>
<input id="task-name" type="text" />

Signals

GitHub stars
2k
Forks
412
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
code-trycompai
Source
github.com/trycompai/comp