Thank you for contributing to Zagrosi. This guide covers the workflow and standards for all contributions.
Note (pre-alpha): Zagrosi is currently a solo project in early design. Most code-level contribution paths below are aspirational; they describe the standard contributors will be held to once the foundation phase ships. Until then, the most useful contributions are issues, discussions, and design feedback.
- See documentation/onboarding.md for local development setup (coming during Phase 0)
- Create a feature branch from
main - Make your changes following the conventions below
- Open a pull request using the PR template
Zagrosi is licensed under AGPLv3. To keep the licensing chain clean, every commit must be signed off under the Developer Certificate of Origin. Sign off automatically with:
git commit -s -m "feat(tasks): add custom field validation"This appends a Signed-off-by: trailer asserting that you have the right to contribute the code under AGPLv3.
The main branch is permanently protected. Direct pushes are blocked. All changes (including documentation, dependency bumps, and one-line fixes) must land via pull request from a feature branch.
Protection rules enforced on main:
- Linear history required (no merge commits from outside PRs)
- Pull request required before merging
- Conventional Commits subject required on the merge commit
- DCO sign-off required on every commit
- All status checks (Rust fmt / clippy / test, web lint / typecheck / test, MCP conformance) must pass
- Force-push and deletion blocked
- Administrators are not exempt
If you find yourself wanting to push directly to main, the answer is always "open a PR", even for typo fixes.
All branches must follow this pattern:
^(feature|fix|hotfix|docs|chore|refactor|test|perf|ci)\/[a-z0-9]+-[a-z0-9-]+$
Examples:
feature/zg-42-task-custom-fieldsfix/zg-108-mcp-stdio-handshakedocs/zg-200-adr-multi-tenanthotfix/zg-99-rls-bypassrefactor/zg-150-incident-event-sourcing
The numeric prefix matches the GitHub issue ID.
We use Conventional Commits. Every commit message must start with a type prefix:
| Prefix | Use |
|---|---|
feat: |
New feature |
fix: |
Bug fix |
docs: |
Documentation only |
chore: |
Maintenance, dependencies |
refactor: |
Code restructuring (no behaviour change) |
test: |
Adding or updating tests |
perf: |
Performance improvement |
ci: |
CI/CD pipeline changes |
Include a scope when helpful, scoped to the bounded context or app:
feat(tasks): add custom field validation
fix(mcp): handle stdio EOF cleanly
refactor(incidents): switch timeline to event sourcing
chore(deps): bump rmcp to 1.6
Allowed scopes: identity, workspaces, tasks, agile, docs, chat, incidents, oncall, postmortems, search, notifications, scheduler, gateway, web, mcp, helm, compose, ci, deps.
- Create a branch following the naming convention
- Make your changes with signed-off conventional commits
- Ensure all checks pass locally:
# Rust cargo fmt --all -- --check cargo clippy --all-targets --all-features -- -D warnings cargo test --all-features # Web pnpm lint pnpm typecheck pnpm test
- Push your branch and open a PR using the template
- Request review from at least one maintainer (until the team grows, this means a self-review walkthrough recorded in the PR description)
- Address review feedback
- Merge using rebase merge (not squash) to preserve individual commit history and DCO sign-offs. The
mainbranch enforcesrequired_linear_history, so merge commits are not permitted; rebase merge keeps every commit (with itsSigned-off-by:trailer) on a linear history.
Reviewers should evaluate PRs against these categories:
- Logic is correct and handles edge cases
- No regressions to existing functionality
- Error handling is explicit (
?propagates,unwrap/expectonly in tests or proven-infallible paths) - Async code does not block the runtime (no
std::sync::Mutexheld across.await, no blocking I/O on the runtime)
- No secrets or credentials in code or fixtures
- User input is validated at boundaries (
serde+ custom validators on the Rust side, Zod schemas on the web side) - SQL injection, XSS, and CSRF protections maintained
- Postgres Row-Level Security policies cover any new tables before they ship; never disable RLS to "fix" a query
- MCP tools that mutate state require an authenticated session and respect the same RBAC as the REST endpoint they wrap
- AuthZ checks happen at the service layer, not the gateway
- No N+1 queries in
sqlxcalls; prefer joins or batchedWHERE id = ANY($1)patterns - Large datasets are paginated (cursor-based by default, not offset)
- Hot paths use prepared queries; one-shot queries use
query!macros for compile-time checking - Client bundles are not unnecessarily increased; new dependencies justified in the PR description
- New features have tests
- Edge cases are covered
- Tests are deterministic; no real time, no flaky timing dependencies. Use
tokio::time::pause()for async timers - Database tests use the per-test transaction-rollback fixture, not a shared dirty database
- Code is self-documenting with clear names
- Complex invariants have brief explanatory comments stating the why, not the what
- No dead code or commented-out blocks
- Public Rust items have rustdoc comments; exported TS types have JSDoc when not obvious from the name
- REST conventions followed for HTTP endpoints; resource-oriented URLs, correct status codes
serderequest types are explicit structs, notserde_json::Valueblobs- MCP tool definitions specify clear input schemas and human-readable descriptions (clients render them to users)
- Response shapes are consistent with existing patterns in the same bounded context
- Breaking changes to MCP tools require a version bump in the tool name (e.g.
create_task_v2)
- All new public functions, API handlers, and MCP tools must have tests
- Per-crate
tests/for integration tests; in-module#[cfg(test)]for unit tests - Database tests use
sqlx::testwith the test transaction fixture - Run with
cargo test --all-features --workspace - Coverage with
cargo llvm-cov --workspace --html
- All new utility functions and React hooks must have unit tests
- Run with
pnpm testorpnpm vitest run - Coverage with
pnpm test:coverage
- Critical user flows must have E2E coverage
- Run with
pnpm test:e2e
- Every new MCP tool must have a conformance test that drives it through
rmcp's test client - Validates input schema, output shape, error mapping, and capability advertisement
- Hot paths (search, RLS-heavy queries, NATS dispatch) have criterion benchmarks
- Run with
cargo bench - Regressions over 10% require justification or a fix
All UI changes must meet WCAG 2.1 Level AA compliance:
- Use semantic HTML elements (
<button>,<nav>,<main>, not<div>with click handlers) - Provide ARIA labels where semantic HTML is insufficient
- Ensure full keyboard navigation (no mouse-only interactions)
- Maintain minimum 4.5:1 contrast ratio for text
- Include visible focus indicators
- Test with screen reader (VoiceOver / NVDA)
- Realtime/collaborative surfaces (docs, chat) announce updates via ARIA live regions, not silent DOM mutations
cargo fmtis enforced on CI; no exceptionscargo clippy --all-targets --all-features -- -D warningsmust pass; fix the warning, do not allow it- No
unwrap()orexpect()outside tests and proven-infallible call sites; use?and typed errors (thiserrorper crate,anyhowonly at binary boundaries) - No
unsafeblocks without an accompanying// SAFETY:comment justifying every invariant - Prefer
tracingoverlogfor instrumentation; structured fields, not formatted strings
- TypeScript strict mode is enabled; follow it
- ESLint runs on CI; fix all warnings before pushing
- No
anytypes; use proper type definitions orunknownwith narrowing - No
// @ts-ignoreor// @ts-expect-error; fix the underlying type issue - No
eslint-disablecomments; fix the lint issue instead
- All schema changes go through
sqlx migrate add; never edit a committed migration - Every new table ships with its RLS policies in the same migration
- Index choices are justified in the migration's leading comment