Skip to content

Latest commit

 

History

History
694 lines (546 loc) · 15.9 KB

File metadata and controls

694 lines (546 loc) · 15.9 KB

Claude Team - Code Standards

File Organization

TypeScript Files

Location: src/{cli,core,types,utils}/

Naming Convention: kebab-case, descriptive names

// ✓ Good
src/cli/ct-send.ts              // Clear purpose
src/core/tmux-manager.ts        // Clear responsibility
src/utils/session-storage.ts    // Clear functionality

// ✗ Avoid
src/cli/send.ts                 // Too vague
src/core/tm.ts                  // Unclear abbreviation
src/utils/ss.ts                 // Unreadable abbreviation

File Size: Target <200 lines per file

  • CLI files: 100-200 lines (single command)
  • Core modules: 100-250 lines (single responsibility)
  • Type files: 50-150 lines (schemas/types only)
  • Utils: 50-100 lines (single utility)

React Components

Location: dashboard/components/

Naming Convention: PascalCase, descriptive names

// ✓ Good
dashboard/components/chat-message.tsx
dashboard/components/voice-recording-modal.tsx
dashboard/hooks/use-speech-recognition.ts
dashboard/lib/speech/soniox-provider.ts

// ✗ Avoid
dashboard/components/Message.tsx      // Not descriptive
dashboard/hooks/useVoice.ts           // Too vague

Workflow Files

Location: workflows/{workflow-name}/

Naming Convention: kebab-case, role names uppercase

workflows/basic/
├── config.yaml                  # Workflow configuration
├── workflow.md                  # Instructions
├── role_conductor.yaml         # CONDUCTOR prompts
├── role_engineer.yaml          # ENGINEER prompts
└── role_tester.yaml            # TESTER prompts

workflows/duo/
├── config.yaml
├── workflow.md
├── role_lead.yaml
└── role_dev.yaml

Code Style

TypeScript Conventions

Imports

// ✓ Good
import { Command } from 'commander'
import { WorkflowLoader, WorkflowLauncher } from '../core/workflow.js'
import { Logger } from '../core/logger.js'
import { getProjectDir } from '../utils/env.js'
import { readFileSync } from 'fs'

// Organize imports:
// 1. External packages
// 2. Local modules (..)
// 3. Relative modules (.)
// 4. Built-in modules

Variables & Constants

// ✓ Good
const projectDir = getProjectDir()        // camelCase for variables
const DEFAULT_TIMEOUT = 5000              // UPPER_SNAKE_CASE for constants
let currentRole: string | null = null

// ✗ Avoid
const ProjectDir = getProjectDir()        // PascalCase for variables
const default_timeout = 5000              // snake_case for constants
var sessionName                           // Use const/let, not var

Classes & Types

// ✓ Good
export class WorkflowLoader { /* ... */ } // PascalCase for classes
export interface WorkflowConfig { /* ... */ }
export type RoleName = 'CONDUCTOR' | 'ENGINEER' | 'TESTER'

// ✗ Avoid
export class workflow_loader { /* ... */ }
export interface iWorkflowConfig { /* ... */ }

Functions

// ✓ Good
export async function launchWorkflow(config: WorkflowConfig): Promise<void> {
  // Implementation
}

export function buildClaudeCommand(role: string): string {
  // Implementation
}

// ✗ Avoid
export async function LaunchWorkflow() { /* ... */ } // PascalCase for functions
export function build_claude_command() { /* ... */ } // snake_case for functions

Async/Await Pattern

// ✓ Good
async function startWorkflow(config: WorkflowConfig): Promise<void> {
  try {
    const launcher = new WorkflowLauncher()
    await launcher.launchWorkflow(config)
    logger.info('Workflow started')
  } catch (error) {
    logger.error('Failed to start workflow', error)
    throw error
  }
}

// ✗ Avoid - using callbacks
function startWorkflow(config, callback) {
  new WorkflowLauncher().launchWorkflow(config, (err, result) => {
    if (err) callback(err)
    else callback(null, result)
  })
}

Error Handling

// ✓ Good
try {
  const config = loadWorkflowConfig(path)
  validateWorkflow(config)
} catch (error) {
  if (error instanceof ValidationError) {
    logger.error('Invalid workflow config', error)
    process.exit(1)
  } else if (error instanceof FileNotFoundError) {
    logger.error(`Workflow not found: ${path}`, error)
    process.exit(1)
  } else {
    throw error // Re-throw unknown errors
  }
}

// ✗ Avoid
try {
  const config = loadWorkflowConfig(path)
} catch (error) {
  console.log('Error:', error)  // Use logger, not console
}

// ✗ Avoid - empty catch
try {
  loadWorkflow(path)
} catch (error) {
  // Silently ignore
}

Array & Object Methods

// ✓ Good - use modern methods
const roleNames = roles.map(r => r.name)
const activeRoles = roles.filter(r => r.active)
const roleMap = Object.fromEntries(roles.map(r => [r.name, r]))

const user = users.find(u => u.id === userId)
if (user) {
  // Use optional chaining
  const profile = user?.profile?.name
}

// ✓ Good - use nullish coalescing
const model = config.model ?? 'sonnet'

// ✗ Avoid - for loops
for (let i = 0; i < roles.length; i++) {
  const role = roles[i]
  // ...
}

// ✗ Avoid - forEach for control flow
roles.forEach(role => {
  if (role.active) {
    // ...
  }
})

Type Safety

Zod Schemas

Location: src/types/workflow.ts, src/types/config.ts

import { z } from 'zod'

// ✓ Good
export const RoleSchema = z.object({
  name: z.string().min(1),
  model: z.enum(['opus', 'sonnet', 'haiku']),
  panePercent: z.number().min(1).max(100).optional(),
  settingsProfile: z.string().optional()
})

export const WorkflowConfigSchema = z.object({
  name: z.string().min(1),
  description: z.string().optional(),
  roles: z.array(RoleSchema).min(1),
  session: SessionConfigSchema.optional(),
  monitor: MonitorConfigSchema.optional()
})

export type Role = z.infer<typeof RoleSchema>
export type WorkflowConfig = z.infer<typeof WorkflowConfigSchema>

// Usage
const config = WorkflowConfigSchema.parse(data)

Type Annotations

// ✓ Good - explicit types for public APIs
export function loadWorkflow(path: string): Promise<WorkflowConfig> {
  // ...
}

export class ConfigManager {
  public loadConfig(path: string): Config { /* ... */ }
  private mergeSettings(base: Config, user: Config): Config { /* ... */ }
}

// ✓ Good - infer types in simple cases
const roles = config.roles // Type inferred from WorkflowConfig
const selectedRole = roles.find(r => r.name === roleName) // Optional<Role>

// ✗ Avoid - missing types on public APIs
export function loadWorkflow(path) { /* ... */ }

// ✗ Avoid - overly explicit in simple cases
const roles: Array<Role> = config.roles

Component Structure (React)

Functional Components

// ✓ Good
import { FC, useState } from 'react'

interface ChatMessageProps {
  role: 'user' | 'assistant'
  content: string
  timestamp?: Date
}

export const ChatMessage: FC<ChatMessageProps> = ({ role, content, timestamp }) => {
  return (
    <div className={`message message-${role}`}>
      <div className="content">{content}</div>
      {timestamp && <div className="timestamp">{timestamp.toLocaleTimeString()}</div>}
    </div>
  )
}

// ✗ Avoid - default exports
export default ChatMessage

// ✗ Avoid - class components
export class ChatMessage extends React.Component { /* ... */ }

Custom Hooks

// ✓ Good
export function useVoiceMode() {
  const [isEnabled, setIsEnabled] = useState(false)
  const [isListening, setIsListening] = useState(false)

  const toggleVoiceMode = useCallback(() => {
    setIsEnabled(!isEnabled)
  }, [isEnabled])

  return {
    isEnabled,
    isListening,
    toggleVoiceMode,
    setIsListening
  }
}

// ✗ Avoid - returning useState directly
export function useVoiceMode() {
  return useState(false)
}

YAML Workflow Configuration

config.yaml Structure

# ✓ Good
name: basic
description: "3-agent workflow with CONDUCTOR, ENGINEER, TESTER"

roles:
  - name: CONDUCTOR
    model: opus
    settingsProfile: conductor
    panePercent: 30

  - name: ENGINEER
    model: sonnet
    settingsProfile: engineer
    panePercent: 40

  - name: TESTER
    model: sonnet
    settingsProfile: tester
    panePercent: 30

session:
  layoutType: horizontal
  pingTarget: CONDUCTOR

monitor:
  file: docs/KANBAN.md
  name: KANBAN
  staleThreshold: 5

Role YAML Structure

# workflows/basic/role_conductor.yaml
# ✓ Good

name: CONDUCTOR
model: opus
description: "Workflow coordinator and task manager"

responsibilities:
  - Plan and break down requirements
  - Assign tasks to ENGINEER
  - Review ENGINEER's work
  - Provide feedback and adjustments
  - Coordinate with TESTER
  - Report progress

communication_protocol:
  - Receive requirements from user
  - Create task breakdown
  - Send tasks to ENGINEER via ct-send
  - Monitor progress via KANBAN
  - Send review feedback to ENGINEER
  - Report status to user

success_criteria:
  - All requirements understood
  - Clear task breakdown created
  - ENGINEER receives clear assignments
  - Quality feedback provided

task_priorities:
  - P1: Requirements gathering
  - P2: Task breakdown
  - P3: Assignment and coordination
  - P4: Review and feedback

Logging Standards

Logger Usage

// ✓ Good
import { Logger } from '../core/logger.js'

const logger = new Logger('WorkflowLauncher')

export class WorkflowLauncher {
  async launchWorkflow(config: WorkflowConfig): Promise<void> {
    logger.info('Launching workflow', { name: config.name })

    try {
      await this.createSession(config)
      logger.info('Workflow launched successfully', { sessionName: this.sessionName })
    } catch (error) {
      logger.error('Failed to launch workflow', error)
      throw error
    }
  }
}

// ✗ Avoid - using console
console.log('Launching workflow')
console.error('Error:', error)

Log Levels

// Use info for important events
logger.info('Workflow started', { name, roles: roleCount })

// Use debug for diagnostic info
logger.debug('Loading config file', { path })

// Use warn for recoverable issues
logger.warn('Config file not found, using defaults', { path })

// Use error for failures
logger.error('Failed to launch agent', error)

Comments & Documentation

When to Write Comments

// ✓ Good - explain why, not what
export function buildClaudeCommand(role: string, settings: RoleSettings): string {
  // Use CLAUDE_ROLE to isolate agent state in tmux pane
  const env = `CLAUDE_ROLE=${role}`

  // Apply settings profile to inject role-specific behavior
  const profile = settings.profile ? `--profile ${settings.profile}` : ''

  return `${env} claude ${profile}`
}

// ✗ Avoid - obvious comments
const roles = config.roles  // Get roles from config
const name = role.name      // Get role name

// ✗ Avoid - commented-out code
// const oldWay = () => { /* ... */ }
// const newWay = () => { /* ... */ }

Documentation Comments

/**
 * Load and validate workflow configuration from YAML file.
 *
 * @param workflowPath - Path to workflow directory
 * @returns Parsed and validated WorkflowConfig
 * @throws ValidationError if config is invalid
 * @throws FileNotFoundError if file doesn't exist
 */
export async function loadWorkflow(workflowPath: string): Promise<WorkflowConfig> {
  // Implementation
}

/**
 * Launch workflow in tmux session.
 *
 * Creates tmux session with panes for each role and starts Claude
 * in each pane with role-specific prompts.
 *
 * @param config - Workflow configuration
 * @param mode - 'new' to create session, 'resume' to reuse existing
 * @throws SessionExistsError if mode is 'new' and session already exists
 */
export async function launchWorkflow(
  config: WorkflowConfig,
  mode: 'new' | 'resume' = 'new'
): Promise<void> {
  // Implementation
}

Testing Standards

Unit Tests

// ✓ Good structure
describe('WorkflowLoader', () => {
  let loader: WorkflowLoader

  beforeEach(() => {
    loader = new WorkflowLoader()
  })

  describe('loadWorkflow', () => {
    it('should load valid workflow config', async () => {
      const config = await loader.loadWorkflow('workflows/basic')
      expect(config.name).toBe('basic')
      expect(config.roles).toHaveLength(3)
    })

    it('should throw on missing file', async () => {
      await expect(loader.loadWorkflow('workflows/nonexistent')).rejects.toThrow(
        'Workflow not found'
      )
    })

    it('should throw on invalid config', async () => {
      await expect(loader.loadWorkflow('workflows/invalid')).rejects.toThrow(
        'Invalid workflow config'
      )
    })
  })
})

Integration Tests

// ✓ Good - test real behavior
describe('Workflow Integration', () => {
  it('should launch workflow and create session', async () => {
    const launcher = new WorkflowLauncher()
    await launcher.launchWorkflow(basicConfig)

    const exists = tmux.sessionExists('test_session')
    expect(exists).toBe(true)

    // Cleanup
    await launcher.killWorkflow()
  })
})

Performance Guidelines

Command Startup Time

Target: <2 seconds from ct --start to agents initialized

// ✓ Good - parallel operations
Promise.all([
  launchRole('CONDUCTOR'),
  launchRole('ENGINEER'),
  launchRole('TESTER')
])

// ✗ Avoid - sequential when parallel possible
await launchRole('CONDUCTOR')
await launchRole('ENGINEER')
await launchRole('TESTER')

Memory Usage

Target: <100 MB per CLI process, <100 MB per agent

// ✓ Good - stream large data
const stream = fs.createReadStream('large-file.txt')
stream.on('data', chunk => {
  processChunk(chunk)
})

// ✗ Avoid - load entire file
const content = readFileSync('large-file.txt', 'utf-8')
processContent(content)

Security Standards

Environment Handling

// ✓ Good - never log sensitive data
logger.info('Connected to API', { host: apiHost })
// Don't log: { apiKey, password, token }

// ✓ Good - use environment variables
const apiKey = process.env.SONIOX_API_KEY
if (!apiKey) throw new Error('Missing SONIOX_API_KEY')

// ✗ Avoid - hardcoded credentials
const apiKey = 'sk-1234567890'

// ✗ Avoid - printing environment
logger.info('Env vars', process.env) // Might include secrets

Input Validation

// ✓ Good - validate all inputs
export function sendMessage(role: string, message: string): void {
  if (!role || typeof role !== 'string') {
    throw new Error('Invalid role')
  }
  if (!message || typeof message !== 'string') {
    throw new Error('Invalid message')
  }
  // Process message
}

// ✗ Avoid - trust user input
export function sendMessage(role, message) {
  tmux.sendCommand(role, message) // Might be dangerous
}

Build & Distribution

TypeScript Compilation

# ✓ Good - strict mode enabled
tsc --strict --noImplicitAny

# ✗ Avoid - loose type checking
tsc --skipLibCheck

Package Scripts

{
  "scripts": {
    "build": "tsc && chmod +x dist/cli/*.js",
    "dev": "tsx watch src/cli/ct.ts",
    "type-check": "tsc --noEmit",
    "format": "prettier --write \"src/**/*.ts\"",
    "format:check": "prettier --check \"src/**/*.ts\""
  }
}

Distribution Files

{
  "files": ["dist", "workflows", ".claude", "CLAUDE.md"],
  "bin": {
    "ct": "./dist/cli/ct.js",
    "ct-send": "./dist/cli/ct-send.js",
    "ct-list": "./dist/cli/ct-list.js",
    "ct-capture": "./dist/cli/ct-capture.js",
    "ct-reminder": "./dist/cli/ct-reminder.js",
    "ct-save-session": "./dist/cli/ct-save-session.js"
  }
}

Summary

  1. Files: kebab-case, descriptive, <200 lines
  2. Code: camelCase variables, PascalCase classes, modern JS/TS
  3. Types: Zod validation, explicit on public APIs
  4. Components: Functional, hooks, PascalCase names
  5. Workflows: YAML config with role-specific files
  6. Logging: Use Logger class, never console
  7. Comments: Explain why, not what
  8. Tests: Comprehensive coverage with clear structure
  9. Security: Validate inputs, protect secrets
  10. Performance: Parallel operations, streaming for large data