File constraint: This file must stay under 200 lines (~150 target). Keep rules explicit and concise.
Sync rule: CLAUDE.md and AGENTS.md must always be identical:
cp CLAUDE.md AGENTS.md. Skills are shared:rsync -a --delete .claude/skills/ .agents/skills/. Agents are NOT shared — Claude uses.claude/agents/*.md(markdown), Codex uses.codex/agents/*.toml(TOML). When adding/modifying an agent, translate to the other format manually. Use theagent-syncskill for format reference, or invoke theagent-syncagent to auto-detect and fix drift. Pre-commit hooks verify guidance and skill sync.
gitlab-agent-webhook — GitLab webhook service that spawns AI coding agents (Claude, Codex, Gemini) for automated code reviews and issue resolution. Built with Bun + TypeScript + Hono + Drizzle/SQLite.
- Origin:
git@github.com:iamriajul/gitlab-agent-webhook.git - Default branch:
main
Every feature, fix, or change follows this exact sequence. Do not skip steps.
1. BRANCH git checkout -b feat/<name> main
|
2. TEST (red) Write failing test in tests/<module>/<name>.test.ts
Run: bun test → confirm it FAILS
|
3. IMPLEMENT Write minimum code to make the test pass
Run: bun test → confirm it PASSES
|
4. VERIFY Run: bun run check (typecheck + lint + test)
ALL must pass. If not, fix and re-run.
|
5. FORMAT Run: bun run format
|
6. COMMIT git add <files> && git commit
(pre-commit hook runs check automatically)
|
7. REPEAT Go to step 2 for the next piece of work
|
8. PUSH git push -u origin feat/<name>
(pre-push hook runs full check)
- Steps 2-3 are TDD: red → green → refactor. Never write code without a failing test first.
- Step 4 is the gate. Code that fails
bun run checkdoes not get committed. - Step 6 triggers pre-commit hooks (typecheck + lint + test). If hooks fail, fix and re-commit.
| Action | Command |
|---|---|
| Verify all code | bun run check — runs typecheck, lint, test. Use before every commit. |
| Type check | bun run typecheck |
| Lint | bun run lint |
| Format | bun run format |
| Test | bun test |
| Dev server | bun run dev |
| DB migrations | bun run db:generate then bun run db:migrate |
| Build | bun run build |
There is exactly ONE way to verify code: bun run check. No alternatives exist.
src/
index.ts Entry point (Bun.serve with Hono)
server/ HTTP layer: routes.ts, middleware.ts (auth, request-id)
events/ Webhook parsing: parser.ts (Zod), router.ts (discriminated union), mention.ts
agents/ CLI spawning: runner.ts, types.ts (AgentType union)
jobs/ SQLite-backed queue: queue.ts, worker.ts, types.ts
gitlab/ Service-level only: reactions + status comments (via @gitbeaker/rest)
sessions/ Agent session tracking for resume: manager.ts
db/ Drizzle ORM: schema.ts, database.ts, migrations/
config/ config.ts (Zod-validated env), constants.ts, logger.ts (pino)
types/ result.ts (neverthrow re-export), errors.ts, events.ts, branded.ts
tests/ Mirrors src/ structure
- No
any. Useunknown+ Zod validation or type guards to narrow. - No
astype assertions except in parser.ts for validated Zod output narrowing. - No non-null assertions (
!). Use proper narrowing or Result types. - No
throw. Every fallible operation returnsResult<T, AppError>from neverthrow. - Exhaustive switches. All discriminated unions (
WebhookEvent,AppError,AgentType,JobPayload) must be handled without adefaultcase so TypeScript catches missing variants at compile time. - Branded types. Use
ProjectPath,IssueIid,MRIid,NoteId,JobId,SessionIdfromsrc/types/branded.tsto prevent mixing up IDs. process.envaccess. Always use bracket notation:process.env["KEY"](required bynoPropertyAccessFromIndexSignature).
- Import
ok,err,Resultfromsrc/types/result.ts(the ONE re-export point for neverthrow). - Error types are in
src/types/errors.ts. Use factory functions:parseError(),authError(),gitlabError(),agentError(),queueError(),sessionError(),configError(). - Never catch and swallow errors. Always propagate via
Resultor log with context. - Every
Resultmust be consumed. Use.match(),.andThen(),.unwrapOr(), or return it. Never discard a Result in a void expression — the error path must always be handled or propagated.
- Formatter: Biome. 2-space indent, 100-char line width. Run
bun run formatto auto-fix. - Imports: Biome organizes imports automatically. Use
import typefor type-only imports. - No
console.log. Useloggerfromsrc/config/logger.ts(pino). - No
forEach. Usefor...ofor.map()/.filter()/.reduce(). - Readonly by default. All interface properties and type members use
readonly.
- Upper layer (this service): Uses
GitLabService(src/gitlab/service.ts) via@gitbeaker/restfor service-level operations only: emoji reactions (acknowledgment) and status comments ("Agent started", "Agent failed"). Never import@gitbeaker/restoutside ofsrc/gitlab/. Do NOT pre-fetch context or attempt intent detection — the service layer has no NLP capability. - Lower layer (spawned agents): Agents use
glabCLI autonomously for ALL GitLab interaction — reading pipelines, fetching MR diffs, posting comments, pushing code. The agent decides what context it needs by reading the user's message. The security boundary is the GitLab token's scope.
- ALL agent process spawning goes through
src/agents/runner.ts. - Agents are spawned via
Bun.spawn()as child processes. - Each agent type (claude, codex, gemini) has its own adapter file in
src/agents/. - System prompts instruct agents to use
glabCLI for all GitLab interaction (non-interactive mode). - Agent sessions are tracked in SQLite for
--resumesupport on follow-up messages. - Agents run autonomously. There is no user at the terminal. System prompts enforce: no waiting for input, post all output as GitLab comments, create feature branches only (never push to protected branches). See
docs/ARCHITECTURE.md§4.4 for system prompt template and §4.6 for security boundaries.
- Write a failing test FIRST in
tests/<module>/<name>.test.ts. - Run
bun testto confirm it fails. - Write the minimum code to make it pass.
- Run
bun run checkto confirm everything passes. - Tests use
bun:test(describe,it,expect). No other test framework.
- Branch naming:
feat/<name>,fix/<name>,chore/<name> - Commit messages: conventional commits (
feat:,fix:,chore:,test:,docs:) - Never use
--no-verify. All commits must pass pre-commit hooks (typecheck + lint + format + test). - Never commit code that fails
bun run check.
Required: GITLAB_WEBHOOK_SECRET, BOT_USERNAME, GITLAB_TOKEN, GITLAB_HOST.
Optional: DEFAULT_AGENT (claude|codex|gemini, default: claude), PORT (default: 3000), DATABASE_PATH, LOG_LEVEL, WORKER_CONCURRENCY, AGENT_TIMEOUT_MS, CLAUDE_PATH, CODEX_PATH, GEMINI_PATH. See .env.example.
Minimum supported: GitLab 11.0+. When working on webhook parsing or the setup script, apply these rules:
- New Zod fields must be
.optional()or.optional().default(value)— never required unless the field has existed since GitLab 10.x. - Never use
z.enum([...])fornoteable_type,action, or similar string discriminators — usez.string()and handle unknown values by routing toignored. - New
actionvalues must be explicitly routed or fall through to theignoredcase; add a test for each. setup-webhooksscript: new API body fields must be gated onsupportsHookName-style version detection.- See
README.md §GitLab Version Compatibilityfor the version matrix.
- Add Zod schema in
src/events/parser.ts. - Add variant to
WebhookEventunion insrc/types/events.ts. - Add case to
routeEvent()insrc/events/router.ts(compiler will error if missing). - Add handler function. Write tests in
tests/events/.
- Create
src/agents/<name>.tsimplementing the same interface as existing adapters. - Add variant to
AgentKindinsrc/types/events.tsandAgentTypeinsrc/agents/types.ts. - Update
src/agents/runner.tsto dispatch to the new adapter. - Add
<NAME>_PATHto config schema insrc/config/config.ts. Write tests intests/agents/.