AI-powered Connector Hub. Python async, provider-agnostic, protocol-first.
- Package manager:
uv(not pip) · Frontend: Next.js + pnpm (frontend/) - Tests:
uv run pytest· Launcher:./start.sh [portal|api]
src/fim_one/
├── core/{agent,model,planner,memory,tools}/
├── web/ # FastAPI backend (agents, connectors, KB, chat)
frontend/ # Next.js portal (shadcn/ui)
- Commit scope: (1) session has context → commit session's files; (2) fresh session no context →
git add -A; (3) user specifies files inline → follow exactly. Always exclude sensitive files (.env, credentials). - Atomic commits: split unrelated changes, even if user says "commit all".
- NEVER
git stash --include-untrackedwith important untracked files —git addthem first. Usegit stash popnotapply+drop. - Worktrees: clean tree before starting; agents commit on their branch; merge via
git merge/cherry-pick, not file copying. - Worktree + migration: worktree agents MUST NOT run
alembic upgrade head(shared SQLite dev DB desyncs). Write migration files + ORM + code only; apply after merge-back. - Worktree merge-back: orchestrator (main conversation) MUST
git merge <branch>when agent finishes — no orphan branches.
- NEVER
rm -rf frontend/.nextwhile dev server is running (kills HMR). - Production builds →
.next-buildviacd frontend && pnpm build.
- No native
confirm/alert/prompt— useAlertDialog/Dialog/ Toast (sonner). - Navigation →
<Link>, not<button onClick={router.push()}>. For shadcn<Button>,asChild+<Link>inside. - Focus rings:
focus-visible:outline-*, neverfocus-visible:ring-*(ring = box-shadow, clipped byoverflow:hidden). Ref:ui/input.tsx. - No
pl-*/pr-*on form wrappers with<Input>/<Textarea>(clipsshadow-xs). Usepx-*orgap-*. - No native
<select>— use shadcn<Select>with<SelectTrigger className="w-full">. Empty default →__default__sentinel (Radix treats""as unset). - Tab/filter state → URL query: sync to
?tab=xxxviauseSearchParams+router.replace. Default tab = no param. Wrap in<Suspense>. Ref:admin/page.tsx. - Motion timing → tokens, never ad-hoc:
lib/motion.ts(Motion components) or--motion-duration-*/--motion-ease-*(CSS keyframes). The two sets mirror each other; change both together. - Card grids/lists →
<StaggerOnce>+<StaggerItem>(ui/fade-in.tsx). Inside a grid,StaggerItemneedsclassName="grid"or same-row cards lose equal height.StaggerOnceanimates on first mount only, so search/pagination must not replay it. Ref:kb/page.tsx. - New CSS keyframes → add a
prefers-reduced-motionrule in the consolidated block at the end ofglobals.css. Animations that start fromopacity: 0withforwardsmust land in the finished state there, not justanimation: none, or the content stays invisible. Motion components are covered globally byMotionProvider; leave indicators that signal work in flight (spinners, progress) animating.
All admin data-table rows MUST follow admin-users.tsx:
- Row actions live in a "..."
DropdownMenu—MoreHorizontalghost button (h-7 w-7 p-0), last column,text-right. - NEVER inline icon buttons (trash/edit/eye/power) in rows. NEVER clickable rows (
cursor-pointer+ rowonClick) — use a "View Details"DropdownMenuItem. - Last
<th>={tc("actions")}withtext-right font-medium text-muted-foreground. - Every
DropdownMenuItemMUST have a lucide icon (<Icon className="mr-2 h-4 w-4" />). - Order: View/Edit → Enable/Disable → Delete last with
variant="destructive"+DropdownMenuSeparatorbefore it. - Dialogs/Sheets stay as table siblings; only the trigger moves into the dropdown.
Two-tier: inline for field errors, toast for system errors.
- Field errors (empty/format/conflict like "username taken") → inline
<p className="text-sm text-destructive">below field. UsefieldErrors+clearFieldErroron change. Addaria-invalid. - System/API errors (5xx, network, auth, external down) →
toast.error(errMsg(err)). - Success →
toast.success(). - Hybrid 400: maps to a field → inline; else toast.
- Never silently close dialogs. Never only
console.error(). Every user action gets visible feedback.
Create/edit forms MUST guard against accidental close (backdrop, X, navigation). AlertDialog is sibling of Dialog, never nested. Refs: connector-settings-form.tsx (modal), agents/[id]/page.tsx (full-page).
All UI text via next-intl — never hardcode English. useTranslations("{ns}"). Shared strings → useTranslations("common"). New namespace = drop JSON in messages/en/, auto-discovered.
English only, auto-sync the rest:
- NEVER manually edit any non-English file:
messages/{zh,ja,ko,de,fr}/,docs/{zh,ja,ko,de,fr}/,README.{zh,ja,ko,de,fr}.md. All auto-generated. - Only edit:
messages/en/{ns}.json,docs/*.mdx(root),README.md. - Pre-commit hook runs
scripts/translate.py(incremental). Full retrans:uv run scripts/translate.py --all. Setup:bash scripts/setup-hooks.sh.
Adding a new locale:
- Frontend:
SUPPORTED_LOCALESinfrontend/src/i18n/request.ts. - Backend: all locale regex in
src/fim_one/web/schemas/auth.py—preferred_language(UpdateProfileRequest) +locale(Send{Verification,Login,Reset,Forgot}CodeRequest). Miss this = silent 400 + locale won't persist. - Docs nav: add to
LOCALESinscripts/build-docs-nav.py, fill glossary, create emptydocs/nav-overrides/{locale}.json.
docs/docs.json is generated — edit docs/nav.template.json / scripts/docs-nav-glossary.json / docs/nav-overrides/*.json. Pre-commit + i18n-sync.yml regenerate it.
Dev = SQLite, prod = PG. One migration set for both. start.sh runs alembic upgrade head on startup.
- Every new ORM model/column MUST have a migration — never
metadata.create_all(), never ad-hocALTER TABLEinengine.py. - Idempotent: use
table_exists()/table_has_column()/index_exists()fromfim_one.migrations.helpers. - Boolean defaults:
server_default=sa.text("FALSE")/"TRUE"— never"0"/"1"(PG rejects). Same for ORM modelserver_default. - Integer default:
server_default="0"OK. Timestamp:sa.text('(CURRENT_TIMESTAMP)')OK. - Timestamp columns MUST be
sa.DateTime(timezone=True)in BOTH the ORM model and the migration. The ORM writes tz-awaredatetime.now(UTC), which asyncpg rejects against a naive PG column — SQLite hides this, so it's a latent PG-only 500. A one-time scan (l2n4p6r8t901) already converted all columns that existed then; any new naive timestamp column re-introduces the bug (ref:m5o7q9s1u234). - JSON: SQLite
json_extract(col, '$.key')vs PGcol::json->>'key'. Checkbind.dialect.namein data migrations (ref:b2d4e6f8a901). - SQLite ALTER COLUMN: use
op.batch_alter_table()(SQLite can't ALTER). - Auto-apply: after writing migration in main worktree (NOT agent worktree), immediately
uv run alembic upgrade head.
New user-owned module writing to disk → update purge_user_data() in src/fim_one/web/services/user_deletion.py (the single path both admin delete and self-serve delete funnel through). ORM cascade only drops rows; files get orphaned. New table with a FK to users and no ondelete cascade → also add an explicit FK-safe DELETE there, or db.delete(user) will hit a FK violation (SQLite now runs with PRAGMA foreign_keys=ON).
Current registry (search Clean up file-system resources):
| Module | Path | Method |
|---|---|---|
| conversations | data/sandbox/{conv_id}/, uploads/conversations/{conv_id}/, data/workspaces/{conv_id}/, data/dag_checkpoints/{conv_id}.json |
shutil.rmtree / unlink |
| knowledge_bases | uploads/kb/{kb_id}/, data/vector_store/user_{user_id}/ |
shutil.rmtree |
| user uploads | uploads/user_{user_id}/ |
shutil.rmtree |
| avatar | uploads/avatars/{user_id}_* |
glob + unlink |
If your module writes under uploads/ or data/, add cleanup.
- Type hints on all public functions. Async-first for I/O.
__init__.pyimports minimal (public API only).
- Every new module →
tests/test_{module}.py. Every feat commit includes tests. - All tests pass before commit:
uv run pytest tests/ -x -q. - Type checks pass:
uv run mypy <changed_files>(strict=true; fix types, nevertype: ignore). - No external services — mock DB/MCP/HTTP/LLM via
unittest.mock/AsyncMock. - Naming:
tests/test_{module}.py·Test{Feature}·test_{behavior}. - Behavioral evals: changing
core/agent/system_prompt.py,core/agent/react.py, orcore/tool/builtin/*→ runuv run pytest evals/ -q(~2 min, real LLM) BEFORE commit. Pre-commit enforces viaevals/.eval-stampfingerprint (watch list:scripts/eval_stamp.py). Agent worktrees:SKIP_EVALS=1 git commit; orchestrator runs evals after merge-back.
After any code change, report:
- What changed — files + brief summary.
- How to test — concrete steps (page + click,
uv run pytest tests/test_foo.py, restart + check Z).
Communication only — doesn't replace automated tests.
After EVERY
git commit, run the checklist below BEFORE responding or moving on. Non-negotiable.
English first, translate on commit. Edit EN; pre-commit hook translates to ZH/JA/KO/DE/FR.
-
docs/changelog.mdx— append under[Unreleased](### Added/Changed/Fixed/Removed). User-facing only. Skip: internal refactor, style, test count, doc typos, CI tweaks, dep bumps with no behavior change. One concise line — no file/class/test-count details. - (feat OR behavior-changing fix)
docs/roadmap.mdx. Treatfix:asfeatwhen user-observable behavior changes (API errors disappearing, protocol compat, new observability, correctness guarantees). Pure internal fixes skip. Rules:- Check off
- [ ]→- [x]if commit satisfies it. - Insert new significant items under fitting planned version;
- [x]if just shipped,- [ ]if spawns follow-up. - Never touch shipped (date-stamped) versions.
- Deferred items →
- [ ]under right version. Internal planning docs are scratchpads, not a substitute. - Roadmap is an INDEX, not a blueprint. Each entry is one line, ≤150 chars — names what ships and the user benefit. No multi-sentence implementation prose, no nested sub-bullets describing schema/tradeoffs/rationale. If you need to write more, you need a
dev/file. - Detail goes to
dev/<topic>.md(single-topic) ordev/<topic>/(multi-file design).dev/is not shipped to Mintlify, so roadmap pointers MUST use a JSX comment: append{/* dev: dev/<topic>.md */}to the section heading (e.g.#### Hook System {/* dev: dev/hook-system.md */}). Mintlify strips JSX comments at compile time → public docs stay clean; Claude reading the file source sees the pointer and can pick up the companion design doc on the next pass. Never use a visible bracketed link like[详见 dev/...]— that leaks internal structure to public readers. Existing dev/ companions:dev/hook-system.md,dev/im-channels.md,dev/connector-rbac/,dev/agent-workspace.md,dev/public-api-phase2.md,dev/prompt-cache-followups.md,dev/channel-integration-sso.md,dev/agent-trace-layer.md. Rule applies forward — don't audit/rewrite already-terse entries; existing verbose entries stay until they ship and get archived. dev/<topic>.mdis a planning-time artifact, not a per-commit artifact. Create or extend it when adding a complex roadmap item (incremental planning — "let's do X in v0.9, here's the design"). When checking off an existing item at commit time ([ ]→[x]), do NOT touch the dev/ file. Implementation may have diverged from the design doc and that's fine — the source of truth is the code. After the version ships and the item is archived, the matching dev/ file may be moved todev/archive/in a follow-up cleanup, but that's optional and never blocks a ship commit.
- Check off
- (feat only)
example.env— new env keys with placeholder + comment; syncdocs/configuration/environment-variables.mdxtable. - (feat only)
README.md— update Key Features / Project Structure if needed. - (provider-behavior change) Provider Capability Matrix in
docs/architecture/llm-provider-guide.mdx— touching_dispatch_acompletion,_build_request_kwargs,reasoning_replay_policy, or the structured-output downgrade chain means updating the matrix in the SAME commit. It is the authoritative provider reference; other docs link to it instead of restating it. - Chinese sync — pre-commit hook handles
docs/zh/+README.zh.md. Commit EN + ZH together.
Version source chain: About dialog (frontend) → GET /api/version → fim_one.__version__ (src/fim_one/__init__.py) → must equal pyproject.toml::version → must equal highest ROADMAP Shipped version → must equal latest archived CHANGELOG version. All five must agree at all times. When they drift, fix in the same commit that surfaced the drift.
BEFORE answering:
- Archive
CHANGELOG [Unreleased]→[vX.Y] - YYYY-MM-DD. Add a fresh empty[Unreleased]above it. - Mark shipped: ROADMAP version heading gets date, moved under Shipped Versions.
- Bump version:
pyproject.toml::versionANDsrc/fim_one/__init__.py::__version__match new shipped version. - Then answer with next priorities from first unfinished version's
- [ ].
Roadmap vs Changelog (don't conflate them): Roadmap = capability index (per-version one-liners; trajectory + headline of what shipped). Changelog = release notes (per-version Added/Changed/Fixed prose; what a developer auditing the diff needs to know). Both are mandatory; they target different readers and different organizing dimensions. Cut-release writes both.