---
title: "Development"
description: "Set up a local Chickpea checkout, run the serial verification gate, and open a pull request."
---

> Documentation Index
> Fetch the complete documentation index at: https://docs.chickpea.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Development

A **development checkout** is a local clone of Chickpea that runs the Node target from source. Where a deployment serves one Slack workspace, a checkout serves one contributor and keeps its state in disposable local files. This page covers the checkout, the everyday commands, the verification gate every change runs through, and the branch and pull request rules.

Chickpea has no hosted continuous integration. This is deliberate: the repository uses no GitHub Actions workflows and requires no GitHub-hosted test results for merging, so every check below runs on your machine.

## Prerequisites

- **Node 24.x, minimum 24.20.0.** Both Chickpea packages set `engines.node` to `>=24.20.0 <25`; `.nvmrc` pins `24.20.0` for development, builds, and verification. Run `nvm install && nvm use`. This is the only routinely tested Node major; update the pin for future Node 24 patch/security releases and verify that baseline once.
- **A GitHub account.** Fork [pejmanjohn/chickpea](https://github.com/pejmanjohn/chickpea) if you do not have push access.
- **Your own Slack test workspace, credentials, and data**, for any change you want to exercise against real Slack. Never point a checkout at a workspace or state you did not create for testing.

## Set up a checkout

```sh
git clone https://github.com/pejmanjohn/chickpea && cd chickpea
nvm install && nvm use
npm ci
npm run dev
```

`npm run dev` starts the Node lane through Vite (`vite dev --config vite.node.config.ts`). To reach `/admin` you also need a setup link, which means exporting `CHICKPEA_AUTH_SECRET` and the two variables `npm run setup:link` prints. The [Node get started page](/start/get-started-node) has that sequence in full.

Chickpea is offline-safe by default. Left unset, most variables take a local path and the app makes no external calls, so a fresh checkout runs and its tests pass without a single credential. State defaults to SQLite; set `TAG_DB_PATH=:memory:` and `SLACK_STATE_DB_PATH=:memory:` only for disposable development.

## Everyday commands

```sh
npm test                    # typecheck, then node --test over tests/
npm run dev                 # local Node lane
npm run build               # Cloudflare build
```

`npm test` runs `tsc --noEmit` first and then Node's test runner over `tests/*.test.ts` and `tests/usage/*.test.ts`, with `DO_NOT_TRACK=1` set so a test run sends no telemetry. Tests live beside the source areas they cover; `CONTRIBUTING.md` maps each area to its directory under `src/`.

`npm run build` is the Cloudflare build. It runs `vite build` against `vite.config.ts` and then validates the artifact in `dist-cf`, proving Wrangler will consume a real bundle and that the Node-only database entry stayed out of the Cloudflare discovery path.

## Verify

Run builds and tests **serially**. Tests inspect generated artifacts, so a concurrent build can make them read a partially written artifact. Select the applicable checks in this order:

For iteration, select the relevant checks below. Once the candidate is stable
and committed, run `npm run verify:regression -- --mode release` once. The clean
export performs the full suite and offline turn/durability/provider checks, so
do not repeat them as outer release gates.

```sh
npm run build
TAG_DB_PATH=:memory: SLACK_STATE_DB_PATH=:memory: CHICKPEA_AUTH_DB_PATH=:memory: TAG_REQUIRE_LOOPBACK=1 npm test
npm run verify:admin-ui
DO_NOT_TRACK=1 node scripts/verify-flue-offline-turn.mjs
DO_NOT_TRACK=1 npm run verify:durability
DO_NOT_TRACK=1 npm run verify:providers
DO_NOT_TRACK=1 npm run verify:cf-smoke
npm run verify:lockfile-integrity
```

| Command | What it proves |
|---|---|
| `npm run build` | The Cloudflare bundle builds and its artifact validates. |
| `npm test` | Typecheck passes and the test suite is green. |
| `verify:admin-ui` | The `/admin` configuration loop works with no Slack network access: real routes, in-memory store, an Owner, Agents, channel grants, and the one-body memory contract. |
| `verify-flue-offline-turn.mjs` | The full turn policy under the built Node server with zero external traffic: signature checks, one streamed final, and a sanitized failure when the provider returns 500. |
| `verify:durability` | Conversation state survives a process restart. A second process on the same SQLite file replays the first turn, and a different file does not. |
| `verify:providers` | The same signed mention is answered through every API-backed provider route, against local fake provider endpoints. |
| `verify:cf-smoke` | The Cloudflare target really works in workerd. |
| `verify:lockfile-integrity` | No `package-lock.json` entry is missing its subresource integrity hash. |

The offline verifiers use fake Slack and provider services and isolated local state. They need no production credentials.

### Isolated databases

The environment prefix on the test line is not decoration. `TAG_DB_PATH`, `SLACK_STATE_DB_PATH`, and `CHICKPEA_AUTH_DB_PATH` set to `:memory:` keep the run off any SQLite file on disk, so a test never resets state an operator is using. `TAG_REQUIRE_LOOPBACK=1` turns a silent skip into a hard failure: the loopback-dependent suites skip themselves when a sandbox denies `listen(127.0.0.1)`, and a silently skipped parity suite lets a regression through.

### What the Cloudflare smoke gate does

`verify:cf-smoke` builds both Cloudflare deployment profiles, boots the core profile under a real workerd through `wrangler dev`, and drives the first-run story against the in-memory fake Slack and fake provider: app creation, bot OAuth, a signed Events proof, and first-Owner sign-in. It then asserts what only workerd can prove. A signed synthetic `app_mention` verifies against the stored signing secret and delivers a final. An identical redelivery is deduped. A workerd restart on the same persistence directory still dedupes the original event and still admits a thread reply. A tampered signature is rejected. Every outbound URL points at `127.0.0.1`.

It takes several minutes.

> **The smoke gate deletes its state directory**
>
> `verify:cf-smoke` removes `.wrangler-state` in the repository root. Do not run it against a directory whose `.wrangler-state` you need to keep: that directory is disposable smoke-test state.

### After you commit

```sh
npm run verify:oss-export
```

This one checks an archive of **HEAD**, not your uncommitted edits, so commit first. It installs from the lockfile into a temporary directory, then runs tests, the offline runtime checks, and a deployment dry run there. Private working documents must stay outside the public export; adding a public document requires updating the explicit export allowlist in the script.

Local automated checks cannot establish real Slack or provider acceptance. Changes to setup, authority, delivery, or persistence also need the attended checks from the maintainers' release checklist. Do not describe a build or a fake-backend result as live acceptance.

## Drive the local server with real Slack events

`npm run slack:bridge` opens a Slack Socket Mode connection, re-signs each event envelope with your app's signing secret exactly as Slack's HTTP delivery would, and posts it to your local server. Use it when you want real Slack events without running a public tunnel. It is a development tool, not a transport: it has a single consumer, it acknowledges immediately, so Slack's retry semantics are not exercised.

1. In the Slack app console, enable Socket Mode. While Socket Mode is on, Slack stops delivering events to the HTTP Request URL.
2. Create an app-level token with the `connections:write` scope. It starts with `xapp-` and is separate from the bot's `xoxb-` token.
3. Put that token and the app's signing secret in `.env.slack.local` at the repository root, as `SLACK_APP_TOKEN` and `SLACK_SIGNING_SECRET`. Shell values win over file values, and `--env <path>` points at a different file.
4. Start the local server, then run `npm run slack:bridge`. It forwards to `PORT`, which defaults to `3583`.

Do not run the bridge at the same time as another Socket Mode consumer; they steal events from each other. Disable Socket Mode again before testing the normal HTTP Events API path.

## Open a pull request

For a substantial feature or behavior change, open an issue first so the problem and the scope are agreed before you write code.

1. Create a topic branch from current `origin/main`, or `upstream/main` in your fork.
2. Make the change and add regression coverage for changed behavior. Keep unrelated refactors and generated-asset changes out of the pull request.
3. Push the topic branch and open a pull request against `main`. Explain the problem, the change, and what you actually verified. Link the issue if there is one.
4. Record your local verification results in the pull request and resolve review feedback.

Maintainers squash-merge on GitHub. A maintainer may instead merge a worktree into root `main`, verify the merged result locally, and push directly; contributors without push access still use pull requests. Never force-push `main`. Merging or pushing does not deploy Chickpea and does not publish a release.

## What never to commit

- `.env` and `.dev.vars` files, SQLite databases, credential keyrings, setup links, and live test transcripts. `.gitignore` covers the usual paths, but the rule is yours to keep.
- Hand edits to generated files. Use the documented builder instead, such as `npm run avatars:build` or `npm run brand:build`.
- Base64 images in TypeScript. Public raster images live in `assets/`; add a new runtime image to `src/assets/public-assets.ts` and to the source-export binary allowlist.
- A dependency change without its lockfile. When a dependency changes, include `package-lock.json` and run `npm audit --omit=dev` along with `npm run verify:lockfile-integrity`.
- A vulnerability. Report it through GitHub's private vulnerability report form, with no working credentials, private Slack content, or exploit details in a public issue or pull request.

Contributions are provided under the repository's Apache 2.0 license.

## Next steps

- [Contribute](/contribute): the architecture and releasing pages that follow this one.
- [How Chickpea works](/start/how-it-works): the layers a change travels through, from the Slack app to the model provider.
- [Get started on Node](/start/get-started-node): the Node get started path, including the setup link and the variables `npm run dev` expects.
- [Deploy and operate](/operate): what production hosting looks like on Cloudflare and on Node.
- [Environment variables](/reference/environment-variables): every environment variable a deployment reads, with its default.

Source: https://docs.chickpea.co/contribute/development/index.mdx
