Contributing
Contributions to DojOps are welcome. This guide covers development setup, coding standards, testing, and how to add new tools and agents.
Prerequisites
- Node.js >= 20
- pnpm >= 8
- TypeScript >= 5.4
Development Setup
# Clone the repository
git clone https://github.com/dojops/dojops.git
cd dojops
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Run tests
pnpm test
# Run linter
pnpm lintMonorepo Structure
packages/
cli/ CLI entry point + TUI (@clack/prompts)
api/ REST API (Express) + web dashboard
tool-registry/ Tool registry + custom tool system (built-in + custom tool discovery)
core/ LLM providers (6) + specialist agents (16) + CI debugger + infra diff + DevOps checker
planner/ Task graph decomposition + topological executor
executor/ SafeExecutor + policy engine + approval workflows + audit log
tools/ 12 built-in DevOps tools
scanner/ 9 security scanners + remediation engine
session/ Chat session management + memory + context injection
sdk/ BaseTool<T> abstract class + Zod re-export + verification types + file-reader utilitiesPackage scope: @dojops/*
Dependency flow: cli -> api -> tool-registry -> tools -> core -> sdk
Build, Test, Lint
# Build all packages via Turbo
pnpm build
# Dev mode (no caching)
pnpm dev
# Run all tests (Vitest)
pnpm test
# Run tests for a specific package
pnpm --filter @dojops/core test
pnpm --filter @dojops/api test
# ESLint across all packages
pnpm lint
# Prettier write
pnpm format
# Prettier check (CI mode)
pnpm format:check
# Run CLI locally (no global install)
pnpm dojops -- "Create a Terraform config for S3"
pnpm dojops -- serve --port=8080Code Style
- TypeScript — ES2022, CommonJS modules
- ESLint — Enforced across all packages
- Prettier — Auto-formatting
- Husky + lint-staged — Pre-commit hooks run linting and formatting
Key conventions:
- Use Zod for all schema validation (inputs, outputs, API requests)
- Use
parseAndValidate()for LLM response parsing - Follow the existing barrel export pattern (
index.tsin each package) - Prefer interfaces over type aliases for public APIs
- Use
async/awaitover raw Promises
Testing
DojOps uses Vitest for testing. Current coverage:
| Package | Tests |
|---|---|
@dojops/runtime | 481 |
@dojops/core | 465 |
@dojops/cli | 247 |
@dojops/api | 236 |
@dojops/tool-registry | 224 |
@dojops/scanner | 110 |
@dojops/executor | 67 |
@dojops/planner | 39 |
@dojops/session | 38 |
@dojops/sdk | 24 |
| Total | 1931 |
Writing Tests
- Place test files in
__tests__/directories mirroring the source structure:src/foo.ts->src/__tests__/foo.test.ts - Mock LLM providers for deterministic tests
- Use
supertestfor API endpoint integration tests - Test both success and error paths
Adding a New Tool
All tools follow the BaseTool<T> pattern. See DevOps Tools for the full pattern.
Step-by-Step
-
Create directory:
packages/tools/src/my-tool/ -
Define schemas (
schemas.ts):import { z } from "@dojops/sdk"; export const MyToolInputSchema = z.object({ name: z.string(), // tool-specific fields existingContent: z .string() .optional() .describe( "Existing config file content to update/enhance. If omitted, tool auto-detects existing files.", ), }); export const MyToolOutputSchema = z.object({ // LLM response structure }); export type MyToolInput = z.infer<typeof MyToolInputSchema>; export type MyToolOutput = z.infer<typeof MyToolOutputSchema>; -
Implement generator (
generator.ts):import { parseAndValidate } from "@dojops/core"; export async function generateMyTool( input: MyToolInput, provider: LLMProvider, existingContent?: string, ) { const isUpdate = !!existingContent; const system = isUpdate ? "Update the existing config. Preserve existing structure and settings." : "Generate a new config from scratch."; const prompt = isUpdate ? `${buildPrompt(input)}\n\n--- EXISTING CONFIGURATION ---\n${existingContent}\n--- END ---` : buildPrompt(input); const response = await provider.generate({ system, prompt, schema: MyToolOutputSchema }); return parseAndValidate(response.content, MyToolOutputSchema); } -
Create tool class (
my-tool.ts):import { BaseTool, readExistingConfig, backupFile, atomicWriteFileSync } from "@dojops/sdk"; export class MyTool extends BaseTool<MyToolInput> { name = "my-tool"; inputSchema = MyToolInputSchema; async generate(input: MyToolInput) { const existingContent = input.existingContent ?? readExistingConfig(outputPath); const isUpdate = !!existingContent; const result = await generateMyTool(input, this.provider, existingContent); return { success: true, data: { ...result, isUpdate } }; } async execute(input: MyToolInput) { const result = await this.generate(input); if (result.data.isUpdate) backupFile(outputPath); atomicWriteFileSync(outputPath, result.data.content); return { ...result, filesWritten: [outputPath], filesModified: result.data.isUpdate ? [outputPath] : [], }; } } -
Optional: Add detector (
detector.ts) for filesystem context detection -
Optional: Add verifier (
verifier.ts) for external tool validation -
Export: Add to
packages/tools/src/index.ts -
Write tests:
my-tool.test.tswith mocked LLM provider — include tests for auto-detection of existing files, update mode prompts, and.bakbackup creation
Creating a Custom Tool
For tools that don’t need to be built into the core, use the custom tool system instead. Custom tools are declarative — no TypeScript code required.
Step-by-Step
-
Scaffold a custom tool:
dojops tools init my-toolThis creates
.dojops/tools/my-tool/with templatetool.yamlandinput.schema.json. -
Edit
tool.yaml— set the name, description, system prompt, output files, and serializer. -
Edit
input.schema.json— define the JSON Schema for your tool’s input parameters. -
Validate the tool:
dojops tools validate .dojops/tools/my-tool/ -
Test by generating a config:
dojops "Generate my-tool config for production"
Custom tools are automatically discovered and available to the Planner, Executor, and API. See DevOps Tools — Custom Tool System for the full manifest format.
Adding a New Agent
There are two ways to add agents: as a custom agent (no source code changes) or as a built-in agent (requires modifying the codebase).
Option 1: Custom Agent (Recommended for Most Cases)
Create a custom agent without modifying DojOps source code:
# LLM-generated (recommended)
dojops agents create "an SRE specialist for incident response and reliability"
# Manual creation via interactive prompts
dojops agents create --manualCustom agents are stored as structured README.md files in .dojops/agents/<name>/ (project) or ~/.dojops/agents/<name>/ (global). They participate in the same keyword-based routing as built-in agents and can override built-in agents by name.
See Specialist Agents — Custom Agents for the full README.md format and discovery rules.
Option 2: Built-in Agent (For Core Contributions)
Built-in agents are defined in packages/core/src/agents/specialists.ts.
-
Add a new entry to the specialists array:
{ name: "my-specialist", domain: "my-domain", description: "Expert in...", keywords: ["keyword1", "keyword2", "keyword3"], toolDependencies: ["optional-tool"], } -
The agent will automatically:
- Be registered in the
AgentRouter - Appear in
dojops agents list - Be available in the API (
GET /api/agents) - Be routable by keyword matching
- Be registered in the
-
Write tests for keyword routing accuracy.
PR Workflow
- Fork the repository
- Branch from
main:git checkout -b feature/my-feature - Implement your changes with tests
- Verify:
pnpm build pnpm test pnpm lint pnpm format:check - Commit with a descriptive message
- Submit a pull request against
main
PR Checklist
- All tests pass (
pnpm test) - Linting passes (
pnpm lint) - Formatting is correct (
pnpm format:check) - New features include tests
- New tools follow the
BaseTool<T>pattern - Breaking changes are documented
License
DojOps is licensed under the MIT License .