Contributing to devorch and understanding its architecture.
- Bun (required, not npm/yarn/pnpm)
- GitHub CLI (
gh)
Install Bun:
curl -fsSL https://bun.sh/install | bash# Clone and setup
git clone https://github.com/guicheffer/devorch.git
cd devorch
bun install
# Run in dev mode
bun run dev
# Run tests
bun test┌─────────────────────────────────────────────────────────────┐
│ devorch │
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ CLI (dist) │ → │ Templates │ │
│ │ (src/cli/) │ │ (templates/) │ │
│ └──────────────┘ └──────────────┘ │
│ ↓ ↓ │
│ Install time Compile │
│ compilation templates with │
│ frontmatter filters │
│ ↓ │
│ ┌──────────────┐ │
│ │ User Project │ │
│ │ .claude/ │ │
│ │ .claude/ │ │
│ └──────────────┘ │
└──────────────────────────────────────────────────────────────┘
devorch/
├── src/
│ ├── cli/ # CLI application (distributed)
│ │ ├── commands/ # CLI command implementations
│ │ ├── lib/ # Core libraries
│ │ │ ├── discovery/ # Custom asset discovery
│ │ │ └── ...
│ │ └── types/ # TypeScript types
│ ├── schemas/ # Zod schema definitions
│ └── utils/ # Shared utilities
├── templates/ # Markdown files (commands, subagents, skills)
│ ├── commands/
│ ├── subagents/
│ ├── skills/ # Skills with optional .updater/ directories
│ └── shared/
├── .github/
│ ├── workflows/ # CI/CD automation
│ │ ├── auto-update-skills.yml # Daily skill auto-updater
│ │ ├── ci.yml # CI checks
│ │ └── release.yml # Release automation
│ └── scripts/ # CI/CD scripts
│ └── generate-update-matrix.ts # Skill update discovery
└── docs/ # Documentation
The root .claude/ folder contains development files for devorch itself:
.claude/
└── settings.local.json # Local Claude Code settings (user-created)
Files explained:
settings.local.json - User's local Claude Code settings
- NOT created by devorch (user-created)
- Contains AWS credentials, permissions, environment variables
- Git-ignored for security
- Used by developers for local Claude Code configuration
Note for contributors:
- These files are for devorch's own development/testing
- User projects will have their own
.claude/folders - Don't commit
settings.local.json(contains credentials)
devorch automatically creates .gitignore files during installation:
# Auto-created by installer
.claude/agents/devorch/.gitignore # Ignores all generated subagents
.claude/commands/devorch/.gitignore # Ignores all generated commands
.claude/skills/.gitignore # Ignores enabled bundled skills only
devorch/.gitignore # Ignores .state/ directory
What gets gitignored:
- ✅ Generated subagents and commands (always gitignored)
- ✅ Enabled bundled skills (gitignored, can be regenerated)
- ✅ Installation state (gitignored)
- ❌ Custom skills (not gitignored - user-created content)
- ❌ Config file (not gitignored - team settings)
Why automatic? These are generated files that can be recreated with devorch install. After cloning, teammates just run devorch install to regenerate everything.
Implementation: See src/cli/lib/installation/installer.ts:160-195
src/cli/
├── index.ts # Entry point
├── commands/
│ ├── setup/
│ │ └── install/ # Install command
│ ├── manage/
│ │ ├── update/ # Update installation
│ │ ├── check-version/ # Version checking
│ │ └── contribute/ # Contribute assets
│ └── diagnose/ # Diagnostics
└── lib/
├── config/ # Config loading and parsing
├── source/ # GitHub/local source loading
├── compiler/ # Template compilation
├── installer/ # Installation logic
├── markdown-di/ # Dependency resolution & injection
└── filesystem/ # File operations
Location: .github/workflows/
The repository uses GitHub Actions for automation:
Runs on every push and pull request:
jobs:
test: # Run test suite
typecheck: # TypeScript validation
lint: # Biome linting
build: # Build CLITriggers:
- Push to any branch
- Pull request opened/updated
- Manual workflow dispatch
What it does:
- Installs dependencies with Bun
- Runs all tests (
bun test) - Checks TypeScript types (
bun run typecheck) - Lints code (
bun run lint) - Builds CLI (
bun run build)
Automatically updates skills with .updater/ configurations:
Schedule: Daily at 2am UTC
Stages:
- Discover Skills - Scans for skills with
.updater/prompt.md - Generate Matrix - Creates update matrix using
.github/scripts/generate-update-matrix.ts - Update Skills - Uses Claude Code Action to update skill content
- Create PR - Aggregates changes into review PR
See: Workflow at .github/workflows/auto-update-skills.yml and deprecated docs at auto-updater.md
Manual trigger:
gh workflow run "Auto-Update Skills"Handles version releases and publishing:
Triggers:
- Manual workflow dispatch with version number
- Git tags matching
v*.*.*
What it does:
- Bumps version in package.json
- Generates changelog
- Creates GitHub release
- Publishes artifacts
Meta-workflow that must pass for PR merge:
Purpose:
- Ensures all CI checks pass
- Prevents broken code from merging
- Enforces quality gates
Checks:
- All tests pass
- Type checking succeeds
- Linting passes
- Build succeeds
View workflow runs:
gh run list
gh run view <run-id>Re-run failed workflow:
gh run rerun <run-id>Debug workflow locally: Use act to run GitHub Actions locally:
act -j test # Run test job locallyWorkflow permissions:
contents: write- Commit changespull-requests: write- Create PRsissues: write- Create issuesid-token: write- AWS authentication
# Development mode
bun run dev
# Build production
bun run build
# Compile standalone binary
bun run compile
# Type checking
bun run typecheck
# Lint and fix
bun run lint:fix
# Format code
bun run formatTest suite: 182+ tests with excellent coverage
# All tests (unit + integration)
bun test
# Watch mode
bun test:watch
# Specific test suites
bun test:unit # Unit tests only
bun test:integration # Integration tests only
# Coverage
bun test --coverage
# Run specific file
bun test src/cli/lib/markdown-di/resolver.test.ts
# Run with filter
bun test -t "dependency resolution"Unit tests - Colocated with source files:
src/cli/lib/markdown-di/
├── resolver.ts
├── resolver.test.ts ← Unit tests here
├── processor.ts
└── processor.test.ts ← Unit tests here
Integration tests - Separate directory:
tests/integration/
├── install.test.ts # Full install workflow
├── compilation.test.ts # Template compilation
└── config.test.ts # Config loading
Unit test example:
import { describe, test, expect } from 'bun:test';
import { DependencyResolver } from '@/lib/markdown-di/resolver';
describe('DependencyResolver', () => {
test('resolves simple dependency', () => {
const resolver = new DependencyResolver();
const result = resolver.resolve(frontmatter, context);
expect(result.dependencies).toHaveLength(1);
expect(result.dependencies[0]).toBe('my-subagent');
});
test('detects circular dependencies', () => {
const resolver = new DependencyResolver();
expect(() => {
resolver.resolve(circularFrontmatter, context);
}).toThrow('Circular dependency detected');
});
});Integration test example:
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
import { mkdtemp, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
describe('devorch install', () => {
let testDir: string;
beforeEach(async () => {
testDir = await mkdtemp(join(tmpdir(), 'devorch-test-'));
});
afterEach(async () => {
await rm(testDir, { recursive: true, force: true });
});
test('installs command successfully', async () => {
// Run install
await runInstall(testDir);
// Verify files created
const commandPath = join(testDir, '.claude/commands/my-command.md');
expect(await exists(commandPath)).toBe(true);
});
});Location: src/cli/lib/filesystem/__snapshots__/
Snapshot tests verify compiled output doesn't change unexpectedly:
import { test, expect } from 'bun:test';
test('compiles template correctly', async () => {
const result = await compileTemplate(templateContent);
// Snapshot the compiled output
expect(result.body).toMatchSnapshot();
});Updating snapshots:
bun test --update-snapshots
# or
bun test -uWhen to update:
- Intentional changes to template compilation
- Frontmatter structure changes
- Output format improvements
When NOT to update:
- Random test failures
- Unexpected changes (investigate first!)
Naming:
- Test files:
*.test.ts - Unit tests: Colocated with source
- Integration tests:
tests/integration/
Structure:
describe('ComponentName', () => {
// Setup
beforeEach(() => { /* ... */ });
afterEach(() => { /* ... */ });
// Happy path first
test('does the thing successfully', () => { /* ... */ });
// Edge cases
test('handles empty input', () => { /* ... */ });
test('handles invalid input', () => { /* ... */ });
// Error cases
test('throws on null input', () => { /* ... */ });
});Assertions:
- Use descriptive expectations
- Test both success and failure paths
- Include edge cases
Good:
test('resolves nested dependencies', () => {
const result = resolver.resolve(nestedDeps);
expect(result.dependencies).toHaveLength(3);
expect(result.dependencies).toContain('parent');
expect(result.dependencies).toContain('child');
expect(result.dependencies).toContain('grandchild');
});Bad:
test('works', () => {
const result = resolver.resolve(nestedDeps);
expect(result).toBeTruthy(); // Too vague!
});File system mocking:
import { mock } from 'bun:test';
import * as fs from 'node:fs/promises';
test('handles missing file', async () => {
// Mock fs.readFile to throw
mock.module('node:fs/promises', () => ({
readFile: async () => {
throw new Error('ENOENT: File not found');
}
}));
await expect(loadTemplate('missing.md')).rejects.toThrow('File not found');
});Logger mocking:
import { logger } from '@/utils/logger';
test('logs error on failure', async () => {
const logSpy = mock(logger.error);
await failingOperation();
expect(logSpy).toHaveBeenCalledWith(
'Operation failed',
expect.objectContaining({ error: expect.any(Error) })
);
});DO:
- ✅ Test public APIs, not internal implementation
- ✅ Use descriptive test names
- ✅ Test error paths
- ✅ Clean up test resources (temp files, etc.)
- ✅ Keep tests fast (< 100ms per test)
- ✅ Use
beforeEach/afterEachfor setup/cleanup - ✅ Test edge cases (empty arrays, null values, etc.)
DON'T:
- ❌ Test implementation details
- ❌ Write tests that depend on other tests
- ❌ Leave console.log in tests
- ❌ Skip tests without a good reason
- ❌ Mock everything (test real code paths when possible)
- ❌ Write flaky tests (random failures)
Tests run automatically in GitHub Actions:
# .github/workflows/ci.yml
- name: Run tests
run: bun test
- name: Check coverage
run: bun test --coverageCoverage thresholds:
- Lines: 80%+
- Functions: 80%+
- Branches: 75%+
Run with debugging:
# Show console output
bun test --verbose
# Run single test
bun test -t "specific test name"
# Debug with breakpoints (VSCode)
# Use "JavaScript Debug Terminal" and run:
bun testCommon issues:
Test timeout:
test('slow operation', async () => {
// Increase timeout for slow tests
await slowOperation();
}, { timeout: 10000 }); // 10 secondsFlaky tests:
// Bad: Depends on timing
test('processes async', async () => {
trigger();
await wait(100); // ❌ Arbitrary delay
expect(result).toBe(true);
});
// Good: Wait for actual condition
test('processes async', async () => {
trigger();
await waitUntil(() => result === true); // ✅ Wait for condition
expect(result).toBe(true);
});Checklist for new features:
- Add unit tests for new functions/classes
- Add integration test if feature spans multiple components
- Test happy path
- Test error paths
- Test edge cases (null, empty, large inputs)
- Update snapshots if output changes
- Verify tests pass in CI
Example: Adding test for new feature
# 1. Create test file colocated with source
touch src/cli/lib/my-feature/my-feature.test.ts
# 2. Write tests
# (see examples above)
# 3. Run tests
bun test src/cli/lib/my-feature/my-feature.test.ts
# 4. Run all tests to check for regressions
bun testFrom any directory:
cd /path/to/your-test-project
devorch install --local=/path/to/devorchFrom a test project within devorch repo:
cd devorch/test-project
bun ../src/cli/index.ts install --local=../Benefits:
- Runs CLI directly with Bun
- No build step needed
- Instant feedback
- Perfect for development
Workflow:
- Edit templates in
templates/or code insrc/cli/ - Run
bun ../src/cli/index.ts install --local=../ - Test changes
- Iterate
1. Create command directory:
mkdir -p src/cli/commands/my-category/my-command2. Create command file:
// src/cli/commands/my-category/my-command/index.ts
import { logger } from '@/utils/logger.js';
export async function myCommand() {
logger.info('Running my command');
// Implementation
}3. Register command:
Update CLI router to include new command.
4. Test:
bun run dev my-command1. Create template:
mkdir -p templates/commands/my-command/single-agent
vim templates/commands/my-command/single-agent/my-command.md2. Add frontmatter:
---
name: my-command
description: What it does
mode: single-agent
---
# My Command
...3. Test installation:
cd test-project
bun ../src/cli/index.ts install --local=../Dependency resolution: src/cli/lib/markdown-di/resolver.ts
Key classes:
DependencyResolver- Resolves and validates dependenciesCircularDependencyDetector- Prevents circular dependencies
Config integration: src/cli/lib/config/config-writer.ts
mergeDependenciesIntoConfigArrays()- Adds dependencies to config
Error definitions: src/utils/errors.ts, src/utils/error-codes.ts
Example:
// src/utils/errors.ts
export class MyError extends BaseError {
static myCustomError(detail: string): MyError {
return new MyError(
ErrorCode.MY_CUSTOM_ERROR,
`My error: ${detail}`,
{ detail }
);
}
}
// src/utils/error-codes.ts
export enum ErrorCode {
MY_CUSTOM_ERROR = 'MY_CUSTOM_ERROR',
}.github/workflows/:
test.yml- Run tests on PRbuild.yml- Build CLI on pushrelease.yml- Create releasesupdate-assets-cron.yml- Scheduled skill updates
1. Compile binaries:
bun run compileOutputs: devorch binary
2. Create release:
- Tag version:
git tag v2.1.0 - Push tag:
git push origin v2.1.0 - GitHub Actions builds for all platforms
- Attaches binaries to release
3. Package templates:
tar -czf config.tar.gz templates/Attached to release with binaries.
# ✅ Correct
bun install
bun run dev
bun test
# ❌ Never
npm install
yarn/pnpm// ✅ Correct
import { UserError, SystemError } from '@/utils/errors.js';
throw UserError.configNotFound(paths);
// ❌ Never
throw new Error('Config not found');// ✅ Correct
import { logger } from '@/utils/logger.js';
logger.info('Loading config', { path });
// ❌ Never
console.log('Loading...');// ✅ Correct
import { INSTALL_PATHS } from '@/cli/lib/filesystem/paths.js';
const dir = join(projectDir, INSTALL_PATHS.claudeAgents());
// ❌ Never
const dir = join(projectDir, '.claude/agents/devorch');Husky + lint-staged runs automatically:
- TypeScript type checking
- Biome linting with auto-fix
- Biome formatting
Manual run:
bun run format # Format code with Biome
bun run lint:fix # Lint and fix with Biome
bun run lint # Check only (no fixes)catalog: protocol not supported:
- Using npm instead of Bun
- Solution:
bun install
GitHub CLI not found:
- Install: https://cli.github.com/
- Authenticate:
gh auth login
Tests failing:
- Run:
bun test --watch - Check test output for details
Type errors:
- Run:
bun run typecheck - Fix TypeScript errors
- Main README
- Extending Guide - Creating custom assets
- Skills Guide - Creating and structuring skills
- Versioning System - Semver, version bumping, and releases
- Auto-Updater - Historical auto-updater documentation (deprecated)
- CLAUDE.md - Guide for AI assistants