Thanks for your interest in contributing to Neeko — a Tauri 2.0 + React 18 desktop app that unifies multi-project AI agent sessions (Local / WSL / SSH).
This guide covers how to set up the project, the coding conventions we enforce, the quality gates that run automatically, and how to get your changes merged.
中文版见 CONTRIBUTING_CN.md
- Development Environment
- Quick Start
- Common Commands
- Project Structure
- Coding Conventions
- Test-Driven Development
- Commit Message Guidelines
- Quality Gates
- Testing Requirements
- Branching & Pull Requests
- Documentation
- Release Process
| Tool | Version |
|---|---|
| Node.js | >=24 |
| pnpm | 11.25.0 |
| Rust | edition 2021 (stable) |
| Tauri | 2.0 |
Exact versions are owned by the
engines/packageManagerfields inpackage.json; the table above is only a snapshot.
Install the Tauri system prerequisites for your platform first:
- macOS: Xcode Command Line Tools (
xcode-select --install) - Linux: WebKitGTK / GTK / AppIndicator / librsvg / patchelf
- Windows: Microsoft C++ Build Tools + WebView2
See the Tauri prerequisites guide for details.
pnpm install # install frontend dependencies
pnpm tauri dev # start the dev app (frontend on port 1420)| Command | Purpose |
|---|---|
pnpm tauri dev |
Run the app in development mode |
pnpm tauri build |
Build a release bundle |
pnpm lint |
Every static check: in-process guards + lint:fe + lint:rust (no tests) |
pnpm lint:fe |
Frontend static checks (ESLint + tsc --noEmit) — the lint_fe gate's argv |
pnpm lint:rust |
Rust static checks (cargo fmt --check + clippy -D warnings) |
pnpm lint:fix |
Write-back fixes: cargo fmt (Rust) + ESLint --fix (frontend) |
pnpm type-check |
TypeScript type check only |
pnpm test |
All test gates: test:fe + test:rust + test:host |
pnpm test:fe |
Frontend tests once (test:fe:watch for watch) |
pnpm test:fe:coverage |
Frontend tests with coverage — enforces the floors in vitest.config.ts |
pnpm test:rust |
Run Rust tests |
pnpm test:rust:coverage |
Rust tests with coverage — enforces --fail-under-lines from package.json |
pnpm test:coverage |
Both coverage gates |
pnpm test:host |
Java debug host self-check (needs a JDK; skips loudly otherwise) |
pnpm check |
Everything local: lint + every test gate (one framework call) |
pnpm release <version> |
Bump version, generate changelog, tag (see Release) |
Coverage is a CI gate, not a push gate: the local hooks stay inside the latency budget, while
frontend-test runs pnpm test:fe:coverage and backend-coverage runs pnpm test:rust:coverage
(only when a PR touches src-tauri/** or package.json), so a floor that slips fails in CI.
Run the same command locally when you touch a file that has a per-file pin in vitest.config.ts,
or add untested Rust code (the floor has ~1.75pt of headroom, not more).
The per-side AGENTS.md files are the single source of truth for directory
trees and module responsibilities; this guide does not restate them (copies drift):
- Frontend (Feature-Based):
src/AGENTS.md→ "模块布局" - Backend (Domain-Driven):
src-tauri/AGENTS.md→ "模块布局" - Full-stack overview:
docs/ARCHITECTURE.md
List directories with ls / Glob instead of maintaining a copy — the root
AGENTS.md "顶层目录" section makes that a standing rule.
The single source of truth for conventions is AGENTS.md at
the repo root: its 15 "review red lines" are enforced by the
check_agents_md_size.py guard (every rule's full text lives in exactly one
file, and the red-line table is the machine-readable ledger). Below is a
location index only:
| Topic | Authoritative location |
|---|---|
| Architecture principles (cohesion/coupling, DIP, OCP, DRY-KISS-YAGNI) | AGENTS.md → "架构基本原则" |
| The 15 review red lines (Block-level; cite by number) | AGENTS.md red-line table (number → summary → home) |
| Frontend import/export firewall | src/AGENTS.md → "模块导入/导出规范" |
| Frontend state management, React performance | src/AGENTS.md → "前端架构约定" |
| Backend command layer, errors & concurrency | src-tauri/AGENTS.md → "Rust 命令层约定", "错误与并发" |
Before 2026-09-25 this guide restated all of the above and demonstrably drifted (the stale copy described
pnpm lintwrong and listed an outdated frontend tree), so it is now an index. Add new conventions to the owning file — never a copy here.
The Red → Green → Refactor loop, the per-layer coverage baseline (pure functions /
Rust managers / hooks / components) and the hard constraints (no code without
tests; tests independent and < 100ms each) live in AGENTS.md →
"TDD 开发模式" — the single source of truth, not restated here.
All new features and bug fixes in this repo must follow that loop; bug fixes start with a regression test.
We follow Conventional Commits 1.0.0, enforced by commitlint:
<type>(<scope>): <subject>
<body>
<footer>
feat, fix, refactor, chore, docs, style, perf, test, build,
ci, revert, wip
- feat — new feature
- fix — bug fix
- refactor — code refactoring (no bug fix, no new feature)
- chore — maintenance (deps, config, etc.)
- docs — documentation only
- style — formatting (no logic change)
- perf — performance improvement
- test — adding/fixing tests
- build / ci — build system / CI changes
- revert — revert a commit
- wip — work in progress (temporary)
- Scope is optional but encouraged (
feat(search): ...). - Subject is short (≤ 50 chars), imperative, no trailing period. Subject may be in English or Chinese.
- Use a body for complex changes: explain why and how.
- Mark breaking changes with
!after the type and aBREAKING CHANGE:footer. - Reference issues in the footer (e.g.
Closes #123).
feat(search): add find-in-files content search panel
Add a full-text search panel (Ctrl+Shift+F) that works across local,
WSL and SSH projects.
fix(file): refresh expanded dir caches on file move/delete
Keep commits atomic: split unrelated changes into separate commits.
lefthook runs automatically on
commit. Hooks are installed via pnpm prepare (or pnpm lefthook install).
lefthook.yml owns the hook list, and each hook is a single call into the
guard framework — which gates run in which context is declared once in
tools/guards/ledger/gates.json, never restated in lefthook.yml:
| Hook | Trigger | Runs |
|---|---|---|
pre-commit |
every commit | pnpm guards run --stage commit --staged |
commit-msg |
every commit | pnpm commitlint |
pre-push |
every push | pnpm guards run --stage push --changed {push_files} --jobs 3 |
The tests and the host self-check run at push, not at commit: commit stays a static
gate (~10s warm), push pays for the tests. Skipping now comes from each gate's
declared scopes (not from hand-written lefthook.yml globs), so a file that can
change a gate's verdict is covered by that gate's own declaration.
A gate is a guard whose verdict comes from an external command (argv) rather
than an in-process function. It is declared in tools/guards/ledger/gates.json:
Consumers need zero edits: pnpm lint / test / check / test:coverage,
lefthook.yml and the CI guards job all select by stage/kind/form. Unknown
keys are rejected at load (a typo like stage must not silently fall back to a
default), ci ⇔ ci_job, and every gate is validated against the CI workflow
and the lefthook hooks by check_gate_topology (A1/A3) — a gate declared for a
CI job whose command is not in that job's steps is a failure, not a silent no-op.
The guard list is the directory tools/guards/checks/ — dropping a module
there registers it, and none of package.json, ci.yml or lefthook.yml needs
to change (three hand-copied lists is exactly how the local and CI gate sets
drifted apart before). A module exports two things:
GUARD = Guard(id="check_my_rule", title="…", scopes=("src/**/*.ts",),
stages=("local", "ci", "commit"), red_lines=(8,), fix_hint="…")
def check(ctx: Context) -> GuardResult: ... # returns findings, never printsThe framework enforces the parts every guard used to re-invent badly:
- no vacuous pass —
scanned == 0is reported as the guard itself broke (exit 2), not as a pass; the repo root is resolved in one place by marker, never byparents[N]; - a companion test is mandatory —
tests/test_<id>.pymust exist, and the guard suite runs before any verdict is printed; - exit codes are distinguished — 0 pass / 1 violation / 2 guard error, so a
broken tool can never masquerade as "checked". A broken guard also raises a CI
::warning::, because a guard that silently died would otherwise hide inside a collapsed log group; - every guard has a time budget (
budget_ms, default 10s) — a gate slow enough that nobody runs it is a gate that has disappeared, so overrunning it is reported as guard error, not as a code violation.
pnpm guards list prints the live registry; pnpm guards list --stage ci shows
exactly what CI gates on (and names any guard left out — the local/CI gate sets
drifted apart precisely because no command could answer that before);
pnpm guards list <id> dumps that guard's ledger.
Stage a guard together with its test. lefthook run pre-commit evaluates the
staged tree, so staging tests/test_check_x.py while core/… or
checks/check_x.py stays at an older revision makes the hook run new tests
against the old framework — it will fail with something that looks like a code
bug but is really a split snapshot.
A commit is blocked until all gates pass. Before opening a PR, run the minimal
regression set locally — its definition lives in AGENTS.md →
"Development Commands" (single source of truth, not restated here).
Every "hard metric" / red line / invariant must be enforced at the strongest
level available, and its enforcement must be machine-resolvable. Register it in
tools/guards/ledger/invariants.json (pointer-only — the rule text stays in its
spec):
{
"id": "my-invariant",
"title": "…",
"tier": "structure", // type | structure | guard | lint | test | prose
"enforcement": [ { "kind": "guard", "ref": "check_my_rule" } ],
"red_line": 12 // optional; must exist in the red-line ledger
}The check_invariant_enforcement guard resolves each pointer (guard →
checks/{ref}.py, test → file, lint → rule in .eslintrc.cjs,
type/structure → file + note) and fails CI when one cannot be resolved.
Invariants that genuinely cannot be mechanized use tier: "prose" with a
reason and are printed on every run — visible debt, not silent debt. See
.trellis/spec/guides/invariant-enforcement.md
for the tier ladder and rationale.
The per-layer coverage baseline lives in AGENTS.md → "TDD 开发模式";
the frontend test framework, directory layout and mock strategy live in
src/AGENTS.md → "测试".
This guide keeps only the rule that has no other home:
- Use
tempfilefor Rust tests that touch the filesystem, and never write to the real~/.neekoconfig in tests.
- Create a branch from
main(e.g.feat/<short-name>orfix/<short-name>). - Implement following TDD, keeping commits atomic and Conventional.
- Run the full quality gate locally (see Quality Gates).
- Open a PR against
mainwith a clear description of the change and why. - Keep the PR focused on a single concern; split large changes.
- Update the relevant docs when behavior changes:
AGENTS.md— single source of truth for project context & conventionsdocs/neeko-development-spec.md— full-stack architecture specdocs/ARCHITECTURE.md— architecture overview
- The project maintains bilingual docs (
README.md/README_CN.md,CONTEXT.md/CONTEXT_CN.md). When adding a doc, consider providing both language versions.
Releases are driven by pnpm release <version> (scripts/release.mjs), which:
- Bumps the version in
package.json,src-tauri/Cargo.toml, andsrc-tauri/tauri.conf.json. - Generates
CHANGELOG.mdviagit-cliff(config incliff.toml). - Commits
release: v<version>and tagsv<version>.
Pushing the tag triggers GitHub Actions to build Windows / macOS / Linux and publish a GitHub Release with installers. Only maintainers with push access run releases.
{ "id": "lint_fe", "title": "eslint + tsc", "argv": ["pnpm", "lint:fe"], "kind": "lint", // lint | test "stages": ["local", "commit", "ci"], "ci_job": "frontend-check", // required iff "ci" ∈ stages "platforms": ["linux", "macos", "windows"], "budget_ms": 180000, // over budget ⇒ exit 2 (gate unavailable) "scopes": ["src/*.ts", "src/**/*.tsx"], // expanded form — no `{a,b}` braces "fix_hint": "…" }