This document describes how agents (and humans) should work in this codebase. It reflects how the project actually operates today, not how earlier specs imagined it.
Different documents serve different purposes. Use the right one:
README.md— how to operate the daemon, install, and run tests. First thing to read.PRODUCT.md— what the product is, what it does differently, the strategic phase plan, open architecture questions.ROADMAP.md— the serialized, dependency-ordered work queue. The first unchecked item is the next thing to build. This is what an agent picks work from.BACKLOG.md— tactical bug list and small specs. The mini-spec for anyBACKLOG.md §-backed ROADMAP item lives here.prompts/agent-router.md— the RFC-2119 session contract: how the daemon-driven agent advances the queue one PR per session.docs/roadmap-from-kiro-specs.md— the method for serializing a Kiro spec into ROADMAP items.AGENTS.md(this doc) — how to do the work once you know what to do..kiro/specs/— Kiro specs. Most ofagent-router/is complete (historical); active specs likebrowser-test-harness/are referenced by ROADMAP items.- The code — the most authoritative source for "how is this actually done." When docs and code disagree, the code wins.
The default flow is ROADMAP-driven. ROADMAP.md is a serialized, dependency-ordered queue. Pick the first unchecked item (- [ ] Complete), implement exactly that one item, and open exactly one PR. Read everything on the item's Spec: line first: for a .kiro/specs/<feature>/ reference, read its requirements.md, design.md, and tasks.md; for a BACKLOG.md § P<n> reference, read that mini-spec. The full per-session contract — branch, test, PR, CI wait, finalize, merge — lives in prompts/agent-router.md and is summarized under "Session mechanics" below.
BACKLOG.md is the mini-spec source and the bug list. Most ROADMAP items that aren't spec-backed point at a BACKLOG.md mini-spec. When you need a brand-new item, write its mini-spec in BACKLOG.md first, then add the corresponding ROADMAP entry. To turn a Kiro spec into ROADMAP items, follow docs/roadmap-from-kiro-specs.md.
For genuinely tactical bugs (typos, prompt edits) outside the queue, no spec is needed — just fix it. Don't add features that aren't in ROADMAP.md, BACKLOG.md, or PRODUCT.md without recording them there first.
How a daemon-driven session advances the queue. This is the human-readable summary; prompts/agent-router.md is the authoritative RFC-2119 version.
- Branch model. Branch off the latest
developmentwith a short descriptive slug:git checkout -b agent/<slug>. PRs targetdevelopment, nevermain. Promotion ofdevelopment→mainis a human operator step. Never push directly tomainand never open a PR against it. - One item per session. Implement exactly one ROADMAP item and open exactly one PR. Never start a second item in the same session.
- Test before push.
npm run typecheckandnpm testmust pass locally before the first push. A behavioral change requires a Tier 2 test in addition to Tier 1. - Register the PR. Immediately after
gh pr create, call the agent-router MCPregister_prtool with the new PR number so CI events route back to this session. - No polling. After any push to a PR-bound branch, stop and wait. CI posts results back as a PR comment and a
check_runevent, which wake the session. Never loop ongh run watch/gh run listor sleep-and-recheck. React only to delivered events; fix-and-push on failure, proceed on green. - Finish before merge. Before merging, on the feature branch: tick the ROADMAP item to
- [x] Complete · PR: #<n>and move it to## Completed; tick the citedtasks.mdsub-tasks (spec-backed items); and land any docs the change justifies. Everything the merge should contain is on the branch before the merge — never a post-merge fixup. - Squash-merge.
gh pr merge <n> --squash --delete-branchintodevelopment. Then the session is complete.
These are settled patterns derived from the existing codebase. Follow them.
Dependency injection over imports for testability. Modules accept their dependencies (logger, database, clock, etc.) via constructor or function parameters. Don't reach out to module-level singletons in code that needs to be tested.
Pure functions are exported individually. Logic without side effects (parsers, validators, prompt composers) gets exported as standalone functions, not wrapped in classes, so they can be unit-tested directly.
Three error classes for three failure modes:
FatalError— daemon cannot continue, exitEventError— this event cannot be processed, skip it, daemon continuesWakeError— wake decision failed for this event, log and continue
Don't invent new error classes without a clear new failure mode.
Structured logging always. Use the Logger interface. console.log is prohibited outside the CLI client's pretty-printer.
Closed-union string fields. Status fields, termination reasons, trust tiers — these are closed unions in TypeScript. When adding a new value, update the union definition first; the compiler tells you everywhere it needs handling.
Atomic writes for state. meta.json writes go through temp-file-plus-rename. stream.log and prompts.log are append-only NDJSON. Never partial-write a state file.
Synchronous SQLite, synchronous file I/O for session files. better-sqlite3 is sync by design. Session file operations use fs sync methods with explicit fsync for durability. Async is for HTTP and ACP transport, not for state writes.
Three tiers:
- Tier 1 (
test/tier1/) — pure logic, unit tests, property tests. Fast. Property tests usefast-checkwith at least 100 iterations. - Tier 2 (
test/tier2/) — full daemon against fake backends. Uses the test harness intest/harness/(FakeGitHubBackend,FakeKiroBackend,TestDaemonImpl). Scenario files intest/scenarios/scriptFakeKiroBackendbehavior. - Tier 3 — real GitHub, real Kiro. Slow, network-dependent, consumes API quota. Run before shipping, not on every change.
Tests must not require network access, real API tokens, or real Kiro for Tier 1 or Tier 2. This is a hard rule.
Test files use .test.ts. Tier 1 tests can co-locate with source if they're testing pure logic in that module.
Tier 2 coverage is required for any meaningful behavioral change. A new wake policy rule, a new termination reason, a new MCP tool — all need Tier 2 coverage in addition to Tier 1.
Follow Conventional Commits 1.0.0.
Format: <type>[optional scope]: <description>
Types:
feat— new feature or capabilityfix— bug fixrefactor— code change that neither fixes a bug nor adds a featuretest— adding or updating tests onlydocs— documentation onlychore— maintenance (deps, scripts, CI config)
Scope is optional but encouraged. Use the module name or backlog ID when it fits: feat(session-mgr): ..., fix(P2.4): ....
Body is optional. Use it for context that doesn't fit in the subject line. Separate from subject with a blank line.
Footer BREAKING CHANGE: <description> if the commit introduces a breaking change.
Examples:
feat(cli): add complete-session subcommand
fix(config): template literal bug in sessionTimeout error
refactor(router): extract self-wake check into pure function
test(hook-parser): add property tests for merge detection
docs: update AGENTS.md with conventional commits guide
chore: bump vitest to 3.2.4
Keep the subject line under 72 characters. Use imperative mood ("add", not "added" or "adds").
- ESM imports with
.jsextensions in import paths (TypeScript with ESM resolution). - Strict TypeScript — no
any, noascasts unless absolutely necessary. - No build step.
tsxhandles execution;tsc --noEmithandles type checking.
Session state lives in two places. In-memory in the session manager (active sessions), and on disk under ~/.agent-router/sessions/<id>/. The on-disk state survives daemon restarts; the in-memory state does not (yet — see backlog P2.1).
Per-session files:
meta.json— session metadata. Atomic-written. Source of truth for status, termination reason, registered PRs.stream.log— append-only NDJSON of every ACP event. Tailable from outside.prompts.log— append-only NDJSON of every prompt injected into the session.
Don't bypass the session manager when manipulating session state. All writes go through session-mgr.ts and session-files.ts for atomicity guarantees.
The wake policy lives in src/router.ts. It's a pipeline:
- Filter by event type
- For comment events, compute trust tier from
comment.author_association,comment.user.login,comment.user.type,repository.owner.login - Resolve the PR from the event payload
- Look up an active session bound to that
(repo, pr) - Apply rate limiting
Each step has a pure function exported for direct testing. When changing the wake policy, update the pure function, add Tier 1 tests for the function, and add a Tier 2 test that exercises the full pipeline through the daemon.
- Don't add dependencies without a clear need. This codebase deliberately keeps the dependency surface small. Justify any new import.
- Don't introduce a build step.
tsxfor execution,tsc --noEmitfor type-checking. No bundlers, no transpilers, no compile-and-run flow. - Don't write to
console.login production code. Use the structured logger. - Don't bypass atomic-write patterns for state files. If you find yourself writing JSON without rename-after-write, you're doing it wrong.
- Don't run Tier 3 tests on every change. They're slow and consume real quota.
- Don't add features that aren't in
BACKLOG.mdorROADMAP.mdwithout first adding them there. Keeps the project's scope clear and prevents drive-by feature creep. - Don't merge a PR unless tests are green. Including new tests that exercise the new behavior. Including Tier 2 if the change is behavioral.
- Don't update documentation as a separate PR from the code change. Docs land alongside the code that justifies them.
- Don't merge before documentation is on the feature branch. For dpdk prompts and any feature workflow: perf-log entries, README roadmap updates, and other doc changes must be committed and pushed to the feature branch before the squash-merge. The merge commit must include everything. Post-merge doc fixups go directly to
development, bypassing PR review — that's the bug this rule prevents.