Hyperlinkv0.8.0-beta.28

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                  wip

Commit 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 touch a shared branch without a per-action go

process

Committing, pushing, merging, or opening a PR against main, a release branch, or any shared or user-owned branch requires explicit, per-action approval. Prepare to merge is not merge; approval for one action does not carry to the next. Your own working branch is yours to push freely — the gate is only on shared history.

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:integration

Changesets 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       # tsup

Red 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.

Edit this page on GitHub