Add a rule to the "Реализация" section of AGENTS.md stating that git worktrees may only be created inside the .claude directory (e.g. .claude/worktrees/<name>); creating them anywhere else is forbidden.
27 KiB
AGENTS.md
This file guides AI agents (Claude Code, opencode, …) working in this
repository. It has two layers: how to run a task end-to-end (the
sections below), and how the codebase is built (the technical sections
further down, formerly in CLAUDE.md).
Жизненный цикл задачи
1. Старт: синхронизация с develop
Перед началом любой работы обнови локальный develop и ветвись от него:
git checkout develop
git fetch gitea
git pull --ff-only gitea develop
git checkout -b <короткое-имя-фичи>
Никогда не пилит фичу прямо в develop и не ветвись от устаревшего
develop — иначе PR будет содержать лишние коммиты или конфликтовать.
2. Реализация
Веди задачу по workflow из системного промпта (Phase 1 анализ → Phase 3 реализация → Phase 4 review → Phase 5 верификация → Phase 6 отчёт). Большие изменения делегируй в general subagent, ревьюй через review subagent.
Worktree'ы создавай только внутри папки .claude (например,
.claude/worktrees/<имя>). Создавать git worktree где-либо ещё — в корне
репозитория, в соседних каталогах или во временных папках — запрещено.
3. Коммит — ТОЛЬКО в Gitea и ТОЛЬКО от claude_code
Это правило без исключений:
- Куда: единственный remote для коммитов/пушей —
gitea(gitea.vvzvlad.xyz). Никогда не пушь вorigin(GitHub-зеркало) и тем более вupstream(оригинальный Docmost). GitHub-зеркало обновляется CI-процессом владельца, не агентом. - От кого: коммить только от агентского identity. Любой коммит,
у которого author или committer —
vvzvlad, считается ошибкой и должен быть переписан.- name:
claude_code - email:
claude_code@vvzvlad.xyz
- name:
Используй --reset-author при amend, иначе git оставит оригинального
автора (по умолчанию config на этой машине — vvzvlad, поэтому проверяй
после каждого коммита):
GIT_AUTHOR_NAME="claude_code" \
GIT_AUTHOR_EMAIL="claude_code@vvzvlad.xyz" \
GIT_COMMITTER_NAME="claude_code" \
GIT_COMMITTER_EMAIL="claude_code@vvzvlad.xyz" \
git commit --amend --no-edit --reset-author
Для обычного нового коммита достаточно один раз выставить локальный config ветки и коммитить штатно:
git config user.name "claude_code"
git config user.email "claude_code@vvzvlad.xyz"
Проверка перед push:
git log -1 --format='Author: %an <%ae>%nCommitter: %cn <%ce>'
# обе строки должны показать claude_code <claude_code@vvzvlad.xyz>
4. Push и PR в develop
PR всегда в develop. Пароль claude_code лежит в macOS keychain как
generic password под service gitea-claude-code (не дублируй его как
internet-password для gitea.vvzvlad.xyz — это создаст конфликт с учёткой
владельца в git credential helper):
AGENT_PASS=$(security find-generic-password -s gitea-claude-code -w)
Push — через временную подстановку кредов в remote URL, после чего URL обязательно возвращается в чистый вид (пароль не должен оседать в git config / reflog):
ORIG_URL=$(git remote get-url gitea)
SAFE_PASS=$(python3 -c "import urllib.parse,sys;print(urllib.parse.quote(sys.argv[1]))" "$AGENT_PASS")
git remote set-url gitea "https://claude_code:${SAFE_PASS}@gitea.vvzvlad.xyz/vvzvlad/gitmost.git"
git push -u gitea <branch>
git remote set-url gitea "$ORIG_URL"
unset AGENT_PASS SAFE_PASS
PR создаётся через Gitea REST API (Basic Auth от claude_code):
curl -s -X POST \
-u "claude_code:$(security find-generic-password -s gitea-claude-code -w)" \
-H "Content-Type: application/json" \
-d @pr_body.json \
"https://gitea.vvzvlad.xyz/api/v1/repos/vvzvlad/gitmost/pulls"
base: develop, head: <branch>. В теле PR — что сделано, что вне scope,
результаты верификации (tsc/lint/tests).
Если push падает с
User permission denied for writing— значит уclaude_codeнет коллабораторских прав на репо. Попроси владельца добавить (один раз, через Gitea UI илиPUT /api/v1/repos/vvzvlad/gitmost/collaborators/claude_codeс{"permission":"write"}от его учётки).
5. Мерж и cleanup
- Мерж PR в develop делает пользователь (не агент). Агент не жмёт кнопку merge.
- После реализации задачи удали её план из
docs/backlog/<task>.md— это часть закрытия задачи, не пользовательская работа. Файлы вdocs/backlog/— это очередь работы, выполненное из неё вычищается. Сделай это в отдельном коммите от того жеclaude_codeв той же ветке (или попроси пользователя удалить, если PR уже открыт и ты не хочешь его перепушивать). - Не закоммичен ли мусор в рабочем дереве? Проверь
git statusперед финальным отчётом.
Релизный цикл: набор на новую версию
Когда в develop накопилось достаточно изменений для релиза, запускается
финальное ревью тремя скиллами-оркестраторами перед мержем/тегом:
- test-orchestrator (skill
code-review-orchestratorс фокусом на тестовом покрытии) — проверяет, что новый код покрыт тестами и нет регрессий в существующих. - review-orchestrator (skill
code-review-orchestrator) — мульти-аспектный код-ревью: безопасность, стабильность, соответствие конвенциям, регрессии, перегруженность. - red-team-orchestrator (red-team скилл) — адверсариальный анализ атакующих сценариев на затронутые компоненты.
Порядок: оркестраторы возвращают списки находок → агент правит всё, что они нашли (через subagent или сам, по правилам делегирования) → повторно прогоняет ревью затронутых мест → режет тег по процедуре «Cutting a release» ниже.
Шпаргалка по учёткам и endpoint'ам
| Что | Значение |
|---|---|
| Единственный remote для коммитов | gitea → https://vvzvlad@gitea.vvzvlad.xyz/vvzvlad/gitmost.git |
| Агентский user (Gitea/git) | claude_code |
| Агентский email | claude_code@vvzvlad.xyz |
| Пароль в keychain | security find-generic-password -s gitea-claude-code -w |
| PR API | https://gitea.vvzvlad.xyz/api/v1/repos/vvzvlad/gitmost/pulls (тут gitmost — реальный slug репо на сервере) |
| Базовая ветка | develop |
origin |
GitHub-зеркало vvzvlad/gitmost — не пушить, обновляется CI владельца |
upstream |
Оригинальный Docmost — не пушить никогда |
Архитектура и кодовая база
What this is
Gitmost is a community fork of Docmost — an open-source collaborative wiki / documentation app. The fork's defining constraint: 100% open, AGPL-only, with no Enterprise-Edition (EE) code. The upstream apps/server/src/ee, apps/client/src/ee and packages/ee directories were deleted; there is no license gating or feature-flag wall. Features that upstream hides behind the enterprise license (comment resolution, the embedded /mcp server, the AI agent chat) are re-implemented from scratch on the community codebase.
Naming gotcha: only the product is rebranded. Internal identifiers are still docmost everywhere — npm package names (docmost, @docmost/mcp, @docmost/editor-ext), the default DB name, env-var prefixes (MCP_DOCMOST_*), and the TS path aliases (@docmost/db/*, @docmost/transactional/*). Do not "fix" these to gitmost; they are load-bearing for Docmost data/image compatibility (the DB schema is a strict superset of Docmost's, so an existing instance migrates by swapping images).
Monorepo layout
pnpm workspace (pnpm@10.4.0) orchestrated by Nx. Four workspace packages:
| Path | Name | Stack | Role |
|---|---|---|---|
apps/server |
server |
NestJS 11 + Fastify, Kysely (Postgres), Redis | Backend API, collaboration, AI |
apps/client |
client |
React 18 + Vite + Mantine 8 + TanStack Query + Jotai | SPA frontend |
packages/editor-ext |
@docmost/editor-ext |
Tiptap/ProseMirror | Shared Tiptap node/mark extensions, imported by both the client and the server |
packages/mcp |
@docmost/mcp |
MCP SDK, Tiptap, Yjs | Standalone MCP server, also bundled into the server at /mcp. Does not import editor-ext — it keeps its own vendored mirror of the schema in packages/mcp/src/lib/ |
build targets are Nx-cached and dependency-ordered (dependsOn: ["^build"]), so editor-ext builds before the apps. nx.json sets affected.defaultBase: main.
Commands
Run from the repo root unless noted. The dev workflow needs Postgres (with the pgvector extension) and Redis reachable per .env (copy .env.example → .env).
pnpm install # install all workspaces (uses pnpm patches; see package.json `pnpm.patchedDependencies`)
pnpm dev # client (Vite) + server (Nest watch) concurrently — primary dev loop
pnpm client:dev # frontend only (Vite proxies /api to APP_URL)
pnpm server:dev # backend only (nest start --watch)
pnpm build # nx run-many -t build (all packages)
pnpm collab:dev # run the collaboration server process standalone (see "Two server processes")
Lint (per package — there is no root lint script):
pnpm --filter server lint # eslint --fix on server .ts
pnpm --filter client lint # eslint on client
Tests (per package — no root test script):
pnpm --filter server test # Jest, matches *.spec.ts under src
pnpm --filter server test -- ai-chat.service # single file by name pattern
pnpm --filter server test -- -t "resolves a comment" # single test by name
pnpm --filter client test # Vitest (vitest run)
pnpm --filter client test -- message-list # single Vitest file by name
pnpm --filter @docmost/mcp test # node --test (unit + mock)
pnpm --filter @docmost/mcp test:e2e # MCP end-to-end against a live instance
Database migrations (Kysely, run from apps/server; they auto-run on server startup too):
pnpm --filter server migration:create --name=my_change # new empty migration
pnpm --filter server migration:latest # apply all pending
pnpm --filter server migration:down # revert last
pnpm --filter server migration:codegen # regenerate src/database/types/db.d.ts from the live DB
Migration files live in apps/server/src/database/migrations/ and are named YYYYMMDDThhmmss-description.ts. Fork-specific migrations only add tables (page_embeddings, ai_chats, ai_chat_messages, ai_provider_credentials, ai_mcp_servers, page_template_references) and columns (e.g. pages.is_template, a NOT NULL DEFAULT false boolean) — never drop/rewrite Docmost data.
Migration ordering — always check when merging branches/features. Kysely runs migrations in alphabetical (= timestamp) order and refuses to start if a new migration sorts before one already applied to the DB (corrupted migrations: ... must always have a name that comes alphabetically after the last executed migration). When you merge a branch or land a feature, verify your migration's timestamp still sorts after every migration that may already be applied on the target (/bin/ls -1 apps/server/src/database/migrations | sort | tail). Branches developed in parallel routinely break this: a feature branch adds …T130000-…, main meanwhile ships and deploys …T150000-…, and after the merge the older-timestamped file is rejected at boot. Fix = rename your migration to a timestamp after the latest one already in the target (content unchanged — the filename is the ordering key), then rebuild so the compiled dist/database/migrations/ picks up the new name.
Architecture — the big picture
Two server processes
apps/server builds one codebase but runs as two distinct entrypoints, both required in production:
- API server —
dist/main(apps/server/src/main.ts), the Fastify HTTP app (AppModule). - Collaboration server —
dist/collaboration/server/collab-main(pnpm collab), a Hocuspocus/Yjs WebSocket server (apps/server/src/collaboration/) handling real-time document editing, persistence, and page-history snapshots. It listens onCOLLAB_PORT(default3001), separate from the API server'sPORT(default3000), and shares state with the API server through Redis.
The API server is a Fastify app with a global /api prefix (main.ts excludes robots.txt, public share pages, and mcp from the prefix). A preHandler hook enforces that a resolved workspaceId exists for most /api routes (multi-tenant by hostname/subdomain via DomainMiddleware). Auth is JWT (cookie + bearer); authorization is CASL (core/casl) — every data access is scoped to the user's abilities.
Module structure (server)
AppModule wires integration modules (integrations/*: storage [local/S3/Azure], mail, queue [BullMQ on Redis], security, telemetry, throttle, mcp, ai) plus CoreModule, DatabaseModule, and CollaborationModule. CoreModule (core/*) holds the domain modules: page, space, comment, workspace, user, auth, group, attachment, search, share, ai-chat, etc. Each domain module follows NestJS controller → service → repo layering; DB repos live under database/repos and are injected app-wide from the global DatabaseModule.
EE removal artifact: app.module.ts still contains a try/require('./ee/ee.module') stub. That path no longer exists, so the require fails and is swallowed (it only hard-exits when CLOUD === 'true'). Treat EE as gone — do not add code that depends on it.
Persistence
- Postgres via Kysely (
nestjs-kysely), typed by the generatedsrc/database/types/db.d.ts. Use the camelCase Kysely query builder, not an ORM. After schema changes, write a migration and regenerate the DB types. - pgvector is mandatory — the RAG feature stores embeddings in
page_embeddings.docker-compose.ymlusespgvector/pgvector:pg18for this reason; the stockpostgresimage will fail theCREATE EXTENSION vectormigration. - Redis backs caching, the BullMQ queues, the WebSocket Socket.IO adapter, and collaboration sync.
The two AI subsystems (the main fork additions)
- Embedded MCP server (
integrations/mcp/+packages/mcp). The standalone@docmost/mcpserver (38 agent-native tools: per-block patch/insert/delete by id, scripted(doc)=>doctransforms with dry-run diff, table editing, version diff/restore, comments, images, shares) is bundled and served over HTTP at/mcp. It writes through Docmost's real-time-collaboration layer so concurrent human edits aren't clobbered. Each request authenticates per-user via theAuthorizationheader — either HTTP Basic (base64(email:password), the user's own Docmost login, validated throughAuthService) or a Bearer access JWT (the user'sauthToken) — and the session acts under that user's permissions.MCP_DOCMOST_EMAIL/MCP_DOCMOST_PASSWORDare an optional service-account fallback, used only when a request carries neither Basic nor Bearer credentials (back-compat for CI/scripts). An admin enables MCP with a workspace toggle (Workspace settings → AI). Optionally protected by a sharedMCP_TOKEN: when set, every/mcprequest must carry a matchingX-MCP-Tokenheader (its own header, separate fromAuthorization, which now carries the per-user Basic/Bearer credentials). Note: this changed from the olderAuthorization: Bearer <MCP_TOKEN>scheme — see.env.exampleand the CHANGELOG Breaking Changes entry. - AI agent chat (
core/ai-chat/server +apps/client/src/features/ai-chat/client). A built-in agent over the wiki using the Vercel AI SDK (ai,@ai-sdk/*) against any OpenAI-compatible provider configured per workspace (integrations/ai/— credentials encrypted at rest viaintegrations/crypto, stored inai_provider_credentials). Key pieces:core/ai-chat/tools/— the agent's ~40 read+write tools. Every tool runs under the calling user's CASL permissions via a per-user loopback access token (docmost-client.loader.ts), so the agent can never exceed what the user could do. Only reversible operations are exposed (page history + trash; no permanent delete). Agent edits get an "AI agent" provenance badge in page history (20260616T130000-agent-provenancemigration).core/ai-chat/embedding/— RAG indexer + a BullMQ consumer onAI_QUEUEthat embeds pages intopage_embeddings(vector search), complementing Postgres full-text search. Pages are (re)indexed on edit;AI_EMBEDDING_TIMEOUT_MSbounds a hung embeddings endpoint.core/ai-chat/external-mcp/— admins can attach external MCP servers (e.g. Tavily) to give the agent web access.ssrf-guard.tsvalidates outbound MCP URLs against SSRF — keep that guard in the path when touching external-MCP connection logic.
Client structure
Vite SPA. Code is organized by feature under apps/client/src/features/* (mirrors the server domains: page, space, comment, ai-chat, editor, …). Conventions:
- TanStack Query for server state (one
queries/file per feature), Jotai atoms for local/shared UI state, Mantine 8 + CSS modules (*.module.css) +postcss-preset-mantinefor UI. - The editor is Tiptap; shared node/mark extensions live in
packages/editor-extand are imported by both the client and the server (collaboration, import/export) — editor schema changes often need to be made ineditor-ext, not just the client. Notepackages/mcpdoes not depend oneditor-ext; it carries its own mirrored copy of the schema, so keep the two in sync manually when the document schema changes. - API access goes through
apps/client/src/lib/api-client.ts(axios). The@alias maps toapps/client/src. - Runtime config is injected at build time by
vite.config.tsviadefine(APP_URL,COLLAB_URL,APP_VERSION, …) — these come from the root.env, not fromimport.meta.env.
Conventions
- Code comments must be in English.
- Errors must never be swallowed or shown as generic messages. Every caught error MUST (1) be logged in full to the console/logger — error name, message, stack,
cause, and (for HTTP/provider failures) the status code and response body — and (2) be surfaced to the user with a specific, human-readable explanation of what actually went wrong, never a bare generic string like "Something went wrong" / "Could not start recording" / "Transcription failed". Include the real reason (the underlying error/provider message) in the user-facing text. On the server, wrap third-party/provider failures withdescribeProviderError(or equivalent) and rethrow as a meaningful HTTP status + message — never let them collapse into an opaque 500. On the client,console.error(<context>, err)the raw error AND show the extracted reason (e.g.err.response?.data?.message, or the errorname: message) in the notification. - The version string shown in the UI comes from
APP_VERSION(CI/Docker) orgit describe --tags --always(local), resolved invite.config.ts— not frompackage.json. - Server TS config is permissive (
noImplicitAny: false,strictNullChecks: false,no-explicit-anylint disabled). Follow the existing relaxed style rather than tightening types broadly. - Dependency versions are heavily pinned via
pnpm.overridesandpnpm.patchedDependencies(scimmy,yjs) in the rootpackage.json. Don't bump pinned/patched deps casually; the patches and overrides exist for compatibility/security reasons.
CI / release
.github/workflows/develop.yml— on push todevelop, builds and pushesghcr.io/vvzvlad/gitmost:develop..github/workflows/release.yml— onv*tags (or manual dispatch), builds multi-arch (amd64 + arm64) images, pushes a manifest list to GHCR (latest+ semver tags), and creates a draft GitHub Release with image tarballs. Uses the built-inGITHUB_TOKEN(not Docker Hub).- The
Dockerfileis a multi-stage pnpm build;APP_VERSIONis passed as a build arg because.gitisn't in the build context.
Cutting a release
The git tag is the source of truth for the displayed version (UI reads git describe --tags); the package.json bump is metadata only. Steps:
- Make sure
mainis clean and pushed (git status,git push). - Pick
vX.Y.Z(SemVer): minor bump for a batch of features, patch for fixes only. Review what landed withgit log <last-tag>..HEAD --no-merges. - Bump
"version"toX.Y.Zin the rootpackage.json,apps/client/package.json, andapps/server/package.json(keep all three in sync). Leavepackages/mcpalone — it is versioned independently. Commit with the bare version as the subject, e.g.0.91.0(matches past bump commits). - Update
CHANGELOG.md(Keep a Changelog format): add a## [X.Y.Z] - YYYY-MM-DDsection summarisinggit log vPREV..HEAD --no-mergesgrouped by type (Breaking / Added / Changed / Fixed / Removed), and add thecompare/vPREV...vX.Y.Zlink at the bottom. Fold the bump + changelog into the release commit. - Tag the release commit with a lightweight tag (existing release tags are lightweight):
git tag vX.Y.Z. - Push commit and tag:
git push origin main && git push origin vX.Y.Z. Pushing thev*tag triggersrelease.yml(multi-arch GHCR images + a draft GitHub Release). - Back-merge the release into
developso develop builds report the new version:git checkout develop && git merge --no-ff main && git push origin develop(push to Gitea as well if that is the canonical remote).
Why develop keeps showing the previous version (and why step 7 matters)
The UI version is git describe --tags --always (see vite.config.ts), which walks backwards from the current commit and picks the nearest tag reachable in that commit's ancestry, then appends -<commits-since-tag>-g<short-hash>.
The release tag (vX.Y.Z) is created on main's release merge commit, and that commit is not in develop's history. So until the release is back-merged, git describe on develop cannot see the new tag and falls back to the previous reachable tag. Result: every develop build — and the ghcr.io/vvzvlad/gitmost:develop image — keeps reporting e.g. v0.91.0-NNN-g<hash> even though main is already tagged v0.93.0. This is the classic git-flow pitfall: the version on develop does not advance just because a release was tagged on main.
Back-merging main → develop (step 7) pulls the tagged release commit into develop's ancestry, after which develop builds correctly show vX.Y.Z-NNN-g<hash>. If develop already drifted (release tagged but never back-merged), just run step 7 now — no new tag is needed.
The tag must also exist on the remote that CI builds from (multi-remote gotcha)
git describe names a tag ref, not just a commit — so the back-merge is necessary but not sufficient. The develop image is built by GitHub Actions (develop.yml, actions/checkout with fetch-depth: 0, then git describe --tags --always), so the version it prints depends on which tags exist on the github remote, not on your local clone or on gitea.
This repo has two writable remotes — gitea (canonical, where commits land) and github (where the :develop and release images are built) — plus upstream (docmost, never push). git push <branch> does NOT push tags; tags must be pushed explicitly and to each remote separately. A release tag that only lives on gitea is invisible to the GitHub Actions build: even with the tagged commit fully in develop's history (step 7 done), git describe on the GitHub runner falls back to the previous tag it does have, so the develop image keeps showing e.g. v0.91.0-NNN while git describe locally already says v0.93.0-NN.
Fix / checklist when develop still shows the old version after a back-merge:
- Confirm the tag is missing on github:
git ls-remote --tags github(compare withgitea). - Push it there:
git push github vX.Y.Z(andgit push gitea vX.Y.Zif it is missing on gitea too). Note: pushing av*tag togithubalso triggersrelease.yml(multi-arch GHCR images + draft Release) — expected, but be aware. - Re-run the develop build (
gh workflow run Develop, or push any commit todevelop) sogit describere-resolves with the tag now present.
(The git push origin ... in steps 6–7 above is shorthand — there is no origin remote here; substitute gitea and github as appropriate, and always push release tags to both.)
Planning docs
docs/*.md hold design plans for in-progress / planned features (mobile app, offline sync, RAG improvements, voice dictation). Arbitrary HTML embed has shipped — it renders inside a sandboxed iframe and, when the htmlEmbed workspace toggle is on, is insertable by any member (no longer admin-only); turning the toggle off hides/stops serving existing embeds on public share pages. docs/backlog/*.md track known issues / follow-ups (e.g. AI-chat review follow-ups). Consult the relevant plan before working on one of those areas.