Raven AI-collaboration spec. Read this file before making any code change in this repo.
Scope: Codex / Claude Code / Claude API / any AI-assisted work. When a rule here conflicts with an ad-hoc instruction in conversation, this file wins — unless the user explicitly says "ignore rule X in AGENTS.md".
Hard constraints only (violations get reverted / rejected). Soft suggestions and style preferences belong in personal notes or conversation, not here. See the Maintenance note before adding sections — shorter is better.
| # | Section | Gist |
|---|---|---|
| 1 | Code comments | Don't comment unless necessary; comments in English |
| 2 | Branch naming | <type>/<snake_desc>; confirm base before cutting |
| 3 | Commits | Conventional Commits, all-English, Co-authored-by trailer |
| 4 | Dependencies | uv only — never pip / hand-edit lockfile |
| 5 | Tests | uv run pytest; strict file-naming |
| 6 | Domain terms | Consult CONTEXT-MAP.md before naming; use canonical terms |
| 7 | Repository assets | No report assets, web artifacts, or large files in PRs |
- Match the style of surrounding lines. If neighboring code has no comments, don't add one to your new line.
- Comment only when:
- the logic is non-obvious;
- there's a hidden constraint (e.g. call-order sensitivity, a caller must do X first);
- you need to explain why, not what (the name already says what).
- Don't add comments that:
- describe what the code does (
# Increment counternext tocounter += 1); - mark edits (
# ← new/# changed this line); - reference a PR / Issue / locally-visible-only doc path (
# Refs: ...— invisible to others); - describe transient task context (
# For the X bug— stale once the task is done).
- describe what the code does (
- Repo source comments must not be in another language — keep comment language consistent across the repo.
❌ Non-English review comment copied straight into source:
self.logger = logger.bind(channel=self.name) # ← new❌ Neighbors have no comments, yet the new line adds a meaningless one:
def __init__(self, config: Any, bus: MessageBus):
self.config = config
self.bus = bus
self._running = False
self.logger = logger.bind(channel=self.name) # ← drop this comment✅ Clean, no comment, consistent:
def __init__(self, config: Any, bus: MessageBus):
self.config = config
self.bus = bus
self._running = False
self.logger = logger.bind(channel=self.name)✅ Rare case that genuinely needs a why, in English:
# Bind channel name into logger context so every log entry auto-tags channel.
self.logger = logger.bind(channel=self.name)<type>/<short-desc>
| type | Use |
|---|---|
feat |
New feature |
fix |
Bug fix |
refactor |
Refactor (not a feature, not a bug fix) |
perf |
Performance |
chore |
Misc (deps bump, doc structure, etc.) |
docs |
Docs only |
test |
Tests only |
short-desc: snake_case, English, 3–5 words describing the change.
| ✅ Good | ❌ Bad |
|---|---|
feat/whatsapp_lid_mapping |
feat/优化 |
fix/cron_dst_transition |
bugfix |
refactor/cli_cron_sentinel |
huangjie-test |
chore/upgrade_uv |
tmp |
- Before cutting any branch (
fix/feat/refactor/ anything), ask the user which base to cut from — don't pick one silently. - If unspecified, default to
main(the integration branch). - Flow:
git fetch origin main, then cut from the latest tip. - Combined with the branch-first rule: confirm base + cut the branch, then start editing — never write on a working branch and carve the branch out afterwards.
<type>(<scope>): <subject>
<body — optional>
<footer — optional>
type — same set as §2.1, plus 3 commit-only types:
| type | Meaning |
|---|---|
feat |
New feature |
fix |
Bug fix |
docs |
Docs only |
refactor |
Refactor |
perf |
Performance |
test |
Tests only |
build |
Build system / external deps |
ci |
CI config |
chore |
Other misc |
revert |
Revert a prior commit |
scope — a top-level subpackage of raven/. See the Repo layout section of README.md for the canonical list. Spanning multiple scopes → omit the scope, or use (*).
subject — lowercase start; ≤ 72 chars; no trailing period; English.
footer (optional):
BREAKING CHANGE: <desc>— triggers a MAJOR bump once public;Closes #123— auto-closes the issue on merge.
No other languages anywhere in the message — not just the subject; body and footer too.
| Part | Rule |
|---|---|
| subject | English, lowercase start, ≤ 72 chars, no period |
| body | All English; when citing a non-English plan / discussion, translate it, don't paste |
| punctuation | ASCII-only — not just no full-width punctuation (:,,,。,「」,"" …) but also no em-dash —, curly quotes, or ellipsis … (all non-ASCII, all rejected by CI); no §-numbering, no non-English path names; the latin part of a §N.M anchor is fine |
| trailer | Co-authored-by: ... is ASCII by format |
Why: Conventional-Commits tooling (commitlint / semantic-release / changelog generators) parses ASCII grammar and mis-lints on non-English text; cross-language reviewers and a public commit history both need English.
Process:
- Before writing: translate the points in your head to English first — don't write a non-English body then translate (that leaves full-width residue).
- After writing: self-check with
git log -1; any non-English char → rewrite. - Already committed but violating: rewrite the message with
git rebase -ionly after explicit user authorization; don't rewrite history unprompted (see §3.4).
✅ Good:
feat(cli): rename cron show/remove to get/delete
fix(channels): default allow_from to ['*'] instead of deny-all
refactor(cli): replace --cron-expr with --cron and --every-seconds with --every
❌ Bad:
更新代码(non-English + no type/scope);update(no type/scope);feat: Cron 命令重命名为 get 和 delete.(uppercase + period + non-English + no scope).
✅ Required:
Co-authored-by: Claude (<model-id>) <noreply@anthropic.com>— when Claude helped write the code, append it at the end of the commit body (blank line above), or at the end of the PR description.<model-id>= the actual current-session model ID (e.g.claude-opus-4-8/claude-sonnet-4-6/claude-haiku-4-5), not a placeholder. The model version keeps per-model contribution distinguishable.- Format follows the aider convention; GitHub renders
Co-authored-byas a co-author on the commit / PR.
- Multiple co-authors → one per line, standard git trailer format (
Name <email>). - The repo squash-merges PRs (rebase only freshens the branch before push; the merge collapses the branch to one commit on
main). The squash commit's subject is the PR title and its body is the PR description (squash_merge_commit_message=PR_BODY) — individual commit bodies are dropped. GitHub still auto-collects each commit'sCo-authored-byinto the squash commit, so keep the trailer in your commit and putCloses #NNN+ reviewer context in the PR description (that is what lands onmain).
❌ Don't add:
Refs: ...pointing at locally-visible-only / git-ignored paths (invisible to others);🤖 Generated with Claude Codeand similar emoji banners —Co-authored-byalready conveys co-authorship (and is the structured, machine-readable attribution); a marketing badge adds no attribution value;- internal commit-hash references / temporary branch names — docs/PRs describe the present state only.
- Don't commit unprompted — only when the user explicitly says "commit" / "提交" / "save".
- "Commit per phase" written in a plan is not pre-authorization — a plan is a reference; committing still needs the user's word.
- After finishing a phase, report and stop; wait for acceptance + an explicit commit instruction.
- Don't
git commit --amenda prior commit (unless the user explicitly asks to amend). - If a pre-commit hook fails, create a new commit to fix it — don't amend.
Rule: before pushing a feature branch, base it on the latest main.
| Step | Command | Note |
|---|---|---|
| 1. Sync remote | git fetch origin <target> |
Doesn't touch the working tree |
| 2. Dry-run conflicts | git merge-tree --write-tree HEAD origin/<target> |
exit 0 = clean; non-zero prints conflicts |
| 3. Rebase (if remote ahead) | git rebase origin/<target> |
Re-applies your branch onto the target tip |
| 4. Re-run tests | uv run pytest <relevant tests> -x |
Confirm the rebase didn't break anything |
| 5. Push | git push -u origin <branch> (first) or git push --force-with-lease (after rebase) |
— |
Why: CI runs "your commits on top of the latest remote" (catches runtime conflicts before merge) and the PR diff stays clean. Rebase here only freshens the branch base before push; the merge itself is a squash, which collapses the branch to a single commit on main.
Force-push boundary:
- ✅
--force-with-lease(checks the remote wasn't changed by others) on your own feature branch after a rebase; - ❌
git push --force(blind, can clobber others' pushes); - ❌ never force-push to long-lived / protected branches (
main).
- Always
git fetchfirst to check ahead/behind; - if the remote target has commits not on your branch, rebase before pushing (the §3.5 flow);
- re-run tests after the rebase;
- don't push unprompted — like §3.4, only when the user says "push";
- use
--force-with-lease, never--force.
After pushing a new feature branch, proactively ask whether to open the PR with gh pr create — don't leave the user to do it in the web UI.
Title: same Conventional-Commits grammar as commits (<type>(<scope>): <subject>), subject reflecting the PR's overall goal, not any single commit. Title length may relax to ≤ 90 chars (the 72 limit is for git log --oneline wrapping; web-UI titles don't wrap) — but shorter is better.
Description must be all English (same as §3.1.1): no other languages / full-width punctuation / § numbering anywhere (subject + body + tables + checklist).
Description structure: use the repo PR template at .github/pull_request_template.md if present (gh pr create picks it up automatically); otherwise fill the structure below into --body by hand (all English):
## Change description
> Description here
## Type of change
- [ ] Bug fix
- [ ] New feature
- [ ] Document
- [ ] Others
## Related issues (if there is)
> Fix [#1]()
## Checklists
### Development
- [ ] Lint rules pass locally
- [ ] Application changes have been tested thoroughly
- [ ] Automated tests covering modified code pass
### Security
- [ ] Security impact of change has been considered
- [ ] Code follows security best practices and guidelines
### Code review
- [ ] Pull request has a descriptive title and context useful to a reviewer. Screenshots or screencasts are attached as necessaryFilling rules:
Change description— the PR's overall goal + key decisions (summarize the phase evolution for multi-commit PRs);Type of change— check what applies;- check only the boxes you actually satisfied — leave the rest blank and explain in the description; never blanket-check;
- anything the template doesn't cover but the reviewer needs (breaking change / cherry-pick option / mixed topics) → append to
Change description.
Trailer (with §3.3):
- squash-merge → GitHub auto-collects each commit's
Co-authored-byinto the squash commit, so keep the trailer in your commit and don't add it to the PR description (that duplicates it); Closes #NNNand reviewer context, by contrast, must live in the PR description — the squash commit body is taken from it (squash_merge_commit_message=PR_BODY), and individual commit bodies are dropped.
Description must NOT contain (same as §3.3):
🤖 Generated with [Claude Code](https://...)marketing banners;Refs: ...to ignored/local-only paths;- internal branch names / commit-hash references (no reviewer context).
Preview-verification (required):
- After drafting, grep for any non-ASCII char first (the CI lints the whole message as ASCII-only via
commit_lint._is_ascii, and the squash commit body is this PR description — so it must be ASCII too):grep -nP "[^\x00-\x7F]" /tmp/pr_description.md # must print nothing (0 matches). Catches em-dash/curly-quotes/ellipsis, # not just CJK + full-width — a CJK-only pattern gives a false pass.
- show the full text for preview;
- only after the user edits/confirms, run
gh pr create --title "..." --body "$(cat /tmp/pr_description.md)"; - report the PR URL.
Not allowed:
- pushing and walking away, leaving PR creation to the user;
- delivering a description without grepping for non-English residue.
| Action | Command |
|---|---|
| Add runtime dep | uv add <package> |
| Add dev dep | uv add --dev <package> |
| Remove dep | uv remove <package> |
| Sync env from lockfile | uv sync |
| Upgrade one package | uv lock --upgrade-package <package> |
| Upgrade all | uv lock --upgrade |
| Run a command in the project env | uv run <command> |
- ❌
pip install/pip uninstall; - ❌ hand-editing
[project.dependencies]/[project.optional-dependencies]/[dependency-groups]inpyproject.toml; - ❌ hand-editing
uv.lock; - ❌
pip freeze > requirements.txt; - ❌
python -m pip install ...to bypass uv.
If the user explicitly says "let me try pip" / "manually add this line to pyproject", follow the user. This rule constrains Claude's default behavior, not the user's direct instructions.
Under tests/test_*.py. CLI unit tests use one shape:
tests/test_cli_<module>_commands.py
- one file per module (aligns with
raven/cli/<module>_commands.py); - don't split by phase / feature / ticket (no
phase4/eve151suffixes); - aspect suffixes are allowed:
- testing a CLI private helper:
test_cli_<helper>.py(e.g.test_cli_helpers.py/test_cli_stacks.py); - cross-module behavior:
test_cli_<aspect>.py(e.g.test_cli_config_precedence.py/test_cli_smoke.py).
- testing a CLI private helper:
Under tests/integration/test_*.py. Run against real environments (real LLM / channel / fcntl / subprocess / VM, etc.).
Naming: test_<scope>_<kind>.py, where <kind> ∈:
| kind | Meaning |
|---|---|
e2e |
End-to-end happy path, single/multi module |
smoke |
Multi-module interplay, just "it runs" |
real_<resource> |
Hits a real resource (real_vm / real_llm / real_channel, …) |
<scope>must not carry a version / ticket number (nov002/eve151) — use a feature/scenario description.
| ❌ Wrong | ✅ Right | Reason |
|---|---|---|
test_cli_cron.py |
test_cli_cron_commands.py |
missing _commands suffix |
test_sentinel_cli.py |
test_cli_sentinel_commands.py |
order reversed |
test_cli_sentinel_phase4.py |
merge into test_cli_sentinel_commands.py |
no phase suffix |
tests/integration/test_v002_smoke.py |
test_<feature>_smoke.py |
no version number |
tests/integration/test_eve151_smoke.py |
test_<feature>_smoke.py |
no ticket number |
- when changing/adding a CLI command, update the matching
test_cli_<module>_commands.py— don't create a new file; - when you spot a legacy file violating §5.1 / §5.2, report it to the user first — don't rename it unprompted (renames touch git history and may collide with follow-up PRs);
- always run tests via
uv run pytest ..., never barepytest(per §4).
- Naming a domain concept tracked in
CONTEXT-MAP.md(the entry point — it routes toCONTEXT.mdfor Runtime terms andui-tui/CONTEXT.mdfor TUI)? Use the canonical term, not a synonym. - Coining a new domain term: define it in the matching
CONTEXT.mdin the same change, with a definition verifiable against the code (not guessed) — add an_Avoid_list only if a confusable synonym exists.
- Do not commit report assets or standalone web artifacts, regardless of size. This includes images, GIFs, SVGs, videos, audio files, PDFs, HTML files, web manifests, and WASM bundles.
- Store public-report assets outside git and link to them when needed.
- Do not add or modify files over 1 MiB unless the maintainer explicitly approves it before the commit.
- Run
make check-large-fileswhen touching docs, demos, reports, assets, or generated outputs; CI enforces the same rule on added and modified PR files.
This file holds hard constraints only (rules whose violation gets reverted / rejected). Soft suggestions, design preferences, and style leanings go in personal notes or conversation — not here.
Before adding a section, confirm with the user in conversation first — the shorter AGENTS.md stays, the more useful it is.