Refs: #1841, #890 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
13 KiB
Contributing — DeskcommCRM
🇧🇷 Português · 🇺🇸 English
Before you start
- Open the repository in your code assistant (Claude Code, Codex, Cursor, OpenCode or
Antigravity): the
deskcomm-contribuirguide (.agents/skills/deskcomm-contribuir/SKILL.md) measures, before the PR, what triage measures after — stale branch, migration triple, fork mark in the diff, release fragment — and arms the git hooks withbash .agents/skills/deskcomm-contribuir/scripts/armar-hooks.sh. To have the guides in any folder:bash scripts/instalar-guias.sh. Going to edit a guide? Runbash scripts/instalar-guias.sh --fonte .in your clone — in Claude Code the global skill beats the project one, and without it you would be testing themainversion, not yours. - Read
CLAUDE.md— non-negotiable conventions. - Read
ARCHITECTURE.md— the 1-page view. - Identify the source epic in
docs/stories/epics/MASTER.md.
Flow
Branches
feat/EPIC-XX-short-slug # new feature
fix/EPIC-XX-short-slug # bug fix
chore/short-slug # chore (deps, configs)
docs/short-slug # docs only
Commits
Conventional commits + EPIC-XX scope:
feat(EPIC-04): kanban drag-and-drop with fractional indexing
fix(EPIC-03): cron recover-stuck-messages marks sending stuck >5min as failed
docs(EPIC-12): mark complete + wave log
PT-BR messages are accepted. The subject must be imperative and ≤72 chars.
epic-executor
Large changes follow docs/stories/epics/. The epic-executor consumes the frontmatter (epic_id, priority, depends_on, status) and executes wave-by-wave with continuous E2E validation.
When finishing an epic:
- Update frontmatter
status: pending → completed (partial: ...)orstatus: completed. - Append "Wave Completion Log" at the end of the file.
- Update the corresponding row in
docs/stories/epics/MASTER.md.
PR process
-
Branch from
main. -
Implement. Add tests (E2E for flows, unit for pure logic).
-
Definition of Done. The list is split in two for a reason: until today it mixed what a machine rejects with what only a person notices, and a contributor ticked the whole checklist in good faith only to be stopped by a gate nobody had told him about.
What CI rejects by itself — run it before opening the PR and there will be no surprise:
pnpm cercas # ~30 s: the structural guards (baseline, MANIFEST, docs, workflows, Spanish i18n, .changes/ fragments) — what rejects the most PRs pnpm typecheck && pnpm lint && pnpm lint:channels && pnpm test:unit && pnpm test:shell && pnpm build pnpm test:db # needs Docker; brings up a clean Postgres and applies the baselineWhat CI does NOT see — it stays with you and with review, and it is where the expensive defects live:
- RLS enabled and policy
tenant_isolation_<tabela>_allif you created a tenant-aware table (the isolation test covers a fixed list of tables; your new one does not get in by itself) - Audit log emitted if there is a relevant mutation
- Rate limit applied if the route is public
- Zod validating every external input
- No forgotten
console.log(uselib/logger.ts).pnpm lintdoes not reject this — the rule is a warning, so it passes green; the check is human - New env vars in
.env.exampleandlib/env.ts, with a default that does not break a fresh install - Schema change shipped as a triple: file in
supabase/migrations/, idempotent appendix insupabase/baseline.sqland a line inMANIFEST.md. The self-host kit applies only the baseline — a migration that never gets there never reaches whoever installed on a VPS. No CI job checks this - If you touched
Dockerfile*,docker-compose*.ymlorhostgator-setup-kit/: the change reaches people who already installed. Law indocs/doctrine/packaging.md. CI rejectsbuild:-only service, install on a moving tag and broken image (imagens-ok); what stays with you is the rest: new variable with a default that does not break an old.env, and the update not asking for manual file editing. No bump may require the VPS operator to edit anything by hand — if it does, open an issue with a migration plan instead of a PR - Docs updated if the contract changed (PRD/spec)
pnpm test:e2e(relevant subset) — optional if you contribute from outside, see below
- RLS enabled and policy
-
Open a PR against
main. The description must reference the epic and list evidence (logs/screenshots of the tests). -
Touched an authority document? Fix the state claims of that document — the ones saying what is active, what is missing, what points where. Do not go hunting in the others: the debt decays by itself if nobody feeds it. Measured findings, with the command for each, in
docs/audits/2026-08-14-afirmacoes-de-estado.md. -
CI must pass before merge. Required:
verify,invariants(RLS isolation),build-and-size,e2eandimagens-ok.imagens-ok(in.github/workflows/publish-image.yml) builds the three images that the self-hoster installs, runs on PRs and blocks since 2026-08-13.Green on
e2eis not "journey proved": it prints, in its own summary, which specs it did not cover. Which ones, read from the workflow itself rather than from this line — it already said the one left out wasvps-fresh-onboarding, the install from scratch, and since PR #983 that one runs in CI:git show origin/main:.github/workflows/e2e.yml | grep -A4 'FORA_DO_CI:'And even a journey that HAS a gate still owes the proof on screen when you change it (DoD 12): the gate proves it did not regress, not that the experience got good.
This list used to say "three required" and called
e2enon-blocking. It was out of date on both points, and whoever used it as a ruler would measure against the wrong ruler. Check the source before trusting any written list:gh api repos/melgarafael/DeskcommCRM/branches/main/protection --jq '.required_status_checks.contexts'
Claiming an issue — the protocol
It exists because we already failed at this: on 2026-07-30 we opened an issue, a contributor started solving it, and a maintainer shipped the same fix 21 seconds before without either of them being able to see the other. Their work went to the trash. The rules below exist so that this does not repeat.
- Comment "pego esta" before coding. One line is enough. A maintainer assigns the issue to you — from that point on it is yours and nobody else touches it.
- An issue with an assigned person is not duplicated. If you still want to help, comment offering; do not open a competing PR.
- A maintainer does not implement an issue labeled
good first issueorhelp wantedwithout first assigning it to themselves publicly. If you see one without an owner, it is yours to take — that is the guarantee we give in exchange for step 1. - No answer within 48h after "pego esta"? Start anyway and say so in the PR. The delay is ours, the cost cannot be yours.
If you are contributing from outside (fork) — read this
One thing will look like your mistake and it is not:
- The workflows sit waiting for approval on your first PR. It is GitHub policy for anyone who has never contributed before. A maintainer releases it; from the second PR on it runs by itself. If it takes long, comment on the PR.
Open the PR from a named branch, never from your fork's main. If the fork's
main already has your customizations — and it almost always does, because that is where your VPS pulls from —, the PR proposes
those customizations to the whole product. It causes no conflict and lights up no gate: they
enter silently into every installation. It was measured (PR #465): seven files with the mark of a
client, six of them merging without a single conflict. The path is git checkout -b fix/o-que-voce-conserta
from this repository's main, with only your fix inside.
With "Allow edits by maintainers" on in your PR, the project can push a fix straight onto the
PR branch — a mechanical adjustment, or main brought in when there is a conflict. Always as a
new commit: never --force, never rebase, and your commits stay as they are. We announce it in the PR before
pushing. When that happens, bring the branch in before continuing (git pull --no-rebase) and only
then push again; a --force on your side would erase what was pushed from our side. With the
option off, the fix goes to a branch of ours. In both paths, the work that is yours lands with
you as its author.
Your installation's brand is not changed by editing code. Do not change DEFAULT_APP_NAME in
lib/branding.ts, nor the titles in app/. The database rules (platform_branding,
organizations.settings.branding), APP_NAME in .env is the seed that install.sh asks for,
and the rest is the Settings › Brand screen. Full recipe in docs/white-label.md.
Editing the constant changes the PRODUCT's default — and your brand disappears on the next git pull, which is the
practical reason the supported path is better for you too.
And about the pnpm test:e2e in the DoD: running the full suite requires Docker, a seeded database and local
WAHA. We do not hold external PRs on it — send what you managed to prove (unit + description of
what you tested by hand), and the proof on screen stays with the maintainer. Requiring proof without delivering the
tool to produce it would be a toll, not rigor.
tests/invariants/ is frozen — and this covers BEHAVIOR, not just the file
The files in tests/invariants/ hold laws of the product, and touching them asks for written
justification. Two things that are not obvious and have already cost contributors time:
- The guard is a local hook of the maintainer (
core.hooksPath=loop/hooks), not a CI check. You will not see it reject in your fork — what you see is the integration breaking later. - A PR can reject an invariant without touching its file. If your fix changes the behavior the law asserts, the red shows up there. This is not your oversight — it is the sign that two concurrent rules exist: the one written and the one you propose.
When it happens, do not delete or loosen the assertion: say in the PR what your reason is and leave the choice explicit. Whoever triages writes the change to the invariant with the required justification, or adjusts the fix to preserve the old law — and the decision is recorded in the PR, which is where it serves the next person.
On-screen text: every new sentence needs Spanish
The product speaks Portuguese and Spanish, and CI rejects a new sentence without a translation. The rule was not written here until 09/16/2026, and a first-contribution PR was rejected because of it — the failure was ours, not the contributor's.
If you added a sentence that appears on screen, it goes through t("...") and gets a line
in lib/i18n/dicionario.ts:
"Digite o identificador do modelo": { es: "Escribe el identificador del modelo" },
The key is the Portuguese text (not a code). Only Spanish needs a line; the rest degrades to Portuguese on purpose.
To check before opening the PR, without running the whole suite:
pnpm test:unit tests/unit/i18n-espanhol-cobre-a-tela.test.ts
It rejects in both directions: a key used on screen without Spanish, and Portuguese prose that did not
go through t(). If you don't speak Spanish, send it anyway and say so in the PR — the translation is
ten seconds of work for whoever triages, and no reason to hold back a fix.
Prohibited anti-patterns
Full list in CLAUDE.md. The most lethal:
- Postgres trigger doing HTTP
- Service role used in a handler without filtering
organization_idmanually getSession()on the backend (usegetUser())- API key in query string
- Bearer plaintext in the DB
console.login merged code
Local setup
See README.en.md §Development — how to run locally.
Support
GitHub Discussions — it is the public channel, works for anyone and is where the answer stays recorded for whoever comes after. For bugs, open an issue.
If it is something that does not fit in public (security, for example): rafael@maudibrasil.com.br — the same
address as in CODE_OF_CONDUCT.md.
This section used to point to an internal Discord whose invite lives in a private Notion — unreachable precisely for whoever needed it most, who is the one coming from outside. It stays here as a reminder that a support channel is tested from the outside.