# AGENTS.md Concise entrypoint for AI agents and new contributors working in this repo. For contributor process see [`CONTRIBUTING.md`](CONTRIBUTING.md). **Assistant sessions:** this file and `CONTRIBUTING.md` are the public entrypoints. For optional workstation instructions, discover private context as described below. **Design context and working records.** [`docs/README.md`](docs/README.md) is the map — from there, current contracts are [`docs/specs/`](docs/specs/README.md) (held to the code by `docs-inventory -strict`), decided architecture is [`docs/decisions/`](docs/decisions/README.md), operational procedures are [`docs/runbooks/`](docs/runbooks/README.md). Existing public proposals are indexed in [`docs/prd/`](docs/prd/README.md). New internal analyses, handoffs and experiments belong in the separate private context repository, discovered with `git config --get crewship.internalContext`; read its `AGENTS.md` and topic index when available. See the [boundary and migration rules](docs/development/private-context.md). Public builds and contributions must not require it. Read the specification that matches your task, not the whole archive. **Placement and tooling:** use the [repository layout](docs/development/repository-layout.md) and [script catalog](scripts/README.md) before adding a directory or running an unfamiliar helper. ## What Crewship is Open-source AI agent orchestration platform. A single Go binary embeds a Next.js static export; agents run inside Docker containers behind a credential-injecting sidecar proxy. Orchestration is **queue + trigger** based (cron schedulers, webhooks, an autonomous assignment queue, durable pipeline waitpoints), not infinite loops. ## Before you touch an issue: claim it Ten sessions work this repo at once and all push as the same GitHub account, so the assignee field cannot distinguish "another session has this" from "that's me". #1481 duplicated #1471's fix and both sides lost the work. ```bash scripts/claim-issue.sh # checks for a live claim, then claims scripts/claim-issue.sh --check # read-only: who holds it, which PRs touch it scripts/claim-issue.sh --list # every open claim in the repo scripts/claim-issue.sh --release "why you stopped" ``` Claim **before the first commit**, release in the same thread when you stop — including when you failed, so the next session starts from your evidence. It exits 3 without posting if someone else holds it. Full convention and failure modes: [CONTRIBUTING.md → Claiming an issue](CONTRIBUTING.md#claiming-an-issue-before-you-work-it). ## Build, run, test ```bash ./dev.sh start # dev: Go :8080 + Next.js :3001 (hot reload). `make up` is an alias. make build # prod: pnpm build → static export → embedded → ./crewship make build:go # Go binary only (+ sidecar) # Verification loop — run until green before considering work done: go test ./... -count=1 -timeout 40m # all Go tests (Docker tests self-skip if daemon absent) go vet ./... # static analysis pnpm lint # ESLint (UI/frontend changes) pnpm build # confirm static export still builds (cross-cutting changes) ``` Multi-instance: a clone named `crewship_N` auto-offsets all ports/data/sockets, so parallel agents/worktrees run conflict-free (see `dev.sh`). **Throwaway servers** (a second, disposable instance for a test or a screenshot pass): start them only with `scripts/throwaway-server.sh run NAME --binary /tmp/crewship-N-dev -- CMD` (or `start`/`stop`). It gives the server its own data directory, port, container prefix and installation identity, and on exit removes exactly the containers and volumes that server created before deleting its data. A server started by hand and its data directory deleted leaves containers no installation can find again. `restart NAME [--down-seconds N]` stops only the server process and starts it again on the same data, port and identity (no reseed); `scripts/acceptance-chat-recovery-throwaway.sh` uses it to prove host chat recovery (#4824). Protected host capture needs a state root whose ancestors are not group-writable (`CREWSHIP_THROWAWAY_STATE`); otherwise the server silently uses the legacy exec transport. **`web/out` embed:** `web/embed.go` embeds the Next.js export, so `web/out/` must exist for *any* Go build to compile. It is tracked-by-one-file (`web/out/.placeholder.html`) so `go build ./...` works in a fresh worktree with zero setup — **do not `mkdir web/out` or hand-roll an `index.html` stub**, and do not let a `git add -A` delete the placeholder after a build. Such a binary has no UI (every UI route → `503` + an explanatory page); run `make build` for a real one. Details: [`docs/development/web-out-embed.md`](docs/development/web-out-embed.md). ## Before you merge: confirm the review happened CodeQL findings can live only in a run’s annotations; inspect them as well as the aggregate check. Review the final diff and document concrete findings, fixes and validation in the PR. Keep required CI checks green; skipped checks are not execution evidence. A self-review must be labelled as such, not presented as an independent review. See [CONTRIBUTING.md](CONTRIBUTING.md#review-before-merge). **Merge queue:** after review, submit with `gh pr merge --auto --squash` (and `--match-head-commit ` for the reviewed head). Let the queue check the current base; do not repeatedly update branches to chase main. Do not use `--admin` or a REST bypass except a recorded emergency with the responsible operator, reason, reviewed SHA and successful exact-SHA required checks. See [CONTRIBUTING.md → Merge through the queue](CONTRIBUTING.md#merge-through-the-queue). **Risk tiers:** low-risk changes (docs, tests, additive code behind a default-off flag) merge on green required checks and an automated review by another session; authorization, privacy, data deletion, migrations, CI workflows, branch rules and infrastructure need a deep independent review of the exact head SHA by a separate session plus a regression test. Only irreversible actions (production, destructive data operations, secrets, history rewrites, removing required checks) wait for the maintainer. Cap review at three rounds, then merge with recorded debt or close. Fix a red main by revert first. Details: [CONTRIBUTING.md → Review scaled by risk](CONTRIBUTING.md#review-scaled-by-risk). ## Architecture map (`internal/`) ```text api/ HTTP API (stdlib http.ServeMux, "METHOD /path" patterns) orchestrator/ agent execution engine — container exec + streaming + env build sidecar/ credential-injecting forward proxy (127.0.0.1:9119) keeper/ credential gating + gatekeeper + Phase-2 self-improvement routines scheduler/ robfig/cron engine — fires scheduled agent runs pipeline/ pipeline runner: schedules, webhooks, durable waitpoints (HITL) harbormaster/ approval gate for dangerous actions (approvals_queue) policy/ per-crew autonomy matrix (strict|guided|trusted|full × warn|block) paymaster/ cost ledger + budgets + per-model pricing journal/ durable run/event journal (the shared audit + activity spine) episodic/ consolidate/ memory/ agent memory: recall before runs, consolidate after manifest/ declarative `crewship apply` (plan/validate, ~20 resource kinds) skills/ SKILL.md parser + importer + bundled skills auth/ JWE (NextAuth v5 compatible) validation database/ SQLite via database/sql (modernc.org/sqlite, pure Go, no CGO) provider/ infra interfaces: Container (Docker/Apple), Storage, State scrubber/ httpsafe/ safepath/ credential scrubbing, SSRF + path-traversal guards ws/ WebSocket hub (pub/sub channels) config/ defaults → YAML → env vars (env wins) ``` Frontend: `app/` (Next.js App Router), `components/` (`ui/` = shadcn), `hooks/`, `lib/` (Zod schemas, CASL permissions). ## Conventions - **Go**: no ORM — raw `database/sql` with `QueryRowContext`/`ExecContext`. Provider interfaces for all infra. Imports stdlib → external → internal. Table-driven tests; `t.Skip()` for optional deps (Docker). - **Errors**: two envelopes, and the split is real rather than aspirational. `replyError` writes `{"error": "…"}` and is the overwhelming majority (~2200 call sites); `writeProblem` writes RFC 7807 (~450). Both send `Content-Type: application/json` — `writeProblem` routes through `writeJSON`, which hard-sets it, so an RFC 7807 body does **not** arrive as `application/problem+json` unless a handler sets that header itself. Match the surrounding file rather than converting one; `cmd/gen-openapi` documents whichever the handler uses, so a mismatch shows up as a contract finding rather than a surprise. (This line used to read "RFC 7807 Problem Details for errors", which described about 17% of the code — #1919.) - **DB**: `snake_case`, plural tables; CUID ids (`internal/api/cuid.go`); soft delete via `deleted_at`; timestamps TEXT (RFC 3339). - **Frontend**: Next.js 16 App Router (static export), Tailwind 4 + Radix/shadcn, Zod, CASL (`lib/permissions/`). ES modules only. `@/*` path alias. - **Responsive**: two axes, and picking the wrong one is how tablets broke (#2483). *Layout* keys on width — `md:` for the phone/desktop split, matching `useIsMobile`'s 768. *Touch sizing* keys on the pointer — `coarse:` (`@media (pointer: coarse)`, defined in `app/globals.css`), so a 44px target applies to every touch device at any width instead of stopping at some breakpoint. Note `--spacing` is `0.23rem`, so `h-12` is the 44px class, not `h-11`. Navigation is defined once in `lib/nav-sections.ts` and read by both the rail and the phone sheet. - **Commits**: conventional (`feat:`, `fix:`, `refactor:`, `docs:`, `test:`, `chore:`). Branch from `main`. ## Migrations **One `.sql` file per migration** in `internal/database/migrations/`. The directory *is* the registry — there is no central list to edit, which is the point: two people adding a migration add two files and cannot conflict. ```bash date -u +%Y%m%d%H%M%S # the version stamp — filenames are # _.sql ``` Versions must be strictly ascending, so **append, never insert** a stamp older than one already committed. Sequential numbers are gone: `v1..v169` were allocated that way and are frozen in the `legacyMigrations` slice in `migrate.go` — **nothing at or below v169 may move or change**, they are applied in databases nobody controls (`migrate_version_scheme.go` enforces this at startup). Migrations that genuinely need Go (schema discovery at apply time, SQLite table rebuilds) still go in that slice with a timestamp version. Everything expressible as plain SQL belongs in a file. `migrations/post_deploy/` runs after the server is serving instead of blocking boot — read its README first, it is a contract about what the running code must tolerate, not a free win. Runs at startup, tracked in `_migrations`. Check your work with `go run ./scripts/lint-migrations`, which fails if an already-shipped migration's SQL changed under it. **Prisma is for TypeScript types only — never run `prisma migrate`.** Full detail: [`internal/database/migrations/README.md`](internal/database/migrations/README.md) and [`docs/guides/migrations.mdx`](docs/guides/migrations.mdx). ## NEVER DO Five of these are checked, not promised — `go run ./scripts/agents-invariants`, which the `Shell` CI job runs. They are marked. The rest are about what a person does and cannot be, so they are prose and say so. **Checked:** - Never add API routes under `app/` — static export silently drops them. - Never use `"sqlite3"` as the driver name — `modernc.org/sqlite` registers `"sqlite"`. - Never use `npm`/`yarn` — `pnpm` only (a stray lockfile is the evidence). - Never change sidecar UID (1002) or agent UID (1001) — it's a security boundary. - Never track root assistant instructions (`CLAUDE.md`, `CODEX.md`, `GEMINI.md`), workspaces (`.claude/`, `.codex/`, `.cursor/`), `.github/copilot-instructions.md`, private-context aliases, internal mockups or captured run reports. The invariant reads the Git index; ignored local files remain allowed. Public test fixtures stay public. **Prose — nothing enforces these but you:** - Never skip the verification loop (`go test` + `go vet`) — or ship without tests. - Never change the GCM byte layout in `internal/encryption/` — breaks all stored creds. - Never discard WIP — `git stash` before switching branches, never `git checkout .`. - Never start an issue without checking it for a live claim — the assignee is not a lock when every session is the same account. - Never commit secrets (`.env.local`, real keys) — gitleaks runs on every PR, so this one has a net, just not a preventative one. ## When unsure Find the right category from [`docs/README.md`](docs/README.md), read the spec that matches the task ([`docs/specs/`](docs/specs/README.md)), then the proposal that motivated it ([`docs/prd/`](docs/prd/README.md)) — before assuming design or requirements. Keep documents current after significant changes — and prefer pointing at the gate that enforces a fact over restating the fact, because prose with no check behind it is the thing that rots.