Contributing to openlore
June 27, 2026 · View on GitHub
Thank you for your interest in contributing. This document covers how to set up your development environment, run tests, and submit changes.
Development Setup
Requirements: Node.js ≥ 22.5.0, npm ≥ 9
git clone https://github.com/clay-good/openlore
cd openlore
npm install
Windows (PowerShell): if
npm installfails with "running scripts is disabled on this system", run this once to allow npm scripts for your user account:Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
Build TypeScript (outputs to dist/):
npm run build
Run the CLI directly during development (no build step needed):
npm run dev -- init
npm run dev -- analyze
npm run dev -- generate
To use the openlore command directly (instead of npm run dev --), link the package globally after building:
npm link
After that, openlore init, openlore analyze, etc. all work. Re-run npm run build before using the linked binary when you change source files.
Agent Context Setup (one-time, after cloning)
This repo dogfoods OpenLore's own tools, so your coding agent can be oriented from the first task. The tracked CLAUDE.md / AGENTS.md reference two things a fresh clone doesn't have yet — both regenerated locally, never committed:
.openlore/analysis/CODEBASE.md(git-ignored) — the architecture digestCLAUDE.mdloads at session start.- The OpenLore MCP server —
CLAUDE.mdinstructs your agent to call tools likeorient,search_code, andrecord_decision. These live in a.mcp.jsonthat is intentionally git-ignored (it's regenerated per machine; it merges with, never overwrites, the trackedCLAUDE.md).
One command wires both, with no API key:
npm run build
npm install -g . && openlore install --preset full # or, without a global link:
node dist/cli/index.js install --preset full
--preset full is recommended for contributors because the decisions-gate workflow below needs record_decision, search_specs, and check_spec_drift, which the lean default preset omits. This also builds the index (generating CODEBASE.md). See Agent Setup in the README for what these files contain and why they matter.
Running Tests
# Run all tests once
npm run test:run
# Run in watch mode during development
npm test
# Run with coverage report
npm run test:coverage
# Run a specific test file
npm run test:run -- src/cli/commands/analyze.test.ts
Tests use Vitest. The test suite runs entirely in-process with mocked filesystem/process calls — no real API calls or disk writes.
Integration & E2E Tests
The e2e suite (src/core/analyzer/e2e.integration.test.ts) runs the full analyze pipeline against the real openlore codebase and verifies that semantic queries return the correct source files. It is the primary non-regression guard for the analyzer.
Prerequisites:
openlore embed --local # switch to the on-device embedder and build the semantic index (no Docker, no API key)
Run:
npm run test:e2e
Tests auto-skip when the embedding server or index is missing, so they never break a cold CI environment. They do not replace npm run test:run — run both.
When to run before committing:
| Change area | Required |
|---|---|
src/core/analyzer/** | yes |
src/core/generator/stages/** | yes |
src/core/services/mcp-handlers/** | yes |
| Everything else | recommended |
Type Checking
npm run typecheck
This must pass with zero errors before any PR is merged. The project uses strict TypeScript.
Linting
npm run lint
Uses ESLint with typescript-eslint. Fix lint errors before submitting.
Project Structure
src/
├── api/ Programmatic API (no process.exit, no console.log)
├── cli/
│ ├── commands/ One file per CLI command + matching .test.ts
│ └── index.ts CLI entry point
├── core/
│ ├── analyzer/ Static analysis (file walker, dependency graph, etc.)
│ ├── drift/ Drift detection and spec mapping
│ ├── generator/ Spec generation pipeline and OpenSpec writer
│ └── services/ Shared services (LLM, config, MCP handlers)
├── types/ Shared TypeScript interfaces
├── utils/ Utilities (logger, errors, shutdown, etc.)
└── constants.ts All magic numbers and path strings
Key conventions
- Constants: All magic numbers and path strings belong in
src/constants.ts. Never hardcode.openlore,openspec, subdirectory names, or numeric thresholds inline. - API vs CLI: The
src/api/layer must never callprocess.exit()or write to stdout/stderr directly — it only throws errors. Thesrc/cli/layer handles all user-facing output. - File existence: Use the async
fileExists()fromsrc/utils/command-helpers.tsinstead offs.existsSync()in async contexts. - Error classes: Use the
errors.*factory functions insrc/utils/errors.tsfor typed, user-facing errors.
Writing Tests
Every CLI command file (src/cli/commands/foo.ts) should have a matching foo.test.ts. Follow the patterns in existing test files:
- Mock
../../utils/logger.jsto suppress output - Mock heavy dependencies (
repository-mapper,dependency-graph, etc.) - Test command configuration (options, defaults, descriptions)
- Test validation paths (invalid inputs should set
process.exitCode = 1) - Test the happy path using mocked services
For each beforeEach, reset process.exitCode = undefined and call vi.clearAllMocks().
Submitting Changes
- Fork the repository and create a branch:
git checkout -b my-feature - Make your changes — keep PRs focused on a single concern
- Ensure
npm run typecheck,npm run lint, andnpm run test:runall pass - If touching
src/core/analyzer/,src/core/generator/stages/, orsrc/core/services/mcp-handlers/: runnpm run test:e2e(requires a semantic index —openlore embed --local) - Open a pull request with a clear description of the change and why
The commit gate (decisions)
This repo ships a decisions pre-commit gate (installed by openlore install). When you git commit after changing source, it can block the commit and print JSON containing "gated": true — this is expected, not a crash. It means OpenLore detected an architectural decision that should be recorded before the change lands.
What to do when a commit is blocked:
- Read the
reasonfield. Common ones:verified(decisions are waiting for you to approve),approved_not_synced(runopenlore decisions --sync),no_decisions_recorded(source changed but nothing was recorded — runopenlore decisions --consolidate --gateto check for undocumented decisions). - To record a decision proactively (and keep commits instant), call
record_decisionbefore writing the code — see the checklist inCLAUDE.md. - Escape hatch:
git commit --no-verifyskips the gate for a commit that genuinely introduces no architectural decision.
The gate adds no LLM latency on the happy path; it only triggers extraction when source changed without a recorded decision.
Reporting Bugs
Open an issue at https://github.com/clay-good/openlore/issues with:
- The command you ran
- The error message or unexpected output
- Your OS, Node.js version (
node --version), and openlore version (openlore --version) - Output of
openlore doctorif relevant