Imported from zgeoff/tools (
AGENTS.md). Install upstream withnpx skills add zgeoff/tools. Copyright stays with the author.
Agent Guidelines
Operations
- AGENTS.md is generated from
agents/shared.mdandagents/project.md— edit the partials, never AGENTS.md itself. The shared partial is synced from zgeoff/tools; cross-project rule changes belong there. - Perform all work on a branch in a git worktree under
.worktrees/(e.g.git worktree add .worktrees/<branch> -b <branch>) — never commit directly onmain. - Use Conventional Commits for all commit messages.
- A squash merge makes the PR title the commit subject, so a PR title is a Conventional Commit too.
feat(#412): add the retry budgetis a title;add the retry budgetis not. - Commit subjects and PR titles use the imperative mood ("add X", never "added X" or a bare noun phrase).
- Open PRs against
mainusing the PR template (.github/PULL_REQUEST_TEMPLATE.md). Descriptions are condensed: lead paragraph ≤2 sentences, one-line bullets, ≤150 words — write the short version first, don't draft long and trim. - After pushing, link the PR URL in your response.
- A PR is ready only when its checks are green: watch CI (
gh pr checks <n> --watch) after opening or updating, and report a failure with what you're doing about it.
Code style
Mechanically enforced rules (oxfmt, oxlint, format-codemod) aren't repeated here — this file covers what tooling can't check.
- One primary export per file, and the file name kebab-cases that export (
with-jest-context.tsexportswithJestContext). Exceptions:index.tsentrypoints,types.tsfor a package's shared types, and side-effect-only modules, which are named for what they do (augment-bun-test.ts). - Module order: imports, the primary export, then private helpers in composition order (depth-first) — never helpers first. Supporting declarations (consts, interfaces, type aliases) sit directly above their first use, never below it and never leading the file; types for the primary export's signature may sit just above it.
- Acronyms stay uppercase in identifiers (
runCLI,parseCLIArgs,ASTNode,pkgURL,isPackageJSON) — except when one starts a camelCase name, where it lowercases whole (cliPath,astNode). ID counts as an acronym:userID,sessionID— neveruserId— andidTokenwhen it starts a name. File names are unaffected: kebab-case lowercases everything (parse-cli-args.tsexportsparseCLIArgs).
Function naming
Every function name starts with a prefix from the closed list below: pick from it, or extend this file in the same PR that introduces the new verb. The prefix is a contract — a reader should know the function's shape without opening it.
Predicates — return boolean, no side effects:
| Prefix | Contract | Example |
|---|---|---|
is |
type or state test | isVarDecl |
has |
containment, possession | hasBlankLine |
can |
capability | canResize |
should |
policy decision | shouldSkipFile |
needs |
requirement | needsBlankLine |
Pure producers — result comes from arguments alone, no side effects:
| Prefix | Contract | Example |
|---|---|---|
build<Result>[From<Source>] |
default constructor for values; drop From<Source> when no single source |
buildEditsFromAST |
define<X> |
identity; its only job is compile-time constraint of its literal argument | defineErrors |
parse |
unstructured input → structure, invalid input reported | parseSource |
encode |
structure → its defined compact or wire form, reversed by decode |
encodeState |
decode |
encode's output → the original structure, malformed input reported |
decodeState |
derive |
one-way cryptographic derivation from secret material | deriveAvatarKey |
plan |
compute an action without performing it | planGapEdit |
pick |
select among known alternatives | pickMode |
find |
search that can miss — null/undefined on miss | findPrevious |
get |
cheap access that cannot miss (throwing on a broken invariant is fine) | getNodeEnd |
collect |
gather from a traversal or scan | collectChildNodes |
count |
how many | countNewlines |
split |
one value → parts | splitLines |
merge |
parts → one value | mergeWindows |
sort |
reorder | sortEdits |
format |
value → human-readable string | formatRange |
render |
structure → output text or markup | renderHunk |
normalize |
variant forms → the canonical form | normalizePath |
resolve |
follow indirection to a concrete value | resolveBinPath |
expand |
compact form → full form | expandInputs |
compress |
value → its reversible compact encoding | compressGraph |
decompress |
reverse a compress encoding (non-encoded shorthand is expand) |
decompressGraph |
to<Result> |
cheap representation change | toPosixPath |
transform |
a package's own source→source operation | transform |
Effectful — touches the world (filesystem, streams, processes, registries):
| Prefix | Contract | Example |
|---|---|---|
apply |
perform previously planned changes | applyEdits |
create |
bring a resource into existence (file, directory, process) | createWorkDir |
claim |
atomically take exclusive ownership of a work item or resource; ownership ends at commit or an explicit release | claimNextChain |
read |
pull raw content from filesystem or network into memory | readSource |
load |
read and parse into a ready structure | loadConfig |
write |
persist to the filesystem | writeOutput |
remove |
delete a resource | removeStaleDist |
update |
mutate existing state or resource in place | updateIndex |
upsert |
single-statement insert-or-update keyed by a natural or composite key, refreshing the conflicting row's columns in place | upsertUser |
set |
assign a store's named state slice wholesale — the store-setter idiom; partial mutation is update |
setSelectedNode |
toggle<Flag> |
invert a boolean state slice | toggleDevCamera |
reset |
return state to its initial value | resetCombatState |
print |
write to stdout/stderr | printHelp |
run |
execute a subprocess, task, or whole pipeline | runCLI |
check |
evaluate and report findings; effects allowed per mode | checkFile |
try<X> |
X with failures captured as a value instead of a throw | tryCheckFile |
register |
add to a registry the caller doesn't own | registerMatcher |
subscribe |
attach a listener to an event source, returning or enabling detachment | subscribeToTicks |
unsubscribe |
detach what subscribe attached |
unsubscribe |
assert |
throw when an invariant doesn't hold | assertSpan |
require |
throw unless a runtime condition holds — a guard real input can trip (assert covers invariants) |
requireAuth |
verify |
test a claim or credential against evidence, rejecting on mismatch | verifySession |
emit |
dispatch an event or notification | emitProgress |
send |
transmit a payload to a remote receiver (fire-and-forget or RPC — no resource semantics; REST mutations are create/update/remove) |
sendWebhook |
wait |
block until an event or condition resolves; may return the awaited value | waitForMessage |
setup |
prepare the environment or fixture the following code assumes; teardown reverses it |
setupTest |
teardown |
release what setup prepared |
teardownTest |
start |
put a long-running resource into service (server, worker, poll loop); stop reverses it |
startQueues |
stop |
take a long-running resource out of service, releasing what start acquired |
stopWorker |
drain |
consume a pending backlog until empty | drainJobs |
Wrappers and factories — the result is behaviour, not data:
| Prefix | Contract | Example |
|---|---|---|
with<X> |
HOF that runs a callback inside a context | withJestContext |
make<X> |
factory whose result is itself a function | makeExcluder |
Framework conventions — where the ecosystem's prefix is load-bearing, it wins:
| Prefix | Contract | Example |
|---|---|---|
use<X> |
React hook — the prefix drives rules-of-hooks linting; helpers inside a hook follow the normal taxonomy | useDebounce |
on<Event> |
event-callback prop or parameter | onRowClick |
handle<Event> |
local implementation passed to an on<Event> prop — the idiomatic React pair; the handle ban applies everywhere else |
handleRowClick |
handle<LifecycleEvent> |
implementation of an engine lifecycle callback, keyed by the engine's lifecycle-event enum | handleTick |
Banned — each is a vaguer or synonymous form of a listed verb; use that one instead: handle
(except the handle<Event> framework conventions), process, manage, do, perform (say what
it does), execute (→ run), compute (→ build), fetch (→ read), save/store (→
write), delete (→ remove), search/lookup (→ find/get).
Algorithm-native vocabulary (walk, backtrack, slideDiagonal) is allowed inside the module
implementing that algorithm — forcing list verbs onto textbook terms hides the algorithm.
Dependencies
- Pin exact versions — no
^/~ranges. (bun addsaves exact automatically viaexact = truein bunfig.toml — the rule applies to hand-written edits.)
Review bots
CodeRabbit reviews every PR. Its shared config lives in the zgeoff/coderabbit repo, and a repo-root
.coderabbit.yaml with inheritance: true layers repo-specific settings on top. CodeRabbit reads
this file as its guidelines. A repo that runs another review bot names it and its config in
agents/project.md, and these rules cover that bot too.
- A PR is ready only after every bot review is read and every finding is answered: a fixed finding's
reply cites the commit that fixed it; a declined finding's reply states the reason — when a
finding contradicts this file, this file wins and the reply names the rule. Reviews land within a
few minutes of opening; read them with
gh pr view <n> --commentsandgh api repos/<owner>/<repo>/pulls/<n>/comments. A finding outside the diff arrives in the review body, not as a thread, so its answer is a PR comment. - Resolve a thread once its reply is posted, fixed and declined alike (GraphQL
resolveReviewThread). A finding the agent cannot confidently judge is escalation, not disposition: reply saying so and leave the thread open for a human. - Never teach a bot through chat (
@coderabbitailearnings and the like) — a correction to bot behaviour is an edit to its config, reviewed in a PR. - Bots review a PR once, at open; an agent invokes a re-review only when asked. The exception is a
PR that got no review at all, such as one opened before the bot was installed: request it once
with that bot's documented trigger, such as
@coderabbitai reviewfor CodeRabbit.
Type checking
Type errors are checked by two engines on purpose: lint:type-aware includes tsgolint's
experimental --type-check (fast, run it locally), while typecheck runs real tsc per package
(ground truth). CI runs both until tsgolint earns trust; if they ever disagree, believe tsc.
Workspace dependencies
- Shared versions live in the root catalog; workspace packages reference them via
catalog:.
Testing
- Never use
describe— write flattest(…)blocks with behavioural titles that start with "it" (test('it pads before a return statement', …)). A file that tests several units prefixes each title with#<unit>in place of "it" (test('#parseSource rejects an empty file', …)). - Test files are co-located with the module they test (
parse-source.tsbesideparse-source.test.ts) — notest/,tests/or__tests__directories, except a roote2e/for the end-to-end suites that run the whole program. Declaration emit excludes*.test.ts, so they never ship. - Run
bun testfrom the repo root: the jest-extended preload lives in the rootbunfig.toml, so package-cwd runs are missing the extra matchers. - Tests declare their own data inline — no fixtures shared between tests, even if that means duplication.
- Tests are mock-free: pure modules assert on return values, file-touching ones use
mkdtemptrees, and CLI behaviour is asserted end-to-end by spawning the real binary. If a module is hard to test without mocking, move its I/O to the caller. - Reach for jest-extended matchers instead of hand-rolling assertions. Frequently useful:
- arrays
toIncludeAllMemberstoIncludeSameMemberstoPartiallyContaintoIncludeAllPartialMemberstoSatisfyAll
- objects
toContainEntrytoContainEntriestoContainAllKeystoBeFrozen
- strings
toStartWithtoEndWithtoIncludetoEqualCaseInsensitivetoEqualIgnoringWhitespace
- values
toBeNiltoBeOneOftoSatisfytoBeWithintoBeEmpty
- dates
toBeAftertoBeBeforetoBeBetweentoBeValidDate
- mocks
toHaveBeenCalledOncetoHaveBeenCalledExactlyOnceWithtoHaveBeenCalledBeforetoHaveBeenCalledAfter
- errors/async
toThrowWithMessagetoResolve(returns a promise — alwaysawait)toReject(returns a promise — alwaysawait)
- arrays
- Matchers also work asymmetrically inside
toStrictEqual/toMatchObject(status: expect.toBeOneOf([…])). - Known gaps:
expect.pass/expect.failare unimplemented upstream and excluded from our types. It'stoEqualCaseInsensitive— not…Insensitivelyas some docs claim; unknown matcher names fail typecheck here (upstream's own types are looser and would let typos through).
