builder-syndicate
AGENTS.md6 starsChanged 8 months agoApache-2.0View on GitHub
OSS ships DefaultBuilderSyndicateModule bundling defaults. Enterprises swap modules: - AuthModule — SSO integration - DatabaseModule — connection config - StorageModule — S3/CDN This repo uses Graphite for stacked PR workflows: For local development, the OSS version ships UnsafeDevLoginAction:
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,542 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository3 other files it gives its agents.
- prs/AGENTS.mdAGENTS.md
- specs/AGENTS.mdAGENTS.md
- specs/alpha-claude/claude.mdCLAUDE.md
The file
We split this AGENTS.md into the rules it gives an agent
12 rules. About 0 are common, 3 are distinctive, and 9 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
9
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 76
No polymorphic FKs — application-level integrity
- Very specific: line 111
Always use
gt stack restack: after a parent branch merges — don't manually rebase - Very specific: line 112
Use
gt create: instead of git checkout -b to keep branches tracked - Distinctive: line 130
Use standard React patterns (hooks, functional components)
- Distinctive: line 135
When creating or working tickets:
- Very specific: line 170
Check for port conflicts before Docker: Run lsof -i :3306 — local MySQL will shadow Docker container
- Very specific: line 179
Flyway on Gradle 9+: Use net.ltgt.flyway plugin (not org.flywaydb.flyway which uses deprecated APIs)
- Very specific: line 238
Use linguist-generated=true to auto-collapse noisy files in PRs.
- Very specific: line 239
Use -diff only when line-by-line diffs are not useful (e.g., binary blobs).
- Very specific: line 258
If a change introduces new generated/noisy files, update .gitattributes in the SAME PR.
- Very specific: line 259
Prefer .gitignore for ephemeral tooling outputs so they never enter the repo.
- Very specific: line 261
Do not refer to internal branch acronyms in PR descriptions when the work maps to a ticket.
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Builder Syndicate - Agent Guide
Project Goals
- OSS-first enterprise link aggregator (Reddit/HN/Lobsters style)
- Exemplary Misk reference implementation — showcase idiomatic patterns, no escape hatches
- LLM-built — multiple AI builders (Claude, Amp, ChatGPT, Gemini)
Architecture
Hexagonal-lite Package Structure
src/main/kotlin/xyz/block/buildersyndicate/
├── core/ # Pure Kotlin — NO framework imports
│ ├── users/ # User entity, UserRepository interface
│ ├── posts/ # Post entity, PostRepository, PostService, MarkdownRenderer
│ ├── comments/ # (future)
│ └── ...
├── adapters/ # Framework integrations
│ ├── db/ # jOOQ repository implementations, DatabaseModule
│ ├── misk/ # WebActions, gRPC services, AuthModule
│ └── ...
└── app/ # Wiring only
├── BuilderSyndicateService.kt # Thin main()
└── BuilderSyndicateModule.kt # Root Guice moduleCore Rules (Invariants)
core/has NO framework imports — only stdlib +java.timemain()is thin — only instantiate modules, callMiskApplication.start()- All Guice modules, services, actions, repositories are
public(notinternal) - Interfaces in
core/, implementations inadapters/
Enterprise Composition
OSS ships DefaultBuilderSyndicateModule bundling defaults. Enterprises swap modules:
AuthModule— SSO integrationDatabaseModule— connection configStorageModule— S3/CDN
Tech Stack
| Layer | Technology |
| Language | Kotlin, Java 21 |
| Framework | Misk |
| Build | Gradle Kotlin DSL |
| API | gRPC (Wire) + WebActions (JSON) |
| Database | MySQL 8.0 (Aurora in prod) |
| Migrations | Flyway |
| Query | jOOQ (generated Kotlin) |
| Frontend | React + TypeScript |
Commands
just dev # Start MySQL via docker-compose
just down # Stop containers
just db-migrate # Run Flyway migrations
just codegen # Generate jOOQ + proto code
just build # Build project
just test # Run tests
just run # Start the appConventions
Database
- Migrations:
V001__description.sql,V002__description.sql - Primary keys:
id BIGINT AUTO_INCREMENT - Timestamps:
created_at DATETIME,updated_at DATETIME - Charset:
utf8mb4_unicode_ci - No polymorphic FKs — application-level integrity
gRPC / WebActions
- One gRPC service per domain:
PostsService,UsersService - Proto3 syntax, request/response naming:
CreatePostRequest,CreatePostResponse - WebActions path:
/api/v1/{resource}/{action} - JSON ↔ protobuf mapping handled server-side
Testing
- Unit tests: Core logic (pure Kotlin, no DB)
- Integration tests: Repositories against docker MySQL
- Service tests: gRPC services with test fixtures
- Test fakes mirror mainline paths —
core/users/FakeUserRepository.ktnottesting/FakeUserRepository.kt - No junk drawer packages — avoid generic
testing/orutil/packages
Commits
- Conventional commits:
feat(scope):,fix(scope):,chore: - Scopes:
core,db,adapters,misk,ui - One logical change per commit
Stacked PRs (Graphite)
This repo uses Graphite for stacked PR workflows:
gt log short # View current stack
gt create -m "feat: add feature" # Create branch + commit in stack
gt stack submit # Push all branches, create/update stacked PRs
gt stack restack # Rebase stack after parent merges
gt track --parent <branch> <branch> # Adopt existing branch into stack- Always use
gt stack restackafter a parent branch merges — don't manually rebase - Use
gt createinstead ofgit checkout -bto keep branches tracked - PRs are automatically linked as a stack on GitHub
Authentication (Dev Mode)
For local development, the OSS version ships UnsafeDevLoginAction:
GET /api/v1/auth/dev-userslists available dev usersPOST /api/v1/auth/loginwith{"username":"dev-alice"}creates session- User is created in DB on first login if not exists
AuthModule is swappable — enterprises replace with real SSO.
Frontend (Misk UI)
- React + TypeScript in
web/directory - Built assets served by Misk at
/ - Build integration:
./gradlew buildWeb - Use standard React patterns (hooks, functional components)
- API calls via generated proto clients or fetch to WebActions
Ticket Guidance
When creating or working tickets:
- Behavioral AC only — "user can X", not "file exists"
- Human-comprehensible size — a reviewer should understand the full changeset without scrolling endlessly
- No standalone scaffolding — fold entity+repo+migration+impl into one vertical slice
- Split by capability — read vs write, not by code layer
- Facets are vertical slices — tickets are labeled by facet (e.g.,
auth,posts,comments) not by code layer
Code Style
- OOP naming: object→noun, method→verb — classes are true nouns (
PostResponse,PostService), methods are verbs (from(),create()). Avoid-ernouns that are just objectified verbs (Mapper,Builder,Handler) — if a class is named for what it does rather than what it is, rethink the design. Prefer companion factories (PostResponse.from(post, author)) over single-purpose-erclasses. - No sloppy overcommenting — code should be self-explanatory; comments are for "why", not "what"
- Don't narrate the obvious —
// Create a new userabovecreateUser()is noise - Match existing patterns — look at neighboring code before inventing new conventions
- Don't refactor unrelated code — only modify files/patterns directly required by the task; "improving" existing patterns creates review noise and merge conflicts
File Paths
| Path | Purpose |
src/main/resources/db/migration/ | Flyway SQL migrations |
src/main/proto/ | Proto definitions |
web/src/ | React frontend |
Common Pitfalls
- Don't import Misk in
core/— keep domain pure - Don't put logic in
main()— use Guice modules - Don't use
internal— modules must be composable - Repository methods return
nullfor not-found — not exceptions
Agent Execution Tips
Environment Pre-flight (CRITICAL)
- Check for port conflicts before Docker: Run
lsof -i :3306— local MySQL will shadow Docker container - If port 3306 is in use: Change docker-compose to use port 3307 and update all JDBC URLs
- Diagnose connection issues at the source — auth errors often mean you're hitting the wrong MySQL instance
Dependency Strategy
- Always use latest stable versions — web search Maven Central / Gradle Plugin Portal before adding any dependency
- Never trust pinned versions — versions in tips files, old branches, or existing code are snapshots, not guidance
- Misk BOM is source of truth for Misk-related dependency versions — don't pin Misk modules separately
- Flyway on Gradle 9+: Use
net.ltgt.flywayplugin (notorg.flywaydb.flywaywhich uses deprecated APIs) - Solve compatibility at root cause — don't downgrade to work around issues, fix forward
Problem-Solving Approach
- Don't cargo-cult from old branches — understand WHY a config exists before copying it
- Separate problems — port conflicts, auth plugins, and plugin compatibility are distinct issues
- Fail fast on environment issues — check ports/connectivity before debugging code
Context Management
- Use
Tasksubagents for multi-file work — creating 5+ files or modifying across layers should be one Task call - Delegate research upfront — use
librarianororaclefor framework questions before writing code - Batch verification — write a verify script, don't run 5 separate curl commands
- Read tickets before implementing — they're in
tasks/, drift is expensive
Misk-Specific Gotchas
- YAML config files cannot be empty — use
{}as minimum content for environment overrides - Prometheus metrics on separate port — default 9102, not
/_admin/metricson main port - Add logback.xml early — Jetty DEBUG logs consume huge token counts; set level to WARN before testing
- Use
jakarta.injectnotjavax.injectfor annotations - WebAction methods can't take HttpCall as parameter — inject
ActionScoped<HttpCall>via constructor instead - Per-request scoped providers — extend
ActionScopedProviderModule, usebindProvider(T::class, TProvider::class); never use@Singleton @ProvideswithActionScopeddependencies - FakeHttpCall for tests — use
misk.web.FakeHttpCall(in misk-testing); it's inmisk.web, notmisk.testing
Build & Test
- DB tasks need
-PwithDbflag — Flyway/jOOQ tasks are skipped during normal build to allow cold builds - Prefer
gradle compileKotlinovergradle run— verify compilation without port conflicts - Hermit manages tooling — use
gradlenot./gradlew, activate withsource bin/activate-hermit - jOOQ generated code is committed — lives in
src/generated/jooq/, enables DB-free builds; regenerate withjust codegenafter migrations - Kill background processes — if you spawn
gradle runfor testing, kill it when done; checklsof -i :8080or:9102
jOOQ & Database Wiring
- DatabaseModule must provide DSLContext — jOOQ repos inject
DSLContext; the module needs a@Providesmethod - BuilderSyndicateModule must install DatabaseModule — auth actions depend on UserRepository
- Test FK constraints — when cleaning test data, delete child tables before parent (e.g., posts before users)
Prior Work
- Check git log first —
git log --oneline -10shows what's already done - Learnings in
tips/— files document non-obvious discoveries (environment-specific, not version pins) - Specs in
specs/specs/— PRD, engineering spec, implementation plan are authoritative - Old branches are not reference implementations — they may contain workarounds, not solutions
Reference Docs
PR Diff Hygiene (GitHub folding / noise files)
To keep PRs reviewer-friendly, configure GitHub’s default diff folding via a root-level .gitattributes and follow these rules:
- Root
.gitattributesis authoritative for GitHub Linguist.- Use
linguist-generated=trueto auto-collapse noisy files in PRs. - Use
-diffonly when line-by-line diffs are not useful (e.g., binary blobs). Prefer folding over suppressing diffs.
- Use
- Required patterns (kept up to date):
# Hermit and helper scripts: collapse in PRs as generated bin/hermit linguist-generated=true bin/activate-hermit linguist-generated=true bin/**/hermit* linguist-generated=true # Gradle wrapper and related binaries gradle/wrapper/** linguist-generated=true # Common generated/minified assets (UI) *.min.js linguist-generated=true *.map linguist-generated=true # Optional (use sparingly): completely suppress diffs # bin/hermit -diff # bin/activate-hermit -diff - If a change introduces new generated/noisy files, update
.gitattributesin the SAME PR. - Prefer
.gitignorefor ephemeral tooling outputs so they never enter the repo. - For already-open PRs, push a new commit after editing
.gitattributesso GitHub reclassifies and folds diffs. - Do not refer to internal branch acronyms in PR descriptions when the work maps to a ticket; link the issue instead (e.g.,
ref: #123).
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

