Update the task lifecycle documentation from Russian to English to improve readability for English‑speaking contributors and ensure consistency across the repository.
25 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).
Task lifecycle
1. Start: sync with develop
Before starting any work, update your local develop and branch off it:
git checkout develop
git fetch gitea
git pull --ff-only gitea develop
git checkout -b <short-feature-name>
Never build a feature directly on develop, and never branch off a stale
develop — otherwise the PR will carry extra commits or conflict.
2. Implementation
Run the task through the workflow from the system prompt (Phase 1 analysis → Phase 3 implementation → Phase 4 review → Phase 5 verification → Phase 6 report). Delegate large changes to a general subagent; review via the review subagent.
Create worktrees only inside the .claude folder (e.g.
.claude/worktrees/<name>). Creating a git worktree anywhere else — the repo
root, sibling directories, or temp folders — is forbidden.
3. Commit — ONLY to Gitea and ONLY as claude_code
This rule has no exceptions:
- Where: the only remote for commits/pushes is
gitea(gitea.vvzvlad.xyz). Never push toorigin(the GitHub mirror), and especially not toupstream(the original Docmost). The GitHub mirror is updated by the owner's CI process, not by the agent. - Who: commit only as the agent identity. Any commit whose author or
committer is
vvzvladis an error and must be rewritten.- name:
claude_code - email:
claude_code@vvzvlad.xyz
- name:
Use --reset-author when amending, otherwise git keeps the original author
(the default config on this machine is vvzvlad, so check after every commit):
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
For a regular new commit, set the branch-local config once and commit normally:
git config user.name "claude_code"
git config user.email "claude_code@vvzvlad.xyz"
Check before push:
git log -1 --format='Author: %an <%ae>%nCommitter: %cn <%ce>'
# both lines must show claude_code <claude_code@vvzvlad.xyz>
4. Push and PR to develop
PRs always target develop. The claude_code password lives in the macOS
keychain as a generic password under service gitea-claude-code (do not
duplicate it as an internet-password for gitea.vvzvlad.xyz — that creates a
conflict with the owner's account in the git credential helper):
AGENT_PASS=$(security find-generic-password -s gitea-claude-code -w)
Push by temporarily injecting the credentials into the remote URL, then always restore the URL to its clean form (the password must not linger in 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
The PR is created via the Gitea REST API (Basic Auth as 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>. In the PR body: what was done, what is out
of scope, verification results (tsc/lint/tests).
If push fails with
User permission denied for writing, thenclaude_codelacks collaborator rights on the repo. Ask the owner to add them (once, via the Gitea UI orPUT /api/v1/repos/vvzvlad/gitmost/collaborators/claude_codewith{"permission":"write"}from their account).
5. Merge and cleanup
- The user merges the PR into develop (not the agent). The agent does not press the merge button.
- After implementing a task, delete its plan from
docs/backlog/<task>.md— this is part of closing the task, not the user's work. Files indocs/backlog/are the work queue; completed items get cleaned out of it. Do this in a separate commit from the sameclaude_codeon the same branch (or ask the user to delete it if the PR is already open and you don't want to repush it). - Any junk left uncommitted in the working tree? Check
git statusbefore the final report.
Release cycle: staging a new version
When enough changes have accumulated on develop for a release, a final
review by three orchestrator skills runs before the merge/tag:
- test-orchestrator (the
code-review-orchestratorskill focused on test coverage) — verifies new code is covered by tests and there are no regressions in existing ones. - review-orchestrator (the
code-review-orchestratorskill) — multi-aspect code review: security, stability, convention conformance, regressions, over-complexity. - red-team-orchestrator (the red-team skill) — adversarial analysis of attack scenarios against the affected components.
Order: the orchestrators return finding lists → the agent fixes everything they found (via a subagent or itself, per the delegation rules) → re-runs the review on the affected areas → cuts the tag per the "Cutting a release" procedure below.
Accounts & endpoints cheat sheet
| Item | Value |
|---|---|
| Only remote for commits | gitea → https://vvzvlad@gitea.vvzvlad.xyz/vvzvlad/gitmost.git |
| Agent user (Gitea/git) | claude_code |
| Agent email | claude_code@vvzvlad.xyz |
| Keychain password | security find-generic-password -s gitea-claude-code -w |
| PR API | https://gitea.vvzvlad.xyz/api/v1/repos/vvzvlad/gitmost/pulls (here gitmost is the repo's real slug on the server) |
| Base branch | develop |
origin |
GitHub mirror vvzvlad/gitmost — do not push, updated by the owner's CI |
upstream |
The original Docmost — never push |
Architecture and codebase
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.