Agent Rules
Additional rules for agents working on the package — how work moves through branches, how designs reach approval, and the bar every change clears before it lands.
Branches are named <type>/<description>
process
Every branch is <type>/<description>. The type is the change kind — feat, fix, docs, refactor, chore, integration — and the description is a short kebab-case summary of the work. Nothing else: no bare names, no personal prefixes.
✅ docs/standards-corpus feat/queue-durability fix/soft-break-spacing
❌ standards my-branch wipCommit and push continuously
process
Commit at every sensible checkpoint and push in the same breath — a local commit is not done until it is on the remote. Small, frequent, always-pushed commits; never let unpushed work pile up, and never wait to be asked. Each finished unit (a chapter, a fix, a passing step) is a commit, pushed.
Work on a working branch; integration advances by merge
process
Cut a <type>/<description> working branch from your integration branch, do your regular commits there, and merge into the integration branch at checkpoints. An integration branch is advanced by merges, never by direct commits — committing straight onto it skips the working-branch step and the review it affords.
Never force-push a shared branch
process
push --force (and --force-with-lease) is forbidden on integration, main, and any branch another agent may have based work on — a stale force-push silently deletes other agents landed work. A rejected push means your local view is stale: fetch, merge, and push normally — the rejection is the system working, not an obstacle to override. Force-push is permissible only on a private working branch that is yours alone, and never after sharing it.
# ❌ bad — the rejection was telling you integration moved
git push --force origin HEAD:integration
# ✅ good — integrate what landed, then push
git fetch origin && git merge origin/integration && git push origin HEAD:integrationChangesets and releases are deliberate
process
A change to public API, behaviour, or package metadata gets one coherent changeset, drafted proactively whenever the change warrants it — dont wait to be asked to write it. But commit the changeset only after confirmation: the file is prepared eagerly, committed deliberately. Beta releases are manual: never run changeset version; the release is assembled by hand (changeset entry, prerelease bump, changelog). Pushing is not publishing.
No code until an explicit go
process
While a design is being discussed, discuss it — do not write the implementation. Only a clear, direct go from the owner on this change is approval. A sounds good, someone elses opinion, or your own certainty is not. Lets discuss first means stop. And never build a design that hasnt been approved — or one that was already rejected — hoping it lands; it only earns a revert.
The decisions doc is the source of truth
process
As each decision is made during a design discussion, record it in a decisions doc right away — dont wait to be asked. From then on, build from the doc, not from memory or chat: never re-derive a decision thats already written, and never re-propose a shape the doc lists as rejected. A do not resurrect section keeps dead ideas dead.
## Decisions
- Tag carries `payload`/`success`/`error`; layer config never overrides them. (locked 07-03)
## Do not resurrect
- Positional-only lane config on CQR — rejected 07-04, breaks named lanes.Approve each item before its locked
process
In an API walk, present one item, wait for the go, then mark it locked — item by item. Dont batch-lock a list, and dont read silence as approval.
Ask the sharp questions, not a barrage
process
Open with the few highest-uncertainty questions, not a questionnaire. When the owner asks for ideas or says just tell me what youd do, answer directly — proposing more questions back is the wrong move.
❌ a wall of 12 questions covering every field
✅ "Two things decide the rest: is durability opt-in or always-on, and does
readiness fan out across nodes? Once those are set I can propose the rest."Stop when a constraint forces a compromise
process
If a limitation is pushing you toward a workaround, a half-ship, or a taxonomy that was already rejected — stop and raise it. Dont silently ship the fallback. Document the blocker with concrete file:line pointers and let the owner make the call.
Green before every commit
test
Four checks pass before anything is committed or released — no exceptions, including docs-adjacent code changes:
pnpm typecheck # tsgo on BOTH projects; patched with the Effect language-service,
# so it enforces the Effect rules plain tsc/tsgo would miss
pnpm lint # eslint
pnpm test # vitest run — the full suite
pnpm build # tsupRed on any of them means it isnt done. Never commit on a broken check to fix later.
Effect programs are tested with @effect/vitest
test
An Effect is tested with it.effect / it.live from @effect/vitest, which run the effect for you. Import expect from plain vitest.
import { it } from "@effect/vitest"
import { expect } from "vitest"
it.effect("dedup rejects a repeat key", () =>
Effect.gen(function* () {
const q = yield* Mail
const first = yield* q.add(job)
const second = yield* q.add(job)
expect(second).toEqual(first)
}),
)Timing and polling tests use it.live
test
it.effect runs on the TestClock, which stalls real sleep, delay, and interval polling — the effect would hang. Anything that advances in real time (a queue that polls, a scheduled process) uses it.live.
// ✅ real interval → live clock
it.live("queue drains on its poll interval", () =>
Effect.gen(function* () {
yield* Mail.pipe(Effect.flatMap((q) => q.add(job)))
yield* Effect.sleep(Duration.millis(50)) // real time passes
expect(yield* size(Mail)).toBe(0)
}),
)Pin public types with *.test-d.ts
test
A public type is a contract; assert it at the type level in a *.test-d.ts file — and with no casts, or the test proves nothing.
// queue-tag.test-d.ts
import { expectTypeOf } from "vitest"
expectTypeOf(Mail.add).returns.toEqualTypeOf<Effect.Effect<string, QueueFull>>()Testing never needs approval
test
Write thorough tests, always — covering the no-op-vs-persist paths, each projection, and the type surface. Tests are exempt from the no code without a go gate: you never wait for permission to add them.