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 abbreviationFile 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)
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 vagueLocation: 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// ✓ 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// ✓ 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// ✓ Good
export class WorkflowLoader { /* ... */ } // PascalCase for classes
export interface WorkflowConfig { /* ... */ }
export type RoleName = 'CONDUCTOR' | 'ENGINEER' | 'TESTER'
// ✗ Avoid
export class workflow_loader { /* ... */ }
export interface iWorkflowConfig { /* ... */ }// ✓ 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// ✓ 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)
})
}// ✓ 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
}// ✓ 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) {
// ...
}
})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)// ✓ 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// ✓ 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 { /* ... */ }// ✓ 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)
}# ✓ 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# 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// ✓ 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)// 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)// ✓ 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 = () => { /* ... */ }/**
* 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
}// ✓ 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'
)
})
})
})// ✓ 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()
})
})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')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)// ✓ 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// ✓ 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
}# ✓ Good - strict mode enabled
tsc --strict --noImplicitAny
# ✗ Avoid - loose type checking
tsc --skipLibCheck{
"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\""
}
}{
"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"
}
}- Files: kebab-case, descriptive, <200 lines
- Code: camelCase variables, PascalCase classes, modern JS/TS
- Types: Zod validation, explicit on public APIs
- Components: Functional, hooks, PascalCase names
- Workflows: YAML config with role-specific files
- Logging: Use Logger class, never console
- Comments: Explain why, not what
- Tests: Comprehensive coverage with clear structure
- Security: Validate inputs, protect secrets
- Performance: Parallel operations, streaming for large data