# Coding Conventions

**Analysis Date:** 2026-02-09

## Naming Patterns

**Files:**
- Source files: camelCase with `.ts` extension — `clientFactory.ts`, `hybridSearch.ts`, `vectorStore.ts`
- React/Ink components: PascalCase with `.tsx` extension (in `src/frontends/tui/components/`)
- Test files: co-located with source, suffixed `.test.ts` — `agent.test.ts` beside `agent.ts`
- Type-only files: `types.ts` in each module directory
- Index barrel files: `index.ts` in each module directory

**Classes:**
- PascalCase: `NativeAgent`, `ModelRouter`, `ToolRegistry`, `ChannelRegistry`, `SessionStore`
- Implementation classes use `implements` with interface: `class AnthropicClient implements ModelClient`

**Interfaces/Types:**
- PascalCase: `ChatRequest`, `ChatResponse`, `ToolResult`, `InboundMessage`
- Config interfaces suffixed with `Config`: `NativeAgentConfig`, `OrchestratorConfig`, `AnthropicClientConfig`
- Union types use `type` keyword: `type ConversationMessage = Message | ToolMessage`
- Zod-inferred types: `type Config = z.infer<typeof configSchema>` in `src/config/schema.ts`

**Functions:**
- camelCase: `loadConfig()`, `expandEnvVars()`, `createClientFromConfig()`
- Factory functions prefixed with `create`: `createMemoryReadTool()`, `createWebSearchTool()`, `createBrowserTools()`
- Boolean checks prefixed with `is`/`has`/`should`: `isSupportedImage()`, `hasImages()`, `shouldCompact()`

**Variables/Fields:**
- camelCase for public: `modelClient`, `systemPrompt`, `toolRegistry`
- Underscore prefix for private fields: `_config`, `_dirtyNamespaces`, `_totalUsage`, `_callCount`
- Constants: UPPER_SNAKE_CASE: `FETCH_TIMEOUT_MS`, `MAX_RESULTS`, `DEFAULT_RETRY_CONFIG`, `MODEL_COSTS_PER_MILLION`

**Tool Names:**
- Dot-separated namespacing: `shell.exec`, `file.read`, `file.write`, `memory.read`, `web.fetch`
- MCP tools use colon separator: `mcp:filesystem:read_file`
- Tool groups prefixed with `group:`: `group:fs`, `group:runtime`, `group:web`, `group:memory`

## Code Style

**Formatting:**
- No Prettier or formatter config detected — formatting is enforced by convention
- 2-space indentation throughout
- Single quotes for strings
- Trailing commas in multiline structures
- Semicolons always used

**Linting:**
- ESLint v9+ configured (via `eslint` devDependency), no config file found — likely uses flat config defaults
- Run with: `pnpm lint` (runs `eslint src/`)

**TypeScript Strictness:**
- `strict: true` in `tsconfig.json`
- Target: ES2022, Module: NodeNext, ModuleResolution: NodeNext
- JSX: react-jsx (for Ink TUI components)
- All declarations, declaration maps, and source maps enabled

## Import Organization

**Order:**
1. Node.js stdlib: `import { readFileSync } from 'fs';`
2. Third-party packages: `import Anthropic from '@anthropic-ai/sdk';`
3. Local imports: `import { configSchema } from './schema.js';`

**Path Style:**
- Always use `.js` extensions for local imports (NodeNext resolution):
  ```typescript
  import { NativeAgent } from './agent.js';
  import type { Config } from '../config/schema.js';
  ```

**Type-only Imports:**
- Use `import type` for type-only imports:
  ```typescript
  import type { ModelClient, ChatResponse } from '../../models/types.js';
  import type { Tool, ToolResult } from '../../tools/types.js';
  ```

**Barrel Files:**
- Every module has an `index.ts` that re-exports public API
- Use explicit named exports, not `export *`:
  ```typescript
  // src/tools/index.ts
  export type { Tool, ToolCall, ToolResult } from './types.js';
  export { ToolRegistry } from './registry.js';
  export { ToolExecutor } from './executor.js';
  ```
- Types re-exported with `export type`:
  ```typescript
  export type { AnthropicToolDef, OpenAIToolDef } from './registry.js';
  ```

## Tool Patterns

Flynn tools follow three distinct patterns. Use the appropriate one:

**Static Tool (no dependencies):**
```typescript
// src/tools/builtin/shell.ts
import type { Tool, ToolResult } from '../types.js';

interface ShellExecArgs {
  command: string;
  cwd?: string;
}

export const shellExecTool: Tool = {
  name: 'shell.exec',
  description: 'Execute a shell command...',
  inputSchema: {
    type: 'object',
    properties: {
      command: { type: 'string', description: 'The shell command to execute' },
    },
    required: ['command'],
  },
  execute: async (rawArgs: unknown): Promise<ToolResult> => {
    const args = rawArgs as ShellExecArgs;
    // implementation
  },
};
```

**Factory Tool (single tool needing dependency injection):**
```typescript
// src/tools/builtin/memory-read.ts
export function createMemoryReadTool(store: MemoryStore): Tool {
  return {
    name: 'memory.read',
    description: '...',
    inputSchema: { ... },
    execute: async (rawArgs: unknown): Promise<ToolResult> => {
      const args = rawArgs as MemoryReadArgs;
      // uses `store` from closure
    },
  };
}
```

**Multi-Factory (related tool set):**
```typescript
// src/tools/builtin/index.ts
export function createMemoryTools(store: MemoryStore, hybridSearch?: HybridSearch): Tool[] {
  return [
    createMemoryReadTool(store),
    createMemoryWriteTool(store),
    createMemorySearchTool(store, hybridSearch),
  ];
}
```

**Registration chain:**
1. Tool file (e.g., `src/tools/builtin/shell.ts`)
2. `src/tools/builtin/index.ts` (barrel exports + `allBuiltinTools` array)
3. `src/tools/index.ts` (barrel re-exports)
4. Registered in `src/daemon/index.ts` via `ToolRegistry.register()`

## Error Handling

**Pattern 1: Return ToolResult with error field (tools):**
```typescript
try {
  const content = store.read(args.namespace);
  return { success: true, output: content };
} catch (error) {
  return {
    success: false,
    output: '',
    error: error instanceof Error ? error.message : String(error),
  };
}
```

**Pattern 2: Throw with descriptive message (config/setup):**
```typescript
if (envValue === undefined) {
  throw new Error(`Environment variable ${envVar} is not set`);
}
```

**Pattern 3: Throw for duplicate registration (registries):**
```typescript
if (this.tools.has(tool.name)) {
  throw new Error(`Tool '${tool.name}' is already registered`);
}
```

**Pattern 4: Yield error events in streams:**
```typescript
try {
  for await (const event of stream) {
    yield { type: 'content', content: event.delta.text };
  }
} catch (error) {
  yield {
    type: 'error',
    error: error instanceof Error ? error : new Error(String(error)),
  };
}
```

**Pattern 5: Fire-and-forget with error logging (channels):**
```typescript
this.messageHandler(msg, reply).catch((err: unknown) => {
  console.error(`Error handling message from '${msg.channel}':`, err);
});
```

**Pattern 6: Promise.allSettled for non-critical failures:**
```typescript
const results = await Promise.allSettled(adapters.map((a) => a.connect()));
for (const [i, result] of results.entries()) {
  if (result.status === 'rejected') {
    console.error(`Failed to start channel '${adapters[i].name}':`, result.reason);
  }
}
```

**instanceof check pattern:** Always use `error instanceof Error ? error.message : String(error)` for unknown errors.

## Logging

**Framework:** `console` (no logging library)

**Patterns:**
- `console.log()` for informational messages: startup, config loaded
- `console.warn()` for non-fatal issues: missing handler, unknown channel
- `console.error()` for failures: failed connections, adapter errors
- No structured logging — messages are plain strings with context

## Comments

**JSDoc-style comments on interfaces:**
```typescript
/** Media attachment received from or sent to a channel. */
export interface Attachment {
  /** MIME type (e.g. "image/jpeg", "audio/ogg", "application/pdf"). */
  mimeType: string;
  /** Base64-encoded data (preferred for model APIs). */
  data?: string;
}
```

**Section dividers in larger files:**
```typescript
// ── Public types ──────────────────────────────────────────────────────
// ── Helpers ─────────────────────────────────────────────────────────
```

**Inline comments for non-obvious logic:**
```typescript
// Policy check (defense in depth — tools should also be filtered at listing time)
// Fire and forget — errors are logged, not propagated
```

**When to comment:**
- Always add JSDoc on exported interfaces and their fields
- Use section dividers in files > 100 lines
- Add inline comments for non-obvious behavior, workarounds, or design decisions
- No comments on self-explanatory code

## Function Design

**Size:** Functions generally stay under 50 lines. Complex logic is extracted into private methods (e.g., `toolLoop()`, `singleTurn()` in `NativeAgent`).

**Parameters:**
- Config objects for constructors with >2 params: `NativeAgentConfig`, `OrchestratorConfig`
- Raw args typed as `unknown`, then cast internally: `const args = rawArgs as ShellExecArgs`
- Optional params use `?` with defaults via `??`: `config.maxTokens ?? 4096`

**Return Values:**
- Async functions return `Promise<T>`
- Tools return `Promise<ToolResult>` with `{ success, output, error? }`
- Streams use `AsyncIterable<ChatStreamEvent>`
- Model clients always return `ChatResponse` with required `content`, `stopReason`, `usage`

## Module Design

**Exports:** Named exports only — no default exports anywhere in the codebase.

**Barrel Files:** Every module directory has `index.ts`. Import from barrel in consuming code:
```typescript
import { ToolRegistry, ToolExecutor, ToolPolicy } from '../../tools/index.js';
```

**Config validation:** Use Zod schemas in `src/config/schema.ts`. All config types are inferred from schemas:
```typescript
export const configSchema = z.object({ ... });
export type Config = z.infer<typeof configSchema>;
```

---

*Convention analysis: 2026-02-09*
