Code conventions¶
The repo enforces conventions through tooling, not docs, every rule below is checked by bun run check. This page is a tour of what's wired so you know what to expect.
Runtime and language¶
- Runtime. Bun for the application; Node.js 20 for the Claude Code CLI subprocess. Both are installed in the Docker
basestage. - Bun version. Pinned in
.tool-versions(bun 1.4.1). All workflows useoven-sh/setup-bunwithbun-version-file: .tool-versions..gitlab-ci.yml'soven/bun:<ver>pins are also verified bycheck:docs-versions, which additionally asserts the two digest-pinned references agree with each other and that the file yields at least one reference to check. - TypeScript. Strict mode plus the strictest flags:
exactOptionalPropertyTypes,noUncheckedIndexedAccess,useUnknownInCatchVariables,noUnusedLocals,noUnusedParameters,noImplicitReturns,noFallthroughCasesInSwitch,noImplicitOverride,noPropertyAccessFromIndexSignature. Module resolution isbundler, imports do not need.jsextensions.
ESLint¶
eslint.config.mjs is the flat config:
- Preset.
@eslint/jsrecommended +typescript-eslint:strictTypeChecked+stylisticTypeChecked. - Plugins.
eslint-plugin-security,eslint-plugin-simple-import-sort,prettier. - Notable rules.
@typescript-eslint/strict-boolean-expressions: error@typescript-eslint/no-explicit-any: warn@typescript-eslint/no-unused-vars(underscore-prefix exempted)simple-import-sort/imports,simple-import-sort/exports: auto-fixablecomplexity: warn(15),max-lines-per-function: warn(120),max-nested-callbacks: error(3)- Security rules from
eslint-plugin-security:recommended - Special restriction.
src/workflows/ship/scoped/triage.tscarries ano-restricted-syntaxrule forbidding GitHub mutations: ship-side triage is suggest-only.
Prettier¶
.prettierrc:
| Rule | Value |
|---|---|
semi |
true |
singleQuote |
false |
trailingComma |
"all" |
printWidth |
100 |
tabWidth |
2 |
endOfLine |
"lf" |
arrowParens |
"always" |
bracketSpacing |
true |
Em dashes¶
The repo-wide style rule (per ~/.claude/CLAUDE.md) forbids em dashes (U+2014). Use commas, colons, semicolons, or parentheses instead. Enforced in CI via scripts/em-dash-sweep.ts --check (also wired into the umbrella bun run check). The check skips src/db/migrations/, CHANGELOG.md, test/**/fixtures/, and build outputs. To remediate an offending line locally, run bun run scripts/em-dash-sweep.ts <path> to apply the heuristic rewrite, then hand-review the result. See issue #116 for the original sweep.
Pre-commit hooks¶
.husky/pre-commit:
gitleaks protect --staged, secret scan. Hard exit 1 ifgitleaksis missing.bunx lint-staged, Prettier and ESLint on staged files.
.husky/commit-msg runs commitlint against @commitlint/config-conventional. Allowed types: feat, fix, docs, style, refactor, test, chore, perf, build, ci, revert, localize, bump.
Logging¶
- Structured JSON via
pinowith child loggers per request. - The ship workflow draws every
eventvalue from the typedSHIP_LOG_EVENTSconstant insrc/workflows/ship/log-fields.tsso a typo is a compile error. See../operate/observability.md.
Configuration¶
- All process-level configuration is validated via
zodat startup insrc/config.ts. The process exits with a clear error if any required variable is missing or malformed. - Environment variable group is the canonical doc surface: see
../operate/configuration.md.
Scripts¶
The full list lives in ../operate/setup.md. For PRs, what matters is bun run check:
bun run audit:ci (used by CI) wraps bun audit --json to gate on severity:
- Blocks on
highandcriticaladvisories. - Warns on
moderateandlow. - Inline GHSA allowlist in
IGNOREDarray; each entry hasghsa,reason, andexpires(ISO date). Expired entries become warnings on next run. - Retries up to 3 times when
bun auditproduces no JSON, then emits a::warning::and passes. An unreachable advisory service yields no verdict either way, so it degrades to a loud skip rather than blocking every merge for the length of an upstream outage. Unparseable JSON still hard-fails, andtrivy-scan.ymlscans the published images daily as the independent control. - Parses
bun audit --json's real shape (a map of package name to advisory list, GHSA id taken from each advisory'surl) and hard-fails on an unrecognised shape so a format change cannot silently degrade the gate tototal=0. A severity value outside the known set is not shape drift: it blocks with anUNKNOWN SEVERITYannotation so the rest of the report still reports. GHSA extraction tolerates a query string, fragment or trailing slash and matches case-insensitively, so a cosmetic url change cannot silently void an allowlist entry.AUDIT_CI_ALLOWLIST_JSONoverrides the inlineIGNOREDlist for tests only. - Never lets registry-supplied text start a GitHub Actions workflow command: advisory fields embedded in
::error::/::warning::/::notice::lines are escaped per the@actions/coreescapeDatarule (%/ CR / LF to%25/%0D/%0A), and raw stdout or stderr dumps are printed one line at a time behind a>prefix so no dumped line can begin with::.
CI pipeline¶
Six pipeline files; each owns one responsibility.
| Workflow | Trigger | Owns |
|---|---|---|
.github/workflows/ci.yml |
pull_request + push: main + workflow_call |
Quality gates only: typecheck, lint, format, audit:ci, test, build |
.github/workflows/secrets-scan.yml |
push: branches-ignore: [gh-pages] + workflow_dispatch |
Standalone gitleaks scan, decoupled so every push (incl. chore/docs) is gated |
.github/workflows/release-please.yml |
push: [main, beta] |
release-please maintains a Release PR per branch; merging it cuts the release then calls docker-build.yml, and on a stable main release dispatches github-app-released to chrisleekr/helm-charts to open the chart-sync PR |
.github/workflows/docker-build.yml |
workflow_call + workflow_dispatch |
Reusable image builder: matrix split-and-merge (amd64 on ubuntu-24.04, arm64 on ubuntu-24.04-arm). Tags each variant <version>-<variant> plus a mutable latest-<variant> on prod releases. No CVE scanning |
.github/workflows/trivy-scan.yml |
schedule (daily 14:00 UTC) + workflow_dispatch |
Trivy CVE scan of the published Docker Hub images, SARIF to the GitHub Security tab. Orchestrator legs gate on CRITICAL/HIGH; daemon legs report only |
.gitlab-ci.yml |
every branch (gates) + main (images) |
GitLab CI: the same quality gates on every branch, then latest-orchestrator / latest-daemon to the GitLab container registry on main |
Notes:
- Multi-arch images. amd64 builds on
ubuntu-24.04, arm64 builds natively onubuntu-24.04-arm(free for public repos). Both runners are explicitly pinned (notubuntu-latest) so the rolling alias cannot silently flip to a new major. Manifest assembled bydocker buildx imagetools create. GHA cache scoped per arch. - Runners pinned repo-wide. Every
runs-on:across.github/workflows/targets an explicit image (ubuntu-24.04), never a*-latestrolling alias.ci.ymlrunsbun run check:runner-pins(scripts/check-runner-pins.ts), which fails the build if anyruns-on:is on a*-latestalias; matrix expressions are exempt. See issue #173. - Defense in depth. Every dynamic input flowing into a
run:block is passed viaenv:first. - CVE scanning is decoupled from releases.
trivy-scan.ymlis a separate workflow, not a job insidedocker-build.yml. A scan gated behindneeds: mergeruns after the manifest lists are already pushed, so it can never stop a vulnerable image from shipping; all it did was fold a CVE verdict into the reusable workflow's conclusion and skipnotify-helm-charts, withholding the chart-sync PR for an image that was already live (this happened on v1.17.0). The daily schedule also catches advisories published after a release, which is how that verdict actually moves. - Releases run on release-please. Push to
mainopens/updates a stable Release PR; merging it tagsv<x.y.z>, updatesCHANGELOG.md+package.json, creates the GitHub release, and builds the prod image (latest). Thebetabranch does the same for prereleases (v<x.y.z>-beta, nolatest). Onlyfeat/fix/!commits bump the version. Branch selection uses two source-controlled config + manifest pairs (release-please-config.json/.release-please-manifest.jsonon main, the.betavariants on beta). On a stablemainrelease, thenotify-helm-chartsjob (afterdocker) sends arepository_dispatchtochrisleekr/helm-chartsto open the chart-sync PR.
Documentation discipline¶
When a PR touches any of these surfaces, update the matching page under docs/:
| Source | Doc |
|---|---|
src/config.ts env schema |
docs/operate/configuration.md |
src/shared/dispatch-types.ts |
docs/operate/observability.md + docs/build/architecture.md |
src/orchestrator/triage.ts |
docs/operate/runbooks/triage.md |
src/webhook/ routing or idempotency |
docs/build/architecture.md |
src/k8s/ephemeral-daemon-spawner.ts |
docs/operate/runbooks/daemon-fleet.md + docs/operate/deployment.md |
src/daemon/ lifecycle |
docs/operate/runbooks/daemon-fleet.md |
src/workflows/ registry, dispatcher, handlers |
docs/use/workflows/*.md |
New MCP server in src/mcp/ |
docs/build/extending.md |
| New Pino field or metric | docs/operate/observability.md |
bun run docs:build (strict) runs in CI. check:docs-sync blocks PRs that touch src/workflows/** without an accompanying docs change.