Relies on BDK foundation (STARTUP_INSTRUCTIONS.md). Assumes environment discovery has already run (language, test runner, build tool are known).
BDK Setup
Initializes .bdk/settings.json for this project. Probes project files to detect languages, test commands, lint commands, and build commands, then confirms with the user before writing.
Workflow
Phase 1: Check existing config
If .bdk/settings.json already exists and --force was not passed:
- Read the file and show current values
- Ask user: "Settings already exist. Overwrite?" — if no, stop
Phase 2: Probe project files
Read the following files if they exist and extract command/tool hints:
Command resolution priority — always prefer running tools via the package manager over direct binary invocation:
package.jsonscripts →npm run <script>/yarn <script>/pnpm <script>(detect manager by lockfile:package-lock.json→ npm,yarn.lock→ yarn,pnpm-lock.yaml→ pnpm)- Python with
pyproject.toml→poetry run <tool>ifpoetry.lockexists, else direct - Ruby →
bundle exec <tool>always - Direct binary only as last resort (no package manager detected)
JavaScript/TypeScript (package.json):
- Scan
scriptsfor keys:test,test:unit,test:e2e,test:integration,lint,lint:fix,build,typecheck,compile - Emit as
npm run <key>(or yarn/pnpm equivalent per lockfile) - Check
devDependencies/dependenciesfor:vitest,jest,@playwright/test,cypress,eslint,prettier,typescript— only as fallback if no matching script key found - Presence of
next.config.*→ addnextto languages - Presence of
reactin deps → addreactto languages
Python (pyproject.toml, setup.py, requirements*.txt):
- Detect
pytest,ruff,mypy,black,flake8 - If
poetry.lockpresent →poetry run pytest,poetry run ruff, etc. - Else direct:
pytest,ruff, etc. - Language:
python
Go (go.mod):
- Test:
go test ./... - Check for
.golangci.ymlor.golangci.toml→ lint:golangci-lint run - Else lint:
go vet ./... - Language:
go
Rust (Cargo.toml):
- Test:
cargo test - Lint:
cargo clippy - Build:
cargo build - Language:
rust
Java (pom.xml or build.gradle):
- Maven:
mvn test,mvn package - Gradle:
./gradlew test,./gradlew build - Language:
java
PHP (composer.json):
- Check
scriptsfor test key; else detectphpunitorartisan test - Check for
phpcsorpintfor lint - Language:
php
Ruby (Gemfile):
- Test:
bundle exec rspec(if rspec in Gemfile) orbundle exec rake test - Lint:
bundle exec rubocop - Language:
ruby
C# (*.csproj or *.sln):
- Test:
dotnet test - Build:
dotnet build - Language:
csharp
Dart/Flutter (pubspec.yaml):
- Test:
flutter test(if flutter sdk) ordart test - Language:
dart
Phase 2b: Fill in tier and scoping forms
The full command is the least useful thing about a tool entry. BDK runs scoped checks throughout a plan and the full suite exactly once, at the end — so every entry needs a tier and, wherever the tool supports it, the narrower forms. Getting these right here is what stops every later agent from guessing at npm run test:unit -- <path> versus vitest related.
Set tier on every test-tools entry (fast | e2e) and every lint-tools entry (lint | format | typecheck). Never leave it out: BDK infers a missing tier from the tool name, and an inferred fast on an e2e runner means a slow suite runs at every group boundary.
{files} is a literal placeholder in these templates — callers substitute a path list. Derive per runner:
| Runner | scoped |
related |
failed |
incremental |
|---|---|---|---|---|
| vitest | npx vitest run {files} |
npx vitest related --run {files} |
npx vitest run --changed |
— |
| jest | npx jest {files} |
npx jest --findRelatedTests {files} |
npx jest --onlyFailures |
— |
| playwright | npx playwright test {files} |
— | npx playwright test --last-failed |
— |
| cypress | npx cypress run --spec {files} |
— | — | — |
| pytest | pytest {files} |
— | pytest --lf |
— |
| go test | go test {files} |
— | — | — |
| cargo test | cargo test {files} |
— | — | — |
| rspec | bundle exec rspec {files} |
— | bundle exec rspec --only-failures |
— |
| eslint | npx eslint {files} |
— | — | — |
| prettier | npx prettier --check {files} |
— | — | — |
| ruff | ruff check {files} |
— | — | — |
| tsc | — | — | — | npx tsc -b --incremental |
| mypy | mypy {files} |
— | — | mypy --incremental . |
Rules for anything not in the table:
- Package-manager script wrapping a runner that takes paths →
<script> -- {files}(npm run test:unit -- {files}). The--is required or the paths reach npm, not the runner. - A tool that takes no path list (most typecheckers, some build-mode linters) → omit
scoped; give anincrementalform if the tool has a cache flag. - Not sure a form exists → omit it. A wrong template is worse than a missing one: BDK falls back cleanly from a missing form, and silently runs the wrong thing with a broken one.
scopedandrelatedmust contain{files}; the config hook rejects settings where they do not, because such a command ignores the file list and quietly runs everything.
Phase 3: Confirm settings via AskUserQuestion
Use the AskUserQuestion tool with up to 4 questions in a single call:
- Test commands — multiSelect: true, options: each detected command as its own option + "None". User can add unlisted commands via "Other".
- Lint commands — multiSelect: true, same pattern
Confirm the full commands only. Tiers and scoped forms are derived from Phase 2b for whatever the user confirms — they are mechanical consequences of the runner, not preferences worth a question. Show them in the completion summary instead so a wrong derivation is visible. 3. Features — multiSelect: true, question: "Which features do you want to disable?", options: "Serena MCP", "CodeGraph MCP", "Caveman mode". Empty selection = all enabled. 4. Build command — only include if a build tool was detected or the language typically has one (e.g. TypeScript, Java, Rust); skip otherwise to stay under 4 questions
If more than 4 confirmation categories exist, prioritize: test → lint → features → build. Handle remaining categories with a follow-up AskUserQuestion call after writing.
"Other" is automatically appended by the UI — user can type any custom command there.
Phase 4: Write .bdk/settings.json
Create directory and file:
.bdk/
├── settings.json
├── plans/
└── design/
Write settings.json with confirmed values plus the tier/scoping forms from Phase 2b:
{
"$schema": "https://raw.githubusercontent.com/broneq/bdk/main/hooks/check-bdk-config/settings.schema.json",
"languages": ["typescript", "react"],
"test-tools": [
{
"type": "vitest",
"tier": "fast",
"command": "npm run test:unit",
"scoped": "npx vitest run {files}",
"related": "npx vitest related --run {files}",
"failed": "npx vitest run --changed"
},
{
"type": "playwright",
"tier": "e2e",
"command": "npm run test:e2e",
"scoped": "npx playwright test {files}",
"failed": "npx playwright test --last-failed"
}
],
"lint-tools": [
{"type": "eslint", "tier": "lint", "command": "npm run lint", "scoped": "npx eslint {files}"},
{"type": "tsc", "tier": "typecheck", "command": "npm run typecheck", "incremental": "npx tsc -b --incremental"}
],
"build-tools": [{"type": "tsc", "command": "npm run build"}],
"features": {
"caveman": true,
"serena": false,
"code-review-graph": false
}
}
type names the runner or framework (vitest, playwright, pytest, eslint, tsc), not the package manager — BDK reads it to infer a missing tier. Omit empty arrays (e.g. no build-tools key if none detected/provided), and omit any per-entry form the tool does not support. build-tools need no tier.
Phase 5: Initialize MCP tools
Run after writing settings.json.
code-review-graph — only if both conditions met:
features.code-review-graphis notfalsein written settings.mcp.json(project or~/.claude/mcp.json) contains acode-review-graphserver entry
Call mcp__plugin_bdk_code-review-graph__build_or_update_graph_tool (the MCP server itself is the source of truth — no need to check .code-review-graph/graph.db from the filesystem; the tool is incremental and skips work when the index is current).
- Success → print
[setup] code-review-graph: index built. - Failure (MCP not reachable / tool error) → print warning and continue — do not abort setup:
[setup] code-review-graph: build failed. Call `mcp__plugin_bdk_code-review-graph__build_or_update_graph_tool` manually once MCP is reachable.
Prefer the MCP tool over shelling out to uvx code-review-graph build — same binary under the hood, but the MCP path uses the already-running server and works inside sandboxed/restricted environments where uvx may not be available.
Serena — no manual action needed at setup time. Serena's active-project state is in-memory only (resets every Claude Code session), so per-session activation is handled by the hooks/activate-serena/activate.py SessionStart hook. The hook emits an instruction telling Claude to call mcp__plugin_bdk_serena__activate_project whenever both features.serena is enabled and .mcp.json declares a serena server. No setup-time call required.
Phase 6: Git guidance
Recommend:
- Commit
.bdk/settings.json(shared with team — consistent commands for all contributors) - Add to
.gitignore:.bdk/plans/and.bdk/design/(personal artifacts)
Show the gitignore lines to add:
.bdk/plans/
.bdk/design/
Ask: "Add these to .gitignore now? [y/n]"
Completion
Print:
[setup] .bdk/settings.json created.
[setup] Test tiers: {tier}={command} (scoped: {scoped|none}) …
[setup] Lint tiers: {tier}={command} (scoped: {scoped|none}) …
[setup] Directories created: .bdk/plans/, .bdk/design/
[setup] Restart your Claude Code session — BDK will inject project settings on startup.
The tier lines exist so a wrong derivation is caught now, by the one person who knows the project, rather than showing up later as a slow suite running at every group boundary.