Update project configuration and add agent-ops skills
Some checks failed
ci / test (push) Has been cancelled

This commit is contained in:
toki 2026-03-21 13:05:45 +09:00
parent cb659b1b11
commit f013531d65
21 changed files with 639 additions and 391 deletions

3
.kilocode/rules/rules.md Normal file
View file

@ -0,0 +1,3 @@
# 필수 준수 사항
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.

View file

@ -0,0 +1 @@
-

View file

@ -0,0 +1 @@
-

View file

@ -0,0 +1 @@
-

View file

@ -0,0 +1 @@
-

View file

@ -0,0 +1 @@
-

View file

@ -0,0 +1 @@
-

3
AGENTS.md Normal file
View file

@ -0,0 +1,3 @@
# 필수 준수 사항
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.

209
Agent-Ops-SKILL.md Normal file
View file

@ -0,0 +1,209 @@
---
name: scaffold-agent-ops
description: 프로젝트 상태를 판별하고 에이전트 진입 파일과 agent-ops 기본 스캐폴드를 생성하기 위한 초기 규칙
version: 1.0.0
---
# 목적
이 스킬은 프로젝트에 Agent-Ops 구조가 없거나 불완전할 때,
현재 프로젝트 상태를 분석하여 다음을 세팅한다.
1. 에이전트 진입 파일
2. agent-ops 기본 폴더 구조
3. global rule
4. domain rule 템플릿
5. skill router
6. skill 템플릿
7. 초기 domain / skill 세팅
이 스킬은 과도한 초기 생성보다
"현재 프로젝트에 맞는 최소 스캐폴드" 생성을 우선한다.
# 핵심 원칙
- 기존 구조를 우선한다.
- 새 파일 생성은 꼭 필요한 최소 범위로 제한한다.
- 에이전트별 md는 얇은 진입점으로 둔다.
- 실제 규칙은 agent-ops 아래로 모은다.
- 도메인은 발명하지 말고 현재 구조에서 발견한다.
- 처음에는 핵심 도메인만 생성한다.
- 반복되는 작업만 초기 skill로 만든다.
- 불확실한 내용은 후보로 제시한다.
# 상태 판별
## 신규 프로젝트
다음 조건이 많으면 신규 프로젝트로 본다.
- 코드/폴더 구조가 단순하다
- 도메인 경계가 아직 약하다
- Agent-Ops 관련 파일이 없다
- 반복 작업 패턴이 아직 적다
## 운영중 프로젝트
다음 조건이 많으면 운영중 프로젝트로 본다.
- 모듈/폴더/패키지 경계가 보인다
- 반복 작업이 드러난다
- 기존 규칙/문서/관례의 흔적이 있다
- 핵심 책임 경계가 식별된다
# 생성 대상
이 스킬은 아래 구조를 기준으로 scaffold를 제안하거나 생성한다.
- CLAUDE.md
- GEMINI.md
- .cursorrules
- agent-ops/rules/global.md
- agent-ops/rules/_templates/domain-rule-template.md
- agent-ops/rules/domain/<domain>/rules.md
- agent-ops/skills/router.md
- agent-ops/skills/_templates/skill-template.md
필요 시 아래 초기 skill도 함께 제안한다.
- agent-ops/skills/find-domain/SKILL.md
- agent-ops/skills/code-change/SKILL.md
# 에이전트 진입 파일 원칙
CLAUDE.md, GEMINI.md, .cursorrules 는 공통적으로 얇게 유지한다.
각 파일에는 아래만 둔다.
- 응답 언어
- 기존 구조 우선
- 새 파일 생성보다 기존 수정 우선
- 작업 전 agent-ops/skills/router.md 참조
- 관련 rule / skill 만 최소 로드
- 세부 규칙은 agent-ops 아래 파일을 따른다
각 에이전트 진입 파일에 프로젝트별 상세 규칙을 중복 작성하지 않는다.
# Rule 구조 원칙
## global.md
전 프로젝트 공통 규칙만 둔다.
포함 항목:
- 응답 언어
- 기존 구조 우선
- 새 파일 생성보다 기존 수정 우선
- 코드 변경 전 관련 domain rule 확인
- 요청 범위를 넘는 변경 금지
- 불확실하면 단정하지 말고 후보 제시
- 플랫폼 특화 규칙은 global에 두지 않음
## domain rule
각 도메인 rules.md 는 아래만 둔다.
- 목적 / 책임
- 포함 경로
- 제외 경로
- 주요 구성 요소
- 유지할 패턴
- 다른 도메인과의 경계
- 금지 사항
# DDD 기준
도메인은 아래 기준으로 분류한다.
## Core Domain
핵심 가치와 주요 유즈케이스를 담당하는 영역
## Supporting Domain
핵심 도메인을 지원하는 영역
## Generic / Common
여러 도메인이 공통으로 사용하는 범용 영역
도메인은 반드시 실제 폴더, 모듈, 패키지, 네임스페이스, 책임 경계와 연결되어야 한다.
# Skill 구조 원칙
## router.md
router.md 는 작업 유형에 따라 필요한 skill 또는 domain rule만 로드하게 한다.
최소 라우팅 축:
- 구조 분석 / 스캐폴드
- 도메인 판별
- 코드 변경
- 흐름 추적
- 문서/릴리즈 정리
처음부터 너무 많은 분기를 만들지 않는다.
## skill-template.md
각 skill 은 아래 형식을 따른다.
- 목적
- 언제 호출할지
- 입력
- 먼저 확인할 것
- 실행 절차
- 출력 형식
- 금지 사항
# 신규 프로젝트 처리
신규 프로젝트일 때는 템플릿 중심으로 세팅한다.
1. 언어/구조를 확인한다.
2. 에이전트 진입 파일 3개를 얇게 생성한다.
3. agent-ops/rules/global.md 생성
4. domain-rule-template.md 생성
5. skills/router.md 생성
6. skill-template.md 생성
7. 핵심 domain placeholder 2~4개만 제안
8. 초기 skill 은 최소 2개만 제안한다.
신규 프로젝트에서는 domain rules 를 과도하게 채우지 않는다.
# 운영중 프로젝트 처리
운영중 프로젝트일 때는 현재 구조 기반으로 자동 채움한다.
1. 구조를 분석한다.
2. Core / Supporting / Generic 도메인 후보를 식별한다.
3. 실제 경로를 반영한 domain rules 초안을 제안한다.
4. 반복 작업 기반으로 필요한 초기 skill 을 제안한다.
5. router.md 에 기본 라우팅을 채운다.
6. 신규 프로젝트보다 한 단계 더 구체적으로 scaffold 한다.
운영중 프로젝트에서는 실제 코드 구조에 맞는 domain / skill 을 일부 채워 넣는다.
# 출력 형식
## 상태 판별
- Agent-Ops 상태: 없음 / 부분 적용 / 운영중
- 프로젝트 상태: 신규 / 운영중
- 근거: 짧게 요약
## 스캐폴드 계획
- 생성할 파일
- 바로 채울 파일
- placeholder 로 둘 파일
## 도메인 제안
- Core Domain
- Supporting Domain
- Generic / Common
## 초기 skill 제안
- skill 이름
- 필요한 이유
## 주의사항
- 지금 만들지 말아야 할 것
- 아직 확정하면 안 되는 것
# 금지
- 플랫폼 전용 규칙을 기본 scaffold 에 넣지 않는다.
- agent 별 md 에 상세 규칙을 중복 작성하지 않는다.
- 실제 구조보다 앞선 추상 구조를 강요하지 않는다.
- 처음부터 많은 domain / skill 을 만들지 않는다.
- scaffold 단계에서 불필요한 세부 구현까지 생성하지 않는다.

394
CLAUDE.md
View file

@ -1,391 +1,3 @@
# CLAUDE.md
## Project Overview
React Native 0.76.9 with **New Architecture disabled** (RCT_NEW_ARCH_ENABLED=0).
## Important Development Notes
- Always run the checks before committing code
- Always test any changes using the mobile mcp before calling something complete or committing code
- Never commit any planning markdown files
## Development Workflow
### Implementing Multi-Phase Features
When implementing complex features with multiple phases:
1. **Use subagents for each phase**: Launch a subagent (Task tool with `subagent_type: general-purpose`) to implement each phase independently
2. **Test after each phase**: Use a subagent to test the feature with mobile MCP after implementation
3. **Commit after each phase**: Create a commit for each completed and tested phase
4. **Never commit planning files**: Keep `.md` planning files (like `PLAN.md`) out of commits
### Testing with Mobile MCP
When testing features that require mobile device interaction:
- **Use subagents**: Launch a subagent to handle mobile MCP testing to avoid excessive context usage
- The subagent can launch the app, navigate, take screenshots, and verify functionality
- **Don't just verify the app launches** - actually trigger and interact with the feature being tested (e.g., send messages, click buttons, verify state changes)
## Key Commands
> **iOS Simulator Setup:** For detailed iOS simulator setup instructions, troubleshooting CocoaPods issues, and build debugging, see [CLAUDE-IOS-SIMULATOR.md](./CLAUDE-IOS-SIMULATOR.md).
```bash
# Setup
npm run pod-install # iOS CocoaPods (or pod-install-m1 for M1 Macs)
npm run ios-gems # Ruby gems for iOS (or ios-gems-m1 for M1 Macs)
# Development
npm start # Start Metro bundler
npm run ios # Run iOS in simulator
npm run android # Run Android in emulator
# TypeScript type checking
npm run tsc
# Fix linting issues
npm run fix
# E2E tests are in separate detox/ package
npm run e2e:ios # cd detox && npm run e2e:ios-test
npm run e2e:android # cd detox && npm run e2e:android-build && npm run e2e:android-test
# Builds delegate to Fastlane via shell scripts
npm run build:ios # ./scripts/build.sh ipa
npm run build:android # ./scripts/build.sh apk
# Utilities
npm run clean # Clean build artifacts
```
## Architecture
### Dual Database System (WatermelonDB)
**Critical**: Two separate SQLite databases via WatermelonDB:
1. **App Database**: One global database for app state, server list (`app/database/models/app/`)
2. **Server Databases**: One database per connected server for channels, users, posts (`app/database/models/server/`)
**DatabaseManager** singleton (`app/database/manager/index.ts`) manages all instances. Data operations go through operators with transformers/handlers/comparators.
**Sync handlers must handle the full lifecycle: create, update, AND delete.** When a handler syncs data from the server (e.g., `handleAIBots`, `handlePlaybookRuns`), it must also remove records that no longer exist on the server. Compare the full set of existing DB records against incoming data and call `prepareDestroyPermanently()` on stale records. Never write a sync handler that only creates/updates.
**Important:** Some server APIs do not return deleted records in their response. When the API only returns active records, the handler can diff against existing DB records to detect removals (as shown above). However, if the API returns a *partial* set of records (e.g., paginated without a deletion flag), you may need to request that the API be augmented to include a `delete_at` field or a separate deletions endpoint so that the handler can correctly identify which records to remove.
**Schema documentation**: When adding or modifying database tables, update `docs/database/server/server.md` (or `app_database/app-database.md`) and bump the schema version number in the header.
**Database directory**:
- iOS: App Group directory (shared with extensions)
- Android: `${documentDirectory}/databases/`
### Network Layer
- **NetworkManager** (`app/managers/network_manager.ts`) creates **one Client instance per server**
- Uses `@mattermost/react-native-network-client`
- **Product-specific API calls** should live in `client/rest.ts` within the product folder using the **ClientMix pattern** (see `app/products/calls/client/rest.ts` or `app/products/agents/client/rest.ts` for examples). Don't call `client.doFetch()` directly from action files.
### Actions Pattern
- **Local Actions** (`app/actions/local/`): Database-only operations
- **Remote Actions** (`app/actions/remote/`): Fetch from API → use operators to persist → return `{error}` on failure
- **Avoid redundant fetches**: Before fetching user profiles, check the DB first. Use `fetchMissingProfilesByIds()` from `@actions/remote/user` instead of raw `client.getProfilesByIds()` — it queries the DB and only fetches missing profiles
### Query Layer
**Query layer** (`app/queries/`) provides both:
- `query*`: Returns WatermelonDB Query
- `observe*`: Returns RxJS Observable
- `get*`: Returns Promise (async)
- `prepare*`: Returns prepared records for batch operations
### State Management
**Hybrid approach**:
- **WatermelonDB**: Primary data store (persisted)
- **Ephemeral Stores** (`app/store/`): In-memory transient UI state
### Navigation
- Uses **`react-native-navigation`** v7 (not react-navigation)
- Each screen is a separate registered component
- Every independently shown/hidden component must be registered as a screen for proper lifecycle management
### Products Architecture
Modular features in `app/products/` with their own database models:
- `agents/`: AI agents
- `calls/`: Voice/video calling
- `playbooks/`: Playbooks with dedicated DB models
#### Agents Streaming Architecture
**WebSocket Events:**
- Agents use `custom_mattermost-ai_postupdate` WebSocket events for streaming (start, message updates, reasoning, tool calls, annotations, end)
- NO specific WebSocket event for tool call status changes after approval/rejection
- Updates arrive via standard `POST_EDITED` event after backend executes tools
**Critical Pattern:**
- Components must clear local streaming state on `ENDED` event to switch from ephemeral streaming data to persisted database data
- Otherwise stale streaming state prevents POST_EDITED updates from displaying
### Custom Native Modules
Located at `libraries/@mattermost/`:
- `@mattermost/rnutils` - Native utilities (orientation, notifications)
- `@mattermost/rnshare` - Share extension integration
- `@mattermost/hardware-keyboard` - External keyboard detection
- `@mattermost/secure-pdf-viewer` - Secure PDF viewing
**Native Bridge patterns** (when modifying native modules):
- iOS: `RCT_EXPORT_MODULE()`, `RCT_EXPORT_METHOD()` in Objective-C++
- Android: Extend `ReactContextBaseJavaModule`, use `@ReactMethod`
### TypeScript Path Aliases
```typescript
@actions/* → app/actions/*
@agents/* → app/products/agents/*
@calls/* → app/products/calls/*
@database/* → app/database/*
@queries/* → app/queries/*
@store → app/store/index
@share/* → share_extension/*
@typings/* → types/*
// ... (see tsconfig.json for complete list)
```
### Share Extension
**Separate bundle** at `share_extension/` for Android system share. Shares code with main app but runs independently.
* iOS has its own native implementation for the Share Extension with Swift and SwiftUI
**Important:** Share extension on Android uses **React Navigation** (not react-native-navigation like main app).
## Testing
**Testing guide:** See [docs/testing_guide.md](docs/testing_guide.md) for how to add and structure unit tests.
### Test Organization
- **Jest coverage excludes** `/components/` and `/screens/` directories
- Mock database manager at `app/database/manager/__mocks__/index.ts`
- **E2E tests** in separate `detox/` package (not in main package.json)
- Add mocks to central `setup.ts` file, not individual test files
### Testing Patterns
#### Database Testing
- Tests use **real in-memory databases** (LokiJS), not mocks
- **Critical**: Set `extraLokiOptions: {autosave: false}` to prevent memory leaks from lingering timeouts
- Always clean up: `await DatabaseManager.destroyServerDatabase(serverUrl)` in `afterEach`
- Initialize database in `beforeEach`: `await DatabaseManager.init([serverUrl])`
#### Ephemeral Store Testing
- Use `jest.resetModules()` between tests to clear singleton state
- Require fresh imports after reset: `const Store = require('./store').default;`
- Pattern from `app/store/ephemeral_store.test.ts`
#### WebSocket Testing
- Mock connection objects, not the WebSocket class itself
- Create mock with methods: `onOpen`, `onClose`, `onError`, `onMessage`, `send`, `readyState`
- Use `client.open()` in tests to trigger `onOpen` callback
- Pattern from `app/client/websocket/index.test.ts`
#### Timer Testing
- **Always use**: `jest.useFakeTimers({doNotFake: ['nextTick']})` to let promises resolve
- Helper: `advanceTimers(ms)` from `test/timer_helpers.ts` advances time AND waits for promises
- Never fake `nextTick` or async operations will hang
#### Component Rendering
- Three helpers in `test/intl-test-helper.tsx`:
- `renderWithIntl`: Basic internationalization
- `renderWithIntlAndTheme`: + Theme context
- `renderWithEverything`: + Database + Server URL
- Use `renderWithEverything` when components need database access
- Wrap async state updates in `act()` when testing React components
#### Remote Action Testing
- Mock `NetworkManager.getClient` in `beforeAll` to return mock client
- Mock client should implement all methods used in tests
- Test pattern: Setup DB state → Call action → Assert no error and data returned
- Pattern from `app/actions/remote/channel.test.ts`
#### DeviceEventEmitter Testing
- Use `DeviceEventEmitter.addListener()` to spy on events
- Remove listener in cleanup: `listener.remove()`
- Pattern: Store listener callback in jest.fn(), assert it was called with expected data
### Test Utilities
**TestHelper** singleton at `test/test_helper.ts`:
- `generateId()`: Deterministic IDs for tests
- `setupServerDatabase()`: Complete DB setup with basic entities
- `fakeUser()`, `fakeChannel()`, etc.: Entity generators with sensible defaults
- `fakeUserModel()`, `fakeChannelModel()`: WatermelonDB model creators
### Writing Quality Tests
**Test names:**
- Use `it('should...')` format, not `test('happy path')`
- Check array lengths in addition to individual items to ensure no extra elements
- Test expectations must match actual implementation behavior - if code deletes state immediately, test for `undefined`, not for modified state values
**Mocking:**
- Use `jest.mocked()` for full implementations
- Use `(thing as jest.Mock)` type assertion when mocking partial implementations that don't satisfy the full interface (e.g., `NetworkManager.getClient` returning minimal mock client)
- Avoid `any` types in tests - use `typeof import('./module').default` for proper typing of dynamically imported modules
**Don't test:**
- JavaScript operators (`===`, `JSON.stringify()`) or library behavior (React, RxJS)
- "Mocks calling mocks" - thin wrapper functions that just pass data through
- Multiple input variations when behavior is identical (testing 0, 1, 5 items when function doesn't special-case counts)
- Duplicate assertions - if test A verifies X, don't add test B that only verifies X
- AI-generated tests are heavily scrutinized and often rejected if they don't demonstrate real intended behavior
**Do test:**
- Business logic, error handling, integration points, side effects
- One example per code path is sufficient
## Development Practices
### TypeScript
- Trust well-formed data - don't add defensive null checks if types guarantee existence
- Use `??` (nullish coalescing) instead of `||` for fallbacks when appropriate
- Avoid non-null assertions (`!`) - use optional chaining: `metadataRes.metadata?.followers?.length ?? 0`
- Type function parameters using `ComponentProps<typeof Component>` instead of `as const` workarounds
- JSDoc comments must match actual parameter names and types
- **Prefer const objects over enums** for better tree-shaking: `export const Status = { Pending: 0, Done: 1 } as const;` with companion type `export type Status = typeof Status[keyof typeof Status];`
### React Hooks
- **`react-hooks/exhaustive-deps` is enforced as error** - incomplete or incorrect dependency arrays will fail CI. Fix deps or use the dedicated hooks below when the omission is intentional.
- **Always explain why dependencies are omitted** from exhaustive-deps with specific comments (and prefer the dedicated hooks below for common cases).
- **Run-on-mount effects**: Use **`useDidMount`** from `@hooks/did_mount` instead of `useEffect(callback, [])` with an eslint-disable. The hook encapsulates the "run only on mount" intent and the single exhaustive-deps exception.
- **Initial value only (no updates)**: Use **`useInitialValue`** from `@hooks/initial_value` instead of `useMemo(factory, [])` with an eslint-disable when the value must be computed once and never recomputed.
- **Nested property as dependency**: When the meaningful dependency is a nested property (e.g. `currentUser?.notifyProps`) because the parent object reference may not change when that property changes, depend on that property in the array and add an eslint-disable with a comment explaining why. See `useNotificationProps` and `useUserTimezoneProps` in `app/hooks/` for the pattern.
- **`useDidUpdate`**: The hook takes a dependency list from the caller; the implementation uses an eslint-disable with the comment that dependencies are provided by the caller.
- Don't over-optimize with `useMemo` - only use for expensive calculations or objects passed as props to children
- Use `useCallback` for render functions passed to components, not for functions called within render
- Move module-level constants outside components
- Replace state variables with derived values when possible
- Stable references (refs, dispatch functions) don't need to be in dependency arrays but must have eslint-disable comments
- React-Native-Reanimated shared values (`.value`) don't cause re-renders and don't need to be dependencies
- **Critical**: When modifying existing `useEffect` hooks, you MUST include ALL dependencies used inside the effect, even if the original code had an incomplete dependency array - ESLint's `react-hooks/exhaustive-deps` rule is enforced strictly
- **Memoize callbacks for list items**: When rendering lists with `.map()`, avoid inline arrow functions like `onPress={() => handler(item.id)}`. Instead, have the child component accept the ID and call the callback internally, allowing the parent to pass a single memoized callback reference.
### React Native & UI
- Don't create hooks inside render functions - extract as local components
- **Never use `TouchableOpacity`** — use `Pressable` with pressed feedback instead
- Prefer `Button` components over `Pressable` when appropriate
- Non-memoized inline styles add render stress - define in stylesheet instead
- **`StyleSheet.create` is unnecessary** when using `makeStyleSheetFromTheme` - just return the plain object
- **Place `getStyleSheet` at file top** (after imports, before interfaces/components) not at the bottom
- Use `Platform.select()` for platform-specific values instead of ternaries
- StyleProps support nested lists - no need for custom `concatStyles()`
- Use `withTiming()` consistently for both states in animations
- Test components with long strings to ensure proper text handling
- Use constants from `PREFERENCES.THEMES` instead of hardcoded colors
- **Use `react-native-reanimated`** instead of React Native's `Animated` API for all animations (better performance, runs on UI thread)
- **`Pressable` must have pressed feedback**: Always add visual feedback via the style callback, e.g., `style={({pressed}) => [pressed && {opacity: 0.72}]}`. A `Pressable` without pressed state feels broken.
- **Extract static objects used as props into module-level constants**: Objects like `hitSlop`, `contentContainerStyle`, or any non-dynamic prop object should be a `const` outside the component to avoid creating new references on every render.
- **Use `usePreventDoubleTap` hook** for button press handlers to prevent accidental double submissions
- **Use `useServerUrl()` hook** instead of passing `serverUrl` as a prop - it's available via context
- **Use existing components**: Check for `<Loading>` instead of `<ActivityIndicator>`, `safeParseJSON()` instead of try/catch JSON.parse
- **Parent checks before mounting**: If a child component would return null for empty data, have the parent conditionally render instead (e.g., `{items.length > 0 && <ItemList items={items} />}`)
- **Use `@utils/url` utilities**: `tryOpenURL()` instead of `Linking.openURL()`, `getUrlDomain()` with `urlParse` instead of `new URL()`
- **Use `FormattedText`** instead of `<Text>{intl.formatMessage(...)}</Text>` for static i18n strings. Only fall back to `Text` + `intl.formatMessage` when the text includes dynamic non-translatable content (e.g., a user's display name)
### Code Quality & Linting
**Import Organization:**
- Consolidate all imports from the same module into a single statement
- Use inline `type` keyword for type imports: `import {SomeValue, type SomeType} from '@module'`
- Example: `import {StreamingEvents, type StreamingState} from '@agents/types'`
**Unused Code:**
- Remove unused imports, parameters, interfaces, and variables completely
- ESLint enforces strict no-unused-vars - code with unused declarations will fail CI
- When a parameter is truly unused, remove it from the function signature
**Pre-commit:**
- Run `npm run fix` to auto-fix linting issues before committing
- Pre-commit hook runs ESLint + TypeScript checking automatically
### Error Handling & Logging
- Use `logError()` instead of `console.error()` or `console.log()`
- Use `logDebug()` for debug-level information
- Don't ignore potential errors silently - handle them or add intentional comments
- **Log on early returns in handlers**: When a handler returns early (e.g., empty input), add a `logDebug` call so the no-op is traceable
- Don't log sensitive information
- **Add function/class prefix to logs**: e.g., `logError('[ClassName.methodName]', error)` to make debugging easier
### State Management
- Always handle errors from database operations
- Consider race conditions in async functions within effects
- Local state initialized with prop values won't update when props change - sync with `useEffect` if needed
### Performance
- Create parsers/expensive objects only once, not on every render (use refs or useMemo)
- Memoize AST output to avoid regenerating on every render
- Use named constants instead of magic numbers and check if one already exists
- **Never use raw `fontSize`, `fontWeight`, or `fontFamily`** in styles — always use `typography()` from `@utils/typography` (e.g., `...typography('Body', 200, 'SemiBold')`)
- Don't set style properties to their default values (e.g., `marginBottom: 0`) — it's dead code
### Localization (i18n)
- **CRITICAL**: Only update `en.json` - never modify other language files or Weblate gets corrupted
- **Adding new strings**: Define the message ID and defaultMessage in code using `defineMessages()`, then run `npm run i18n-extract` to automatically add them to `en.json`
- Default messages in code must match JSON translations exactly, including newlines
- Translation IDs should be descriptive enough for translators to understand context
- Don't reuse translation IDs
- Translate user-facing strings, not debug/error messages
### Markdown Component Usage
**Required props:**
- `baseTextStyle`, `value`, `theme`, `location`
- Use `Screens.CHANNEL` or appropriate constant from `@constants` for the `location` prop
- `textStyles`, `blockStyles`, `enableLatex`, and similar props are auto-generated via HOC
**Important Limitations:**
- **Cannot embed custom React components inline**: The Markdown component renders CommonMark to native components. You cannot insert custom React components (like badges, icons) inline within the markdown text flow
- **Link handling**: Links in markdown go through `openLink()` → deep link system → `tryOpenURL()`. Custom URL schemes (e.g., `citation:`, `action:`) will show error alerts unless you implement a full deep link handler in `app/utils/deep_link/`
- **Post-processing not possible**: Unlike web, you cannot post-process the rendered markdown JSX tree to replace markers with React components
### Adding New Post Types
When adding custom post types (e.g., for new products):
1. Add the type string to `PostType` union in `types/api/posts.d.ts`
2. Define constants in your product's constants file (e.g., `app/products/agents/constants.ts`)
3. Use the globally-available `Post` type (defined in `types/api/posts.d.ts`) - no import needed
### Platform-Specific
- iOS: Opt out of iOS 18+ features (liquid glass) until UI is properly addressed
- Android: Test on multiple API levels (34, 35, 36) to verify behavior with edge-to-edge changes
- Use `Set` instead of `Array` for exception lists (faster, ensures uniqueness)
### testID Convention
Components use hierarchical testIDs: `component.subcomponent.element`
- Example: `channel_list.category.CATEGORY.channel_item.ID.display_name`
- Example: `channel.post_draft.send_action.send.button`
## Common Mistakes to Avoid
### JavaScript Compatibility
- **Don't use `Object.hasOwn()`** - React Native doesn't support ES2022+ features
- ❌ `Object.hasOwn(obj, 'key')`
- ✅ `'key' in obj`
### Anti-Patterns
- Conditionals in tests for TypeScript satisfaction
- Creating new arrays/objects on every render when passed as props
- Over-engineering - follow YAGNI principle
- Impossible states with boolean + optional parameters that depend on each other
- Redundant checks - if `array.length >= 3`, indices 0,1,2 are guaranteed to exist
- Verbose error messages that reveal internal system details
- Mass assignment risks - whitelist fields instead of sending `Partial<Model>` objects
- Duplicate conditional branches that return identical results - consolidate into a single condition (e.g., `if (A && B)` and `if (A && !B)` both returning same thing should be just `if (A)`)
### Security
- Question whether showing secrets with "eye" toggle is a security concern
- Don't commit files that likely contain secrets (.env, credentials.json)
- Consider pre-populating sensitive fields with `*********` instead of actual values
## Configuration
- **Patches**: Applied via `patch-package` on postinstall (`patches/` directory)
- **Self-compiled apps**: Require your own Mattermost Push Notification Service
- **Self-signed certificates**: Not supported
## Development Notes
### Hot Reload & Build Times
- **Hot reload**: ~3 seconds for JS/TS changes; does NOT work for native code changes
- **Native rebuilds**: 10-30 minutes (avoid unless necessary)
- **Pre-commit hook**: Runs ESLint + incremental TypeScript checking
### Known Issues
- Many components require `theme` prop - check for `useTheme()` hook in parent component
# 필수 준수 사항
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.

3
GEMINI.md Normal file
View file

@ -0,0 +1,3 @@
# 필수 준수 사항
- agent-ops/rules/global.md 파일을 읽고 이에 대해 꼭 준수해서 진행해야한다.
- global.md 내용을 모르면 추정하지 말고 먼저 해당 파일을 확인한다.

View file

@ -0,0 +1,32 @@
---
name: <domain-name>
description: <도메인 설명>
type: core | supporting | generic
---
# <Domain Name>
## 목적 / 책임
< 도메인이 담당하는 핵심 책임>
## 포함 경로
- `app/<path>/`
- `app/actions/local/<file>.ts`
- `app/actions/remote/<file>.ts`
## 제외 경로
- < 도메인에 속하지 않는 경로>
## 주요 구성 요소
- **<component>**: <역할>
- **<component>**: <역할>
## 유지할 패턴
- < 도메인에서 따라야 패턴>
## 다른 도메인과의 경계
- **database 도메인**: DB 조작은 database 도메인 패턴을 따른다
- **network 도메인**: API 호출은 ClientMix 패턴을 따른다
## 금지 사항
- < 도메인에서 하면 되는 >

View file

@ -0,0 +1,46 @@
---
name: database
description: WatermelonDB 이중 DB 시스템 - 앱 DB와 서버별 DB
type: supporting
---
# Database Domain
## 목적 / 책임
WatermelonDB 기반 이중 데이터베이스를 관리한다. DatabaseManager 싱글톤, operator, 스키마, 모델을 담당한다.
## 포함 경로
- `app/database/` (전체)
- `app/database/manager/` - DatabaseManager 싱글톤
- `app/database/models/app/` - 앱 전역 모델 (서버 목록 등)
- `app/database/models/server/` - 서버별 모델 (채널, 유저, 포스트 등)
- `app/database/operator/` - 데이터 쓰기 operator
- `app/database/schema/` - DB 스키마 정의
- `docs/database/` - DB 스키마 문서
## 제외 경로
- `app/queries/` (Query Layer는 별도 - 여기서 DB를 읽는다)
## 주요 구성 요소
- **DatabaseManager**: 모든 DB 인스턴스 관리 싱글톤 (`app/database/manager/index.ts`)
- **App Database**: 서버 목록 등 전역 앱 상태 (하나만 존재)
- **Server Database**: 서버별 채널/유저/포스트 데이터 (서버당 하나)
- **Operators**: transformer/handler/comparator 패턴으로 데이터 쓰기
- **Schema**: 테이블 정의 및 마이그레이션
## 유지할 패턴
- DB 경로: iOS는 App Group, Android는 `${documentDirectory}/databases/`
- Operator를 통해 batch 작업으로 데이터 쓰기
- Sync handler는 create/update/delete 전체 라이프사이클 처리 필수
- 스키마 변경 시 반드시 버전 번호 증가 + 문서(`docs/database/`) 업데이트
- Query Layer(`app/queries/`)는 `query*`, `observe*`, `get*`, `prepare*` 패턴
## 다른 도메인과의 경계
- **모든 도메인**: DB 직접 접근보다 Query Layer(`app/queries/`) 사용 권장
- **network 도메인**: remote action이 API 응답을 operator에 전달
## 금지 사항
- 스키마 변경 후 `docs/database/` 문서 미업데이트
- Sync handler에서 stale 레코드 삭제 누락 (`prepareDestroyPermanently()` 필수)
- 테스트에서 `extraLokiOptions: {autosave: false}` 누락 (메모리 누수)
- 테스트 후 `DatabaseManager.destroyServerDatabase()` cleanup 누락

View file

@ -0,0 +1,45 @@
---
name: messaging
description: 채널, 포스트, 스레드, 드래프트 등 핵심 메시징 기능
type: core
---
# Messaging Domain
## 목적 / 책임
Mattermost의 핵심 메시징 기능: 채널, 포스트, 스레드, 리액션, 드래프트, 예약 포스트를 담당한다.
## 포함 경로
- `app/actions/local/channel.ts`, `post.ts`, `thread.ts`, `draft.ts`, `reactions.ts`, `scheduled_post.ts`
- `app/actions/remote/channel.ts`, `post.ts`, `thread.ts`, `reactions.ts`, `search.ts`
- `app/queries/servers/channel.ts`, `post.ts`, `thread.ts`, `drafts.ts`, `scheduled_post.ts`, `reaction.ts`
- `app/database/models/server/channel*.ts`, `post.ts`, `thread*.ts`, `draft.ts`, `reaction.ts`, `scheduled_post.ts`
- `app/screens/channel/`, `app/screens/thread/`, `app/screens/edit_post/`
- `app/components/post_list/`, `app/components/post_draft/`
## 제외 경로
- `app/products/` (products는 각 product 도메인)
- `app/actions/remote/user.ts` (user 도메인)
## 주요 구성 요소
- **Channel**: 채널 CRUD, 멤버십, 카테고리 관리
- **Post**: 포스트 생성/수정/삭제, 파일 첨부, 리액션
- **Thread**: 스레드 팔로우, 언급, 참여자 관리
- **Draft**: 임시저장, 예약 포스트
- **Search**: 채널/포스트 검색
## 유지할 패턴
- Remote action: `fetchXxx()` → API 호출 → operator로 DB 저장 → `{error}` 반환
- Local action: DB 조작만 수행
- Sync handler는 create/update/delete 전체 라이프사이클을 처리한다
- `fetchMissingProfilesByIds()` 사용 - 직접 `client.getProfilesByIds()` 호출 금지
## 다른 도메인과의 경계
- **database 도메인**: WatermelonDB 스키마 변경 시 `docs/database/server/server.md` 업데이트 필요
- **network 도메인**: API 호출은 `client/rest.ts`의 ClientMix 패턴 사용
- **ui 도메인**: 화면 렌더링은 ui 도메인, 데이터 로직은 이 도메인
## 금지 사항
- `console.error()` / `console.log()` 직접 사용 (→ `logError()`, `logDebug()` 사용)
- `TouchableOpacity` 사용 (→ `Pressable` 사용)
- DB sync handler에서 삭제 누락 (stale 레코드는 `prepareDestroyPermanently()` 처리)

View file

@ -0,0 +1,49 @@
---
name: products-agents
description: AI 에이전트 기능 - WebSocket 스트리밍, AI 봇, 툴 호출
type: core
---
# Products/Agents Domain
## 목적 / 책임
AI 에이전트 기능을 담당한다. WebSocket 스트리밍, AI 봇 관리, 툴 호출/승인, 어노테이션을 처리한다.
## 포함 경로
- `app/products/agents/` (전체)
- `app/products/agents/actions/`
- `app/products/agents/client/rest.ts`
- `app/products/agents/components/`
- `app/products/agents/constants.ts`
- `app/products/agents/hooks/`
- `app/products/agents/screens/`
- `app/products/agents/store/`
- `app/products/agents/types/`
- `app/products/agents/websocket/`
## 제외 경로
- `app/actions/remote/` (messaging 도메인)
- `app/products/calls/` (calls 도메인)
## 주요 구성 요소
- **WebSocket Handler**: `custom_mattermost-ai_postupdate` 이벤트 처리 (start, message, reasoning, tool_call, annotation, end)
- **Streaming Store**: 스트리밍 중 ephemeral 상태 관리
- **AI Bot**: AI 봇 목록 조회 및 DB 동기화 (`handleAIBots`)
- **Tool Call**: 툴 호출 승인/거절 처리
- **Client (ClientMix)**: `app/products/agents/client/rest.ts`
## 유지할 패턴
- **WebSocket 스트리밍**: `ENDED` 이벤트에서 로컬 스트리밍 상태를 반드시 초기화 (stale state 방지)
- **ClientMix 패턴**: API 호출은 `client/rest.ts` 내에서만 수행, action에서 직접 `doFetch()` 금지
- **PostType 추가 시**: `types/api/posts.d.ts``PostType` union에 타입 추가
- **Sync handler**: create/update/delete 모두 처리 (`prepareDestroyPermanently()` 필수)
## 다른 도메인과의 경계
- **messaging 도메인**: 포스트 데이터는 `POST_EDITED` WebSocket 이벤트로 수신
- **database 도메인**: Agents 전용 DB 모델 사용
- **network 도메인**: ClientMix를 통해 NetworkManager와 연결
## 금지 사항
- 툴 호출 상태 변경을 별도 WebSocket 이벤트로 처리 (→ `POST_EDITED`로 수신)
- `ENDED` 이벤트 후 스트리밍 상태 미초기화
- action 파일에서 직접 `client.doFetch()` 호출

View file

@ -0,0 +1,41 @@
---
name: products-calls
description: 음성/화상 통화 기능
type: core
---
# Products/Calls Domain
## 목적 / 책임
음성/화상 통화 기능을 담당한다. 통화 연결, 상태 관리, UI를 처리한다.
## 포함 경로
- `app/products/calls/` (전체)
- `app/products/calls/actions/`
- `app/products/calls/client/rest.ts`
- `app/products/calls/components/`
- `app/products/calls/constants.ts`
- `app/products/calls/hooks/`
- `app/products/calls/screens/`
- `app/products/calls/store/`
- `app/products/calls/types/`
## 제외 경로
- `app/products/agents/` (agents 도메인)
- `app/products/playbooks/` (playbooks 도메인)
## 주요 구성 요소
- **Call Connection**: WebRTC 기반 통화 연결 관리
- **Call State**: Ephemeral store로 통화 상태 관리
- **Client (ClientMix)**: `app/products/calls/client/rest.ts`
## 유지할 패턴
- **ClientMix 패턴**: API 호출은 `client/rest.ts` 내에서만 수행
- **Ephemeral Store**: 통화 상태는 ephemeral store로 관리 (DB 아님)
## 다른 도메인과의 경계
- **messaging 도메인**: 채널 정보는 messaging 도메인 쿼리 사용
- **network 도메인**: ClientMix를 통해 NetworkManager와 연결
## 금지 사항
- action 파일에서 직접 `client.doFetch()` 호출

View file

@ -0,0 +1,53 @@
---
name: ui
description: 컴포넌트, 스크린, 네비게이션, 스타일링
type: generic
---
# UI Domain
## 목적 / 책임
React Native UI 컴포넌트, 스크린, 네비게이션, 스타일링을 담당한다.
## 포함 경로
- `app/components/` - 재사용 컴포넌트
- `app/screens/` - 화면 단위 컴포넌트
- `app/hooks/` - 공통 React hooks
- `app/utils/` - UI 유틸리티
- `share_extension/` - Android 공유 확장 (React Navigation 사용)
## 제외 경로
- `app/products/*/components/` (각 product 도메인)
- `app/actions/` (비즈니스 로직은 messaging/product 도메인)
## 주요 구성 요소
- **Navigation**: `react-native-navigation` v7 (메인 앱), `React Navigation` (share extension)
- **Components**: 60+ 재사용 컴포넌트
- **Screens**: 63+ 화면 컴포넌트 (각각 독립 등록)
- **Themes**: `PREFERENCES.THEMES` 상수 사용
## 유지할 패턴
- `Pressable` 사용 (→ `TouchableOpacity` 금지), 항상 pressed 피드백 추가
- `typography()` 사용 (→ raw `fontSize`/`fontWeight`/`fontFamily` 금지)
- `makeStyleSheetFromTheme` 사용 시 `StyleSheet.create` 불필요
- `getStyleSheet` 는 파일 상단(imports 이후)에 위치
- 정적 객체(hitSlop 등)는 모듈 레벨 상수로 추출
- `FormattedText` 사용 (→ `<Text>{intl.formatMessage(...)}</Text>` 지양)
- `useServerUrl()` hook 사용 (→ `serverUrl` prop 전달 지양)
- `usePreventDoubleTap` hook 사용 (버튼 중복 클릭 방지)
- 리스트 렌더링: 인라인 화살표 함수 금지, 자식 컴포넌트에서 ID 받아 처리
- `useDidMount` (→ `useEffect(cb, [])` 대신), `useInitialValue` (→ `useMemo(f, [])` 대신)
## 다른 도메인과의 경계
- **messaging/product 도메인**: UI는 데이터 표시만, 비즈니스 로직은 해당 도메인
- **database 도메인**: `observe*` 쿼리를 통해 반응형 데이터 구독
## 금지 사항
- `TouchableOpacity` 사용
- raw `fontSize`, `fontWeight`, `fontFamily` 스타일 직접 사용
- `Object.hasOwn()` 사용 (ES2022+, RN 미지원 → `'key' in obj` 사용)
- `Animated` API 사용 (→ `react-native-reanimated` 사용)
- `new URL()` 사용 (→ `urlParse` 사용)
- `Linking.openURL()` 직접 사용 (→ `tryOpenURL()` 사용)
- `ActivityIndicator` 직접 사용 (→ `<Loading>` 컴포넌트 사용)
- 커스텀 React 컴포넌트를 Markdown 컴포넌트 내부에 인라인 삽입

23
agent-ops/rules/global.md Normal file
View file

@ -0,0 +1,23 @@
---
name: global
description: 전 프로젝트 공통 규칙
version: 1.0.0
---
# Global Rules
## 응답 언어
- 한국어로 응답한다.
## 기존 구조 우선
- 새 파일 생성보다 기존 파일 수정을 우선한다.
- 기존 패턴과 컨벤션을 따른다.
## 코드 변경 원칙
- 코드 변경 전 관련 domain rule을 확인한다.
- 요청 범위를 넘는 변경을 금지한다.
- 불확실하면 단정하지 말고 후보로 제시한다.
## 작업 전 확인
- `agent-ops/skills/router.md` 를 먼저 참조하여 필요한 skill 또는 domain rule만 로드한다.
- 관련 rule / skill 만 최소로 로드한다.

View file

@ -0,0 +1,35 @@
---
name: <skill-name>
description: <skill 설명>
version: 1.0.0
---
# <Skill Name>
## 목적
< skill이 해결하는 문제>
## 언제 호출할지
- < skill을 사용해야 하는 상황>
- < skill을 사용해야 하는 상황>
## 입력
- `<param>`: <설명> (필수/선택)
## 먼저 확인할 것
- [ ] <사전 확인 항목>
- [ ] <사전 확인 항목>
## 실행 절차
1. <단계>
2. <단계>
3. <단계>
## 출력 형식
```
<출력 예시>
```
## 금지 사항
- <하면 되는 >
- <하면 되는 >

View file

@ -0,0 +1,48 @@
---
name: code-change
description: 코드 변경 절차 - 읽기 → 도메인 rule 확인 → 변경 → 검증
version: 1.0.0
---
# Code Change
## 목적
코드 변경 시 도메인 규칙을 준수하고 기존 패턴을 따른다.
## 언제 호출할지
- 새 기능을 추가할 때
- 버그를 수정할 때
- 기존 코드를 수정할 때
## 입력
- `변경 대상`: 파일 경로 또는 기능 설명 (필수)
- `변경 내용`: 무엇을 어떻게 변경할지 (필수)
## 먼저 확인할 것
- [ ] `find-domain` skill로 대상 도메인 판별
- [ ] 해당 domain rule 로드 (`agent-ops/rules/domain/<domain>/rules.md`)
- [ ] 변경할 파일을 Read tool로 먼저 읽기
- [ ] 기존 패턴과 컨벤션 파악
## 실행 절차
1. 대상 파일 읽기 (Read tool 사용)
2. 도메인 rule의 "유지할 패턴"과 "금지 사항" 확인
3. 변경 범위를 요청 범위로 제한 (추가 리팩터링 금지)
4. 변경 적용 (Edit tool 우선, 전면 재작성은 Write tool)
5. TypeScript 타입 확인 (`npm run tsc`)
6. 린팅 확인 (`npm run fix`)
## 출력 형식
변경 후:
```
변경 파일: <path>
변경 내용: <요약>
확인 필요: <있으면 명시>
```
## 금지 사항
- 파일을 읽기 전에 변경하지 않는다
- 요청 범위를 넘는 리팩터링, 주석 추가, 타입 어노테이션 추가
- 도메인 rule의 금지 사항 위반
- `console.log()` / `console.error()` 직접 사용
- 다른 언어 파일 수정 (`en.json` 외 i18n 파일 수정 금지)

View file

@ -0,0 +1,40 @@
---
name: find-domain
description: 파일 경로 또는 기능으로 해당 도메인을 판별한다
version: 1.0.0
---
# Find Domain
## 목적
변경하거나 분석할 코드가 어느 도메인에 속하는지 판별한다.
## 언제 호출할지
- 어떤 파일을 수정해야 하는지 불분명할 때
- 기능이 어느 도메인에 속하는지 모를 때
- 여러 도메인에 걸친 변경이 필요한지 파악할 때
## 입력
- `경로 또는 기능 설명`: 파일 경로, 컴포넌트 이름, 또는 기능 설명 (필수)
## 먼저 확인할 것
- [ ] `agent-ops/skills/router.md`의 경로 패턴 테이블 확인
- [ ] CLAUDE.md의 TypeScript Path Aliases 섹션 확인
## 실행 절차
1. 입력이 파일 경로면 → router.md 테이블과 매칭
2. 입력이 기능 설명이면 → 관련 파일을 Glob/Grep으로 탐색
3. 도메인 후보를 식별하고 해당 domain rule 로드
4. 여러 도메인에 걸친 경우 → 주 도메인 + 영향 도메인 명시
## 출력 형식
```
도메인: <domain-name>
근거: <판별 근거>
관련 rule: agent-ops/rules/domain/<domain>/rules.md
영향 도메인: <있으면 명시>
```
## 금지 사항
- 도메인을 단정하지 않고 불확실하면 후보를 제시한다
- 관련 없는 domain rule까지 로드하지 않는다