For a complete and accurate architecture reference, read
docs/README.mdfirst. The sections below are a quick-start guide;docs/is the authoritative source.
A CLI tool that coordinates git worktrees, tmux sessions, AI agents (Claude/Gemini), and GitHub PR state across one or more projects. It is more than a worktree manager — it is a workspace coordinator for multi-repo AI-assisted development.
- Runtime: Node.js 18+ (ESM modules)
- Framework: Ink (React for CLI)
- Language: TypeScript with strict mode
- Testing: Jest with ts-jest
- Build: tsc compiler
Git worktrees allow multiple branches to be checked out simultaneously in different directories. This app manages worktrees in a structured way:
- Main projects:
{projects-directory}/{project-name}/ - Feature branches:
{projects-directory}/{project-name}-branches/{feature-name}/ - Archived features:
{projects-directory}/{project-name}-archived/archived-{timestamp}_{feature-name}/
The projects directory is configurable:
- CLI Argument:
devteam --dir /path/to/projects - Environment Variable:
PROJECTS_DIR=/path/to/projects devteam - Default: Current working directory
Each worktree gets associated tmux sessions:
- Main session:
dev-{project}-{feature}(for Claude AI) - Shell session:
dev-{project}-{feature}-shell(for terminal work) - Run session:
dev-{project}-{feature}-run(for executing commands)
The app supports multiple AI CLIs (Claude, Gemini) and monitors AI status in tmux panes:
- Working: Shows "esc to interrupt"
- Waiting: Shows numbered prompt (e.g., "1. ")
- Idle: Shows standard prompt
- Thinking: Shows thinking indicator
Tool preference is stored per-project in .devteam/config.json.
See docs/reference/code-map.md for the full file map. Key layout:
src/
├── App.tsx # Root component; provider nesting
├── bootstrap.tsx # Ink render entry
├── bin/devteam.ts # CLI executable
├── cores/ # Core engine classes (business logic, no React)
│ ├── WorktreeCore.ts # Worktree list, sessions, git status
│ └── GitHubCore.ts # PR status, cache, GitHub operations
├── engine/core-types.ts # CoreBase<T> interface
├── contexts/ # React wrappers around Core engines
│ ├── WorktreeContext.tsx
│ ├── GitHubContext.tsx
│ ├── UIContext.tsx # Navigation state machine (no Core behind it)
│ └── InputFocusContext.tsx
├── screens/ # Three full-screen components
│ ├── WorktreeListScreen.tsx
│ ├── CreateFeatureScreen.tsx
│ └── ArchiveConfirmScreen.tsx
├── services/ # Stateless external I/O (git, tmux, gh, disk)
├── components/ # UI components (dialogs, views, common)
├── hooks/ # useKeyboardShortcuts and others
├── models.ts # WorktreeInfo, PRStatus, GitStatus, SessionInfo
└── constants.ts # AI_TOOLS, refresh intervals
tests/
├── fakes/ # In-memory service implementations
│ └── stores.ts # Shared memory data stores
├── utils/renderApp.tsx # Test app renderer
├── unit/ # Unit tests
└── e2e/ # E2E tests (mock-rendered + terminal/)
-
Import Style: Use ESM imports with
.jsextension (even for TS files)import {GitService} from '../services/GitService.js';
-
JSX Syntax: Use modern JSX syntax for React components
return ( <Box flexDirection="column"> <Text>Hello</Text> </Box> );
-
File Extensions:
.tsfor non-React files (services, utils, models).tsxfor React components and contexts
-
Class Models: Use classes with constructor initialization
export class WorktreeInfo { project: string; feature: string; constructor(init: Partial<WorktreeInfo> = {}) { this.project = ''; this.feature = ''; Object.assign(this, init); } }
-
Service Pattern: Services are classes with dependency injection
export class WorktreeService { constructor( private gitService?: GitService, private tmuxService?: TmuxService ) { this.gitService = gitService || new GitService(); this.tmuxService = tmuxService || new TmuxService(); } }
-
Context Providers: Subscribe to a Core engine; re-render on state change
export function WorktreeProvider({children}) { const [state, setState] = useState(() => core.getState()); useEffect(() => core.subscribe(setState), []); const value = { ...state, createFeature: core.createFeature.bind(core) }; return ( <WorktreeContext.Provider value={value}> {children} </WorktreeContext.Provider> ); }
- Files: camelCase for
.ts, PascalCase for.tsx - Components: PascalCase (e.g.,
WorktreeListScreen) - Hooks:
useprefix (e.g.,useKeyboardShortcuts) - Services: PascalCase with
Servicesuffix - Constants: UPPER_SNAKE_CASE
- Interfaces: PascalCase, often with
InfoorStatesuffix
- Default to no comments — well-named identifiers and types should carry the load.
- When a comment is genuinely warranted (a non-obvious WHY, a hidden constraint, a workaround), prefer a single line. Multi-line comment blocks and multi-paragraph JSDoc are not banned, but should be rare.
- Don't explain WHAT the code does or restate parameter names; don't reference current task / fix / caller (those belong in PR descriptions and rot in the codebase).
-
Service Layer (Stateless External I/O):
- GitService: Local git operations (worktrees, branches, status, diff)
- GitHubService: GitHub API operations (PRs, checks, issues)
- TmuxService: Tmux session management
- WorkspaceService: Workspace layout and disk operations
- PRStatusCacheService: Disk cache for PR data
- AIToolService: AI tool detection and preference
- Services are stateless and only fetch/transform data; no mutable state
-
Core Engine Layer (Business Logic + State):
- WorktreeCore: Owns worktree list, git status, session state; runs refresh loops
- GitHubCore: Owns PR status; manages cache, throttled batch fetching
- Cores implement
CoreBase<T>withsubscribe(fn)for React observation - No React dependency; fully testable as plain TypeScript
-
Context Layer (React Wrappers):
- WorktreeContext: Subscribes to WorktreeCore; exposes state + operations as hooks
- GitHubContext: Subscribes to GitHubCore; exposes PR operations
- UIContext: Pure React state machine for navigation (no Core behind it)
- InputFocusContext: Tracks which component owns keyboard focus
-
Component Layer: Thin components that use contexts; no business logic
- Minimal Mocking: Only mock external dependencies (git, tmux, gh)
- Real Components: Run actual UI components in tests
- In-Memory Database: Fake services use memory stores
- UI-Driven Testing: Test through user interactions
-
Unit Tests (
tests/unit/): Test services and state logic in isolationtest('should create worktree', () => { const gitService = new FakeGitService(); const result = gitService.createWorktree('project', 'feature'); expect(result).toBe(true); });
-
E2E Tests (
tests/e2e/): Full user workflows and cross-service interactions
test('complete feature workflow', async () => {
const {result, stdin} = renderApp();
stdin.write('n'); // Create new
await delay(100);
stdin.write('\r'); // Select project
// ... continue workflow
});-
tests/e2e/ (mock-rendered):
- Uses
tests/utils/renderApp.tsx(mock output driver) for deterministic frames. - Runs real app logic with in-memory fakes; avoids raw-mode/alt-screen quirks.
- Fast and stable; preferred for most end-to-end flows.
- Uses
-
tests/e2e/terminal/ (terminal-oriented, Node runner):
- Uses Node scripts with Ink to verify real terminal rendering, avoiding Jest’s TTY/raw‑mode quirks.
- Renders real Ink components and providers with fakes and asserts on terminal frames.
- Command:
npm run test:terminal— builds the project, compiles fakes, and runs terminal checks:tests/e2e/terminal/run-smoke.mjs: Ink smoketests/e2e/terminal/run-mainview-list.mjs: MainView rows rendertests/e2e/terminal/run-app-full.mjs: Full App providers render and list rows appear
- Note: These scripts import from
dist/anddist-tests; the script runs both builds. - Jest-based terminal tests were removed in favor of the Node runner; Jest E2E tests remain for app logic and flows.
npm test # Run all Jest tests (unit + E2E)
npm run test:watch # Jest watch mode
npm run typecheck # Type checking only
npm run test:terminal # Run terminal rendering tests (Node runner)Create in src/components/dialogs/:
import React from 'react';
import {Box, Text} from 'ink';
interface MyDialogProps {
title: string;
onClose: () => void;
}
export default function MyDialog({title, onClose}: MyDialogProps) {
return (
<Box flexDirection="column">
<Text>{title}</Text>
{/* Dialog content */}
</Box>
);
}Create in src/services/:
export class MyService {
fetchData(params: any): Promise<DataType[]> {
// Fetch and transform data only - no state
return runCommand(['some-command', params]);
}
transformData(raw: any): DataType {
// Pure transformation functions
return new DataType(raw);
}
}Create in src/contexts/:
export function MyContextProvider({children}) {
const [state, setState] = useState(() => myCore.getState());
useEffect(() => myCore.subscribe(setState), []);
const value = { ...state, doThing: myCore.doThing.bind(myCore) };
return <MyContext.Provider value={value}>{children}</MyContext.Provider>;
}Create in src/screens/:
import React from 'react';
import {Box} from 'ink';
import {useMyContext} from '../contexts/MyContext.js';
import {useUIContext} from '../contexts/UIContext.js';
export default function MyScreen() {
const {data, loading, createItem} = useMyContext();
const {showList} = useUIContext();
return (
<Box>
{/* Screen content that uses context state and operations */}
</Box>
);
}For multi-step operations, use src/ops.ts:
export async function complexOperation(
services: Services,
params: OperationParams
): Promise<Result> {
// Step 1: Validate
// Step 2: Execute
// Step 3: Update state
return result;
}The app copies these files to worktrees:
.env.local- Environment variables.claude/settings.local.json- Claude settingsCLAUDE.md- Claude documentation
-
Refresh Rates: Different refresh intervals for different data:
- AI Status: 2s
- Git Status: 5s
- PR Status: 30s
- Full Refresh: 30s
-
Pagination: List views paginate at 20 items by default
-
Memory Management: Fake services clear old data periodically
try {
const result = runCommand(['git', 'status']);
return result;
} catch (error) {
// Silent fail for UI operations
return null;
}const loadData = async () => {
const data = await fetchPRStatus();
setState(prev => ({...prev, prStatus: data}));
};useKeyboardShortcuts({
onMove: (delta) => moveSelection(delta),
onSelect: () => handleSelect(),
onCreate: () => setMode('create')
});The app includes comprehensive file-based logging for all console output and errors:
./logs/
├── errors.log # Error messages and stack traces
└── console.log # All console output (log, warn, info, debug)
-
Automatic Console Logging: All
console.log,console.error,console.warn,console.info, andconsole.debugcalls are automatically logged to files wheninitializeFileLogging()is called. -
Manual Logging Functions: Use these functions for structured logging:
import {logError, logInfo, logWarn, logDebug} from '../shared/utils/logger.js'; logError('Database connection failed', error); logInfo('User created successfully', {userId: 123}); logWarn('API rate limit approaching', {remaining: 10}); logDebug('Cache hit', {key: 'user:123'});
-
Log Management:
import {getLogPaths, clearLogs} from '../shared/utils/logger.js'; // Get log file paths const {errorLog, consoleLog} = getLogPaths(); // Clear all logs clearLogs();
Each log entry includes:
- ISO timestamp
- Log level (ERROR, LOG, WARN, INFO, DEBUG)
- Message
- Data object (JSON formatted if provided)
Example:
[2025-08-27T10:30:45.123Z] ERROR: Database connection failed {"host":"localhost","port":5432}
[2025-08-27T10:30:46.456Z] INFO: User login successful {"userId":123,"email":"user@example.com"}
- Logs automatically rotate when they exceed 10MB
- Old logs are renamed with timestamp suffix:
errors.log.1724765445123 - Silent failure ensures logging issues never crash the app
- File Logs: Check
./logs/for detailed error traces and debug info - Console Output: Use
console.error()(stdout is used by Ink) - Test Mode: Run with fake services for testing
- Tmux Inspection: Check sessions with
tmux ls - Log Analysis: Use
tail -f ./logs/errors.logto monitor errors in real-time
-
Error Logging: Always log errors with context:
try { await createWorktree(project, feature); } catch (error) { logError('Failed to create worktree', {project, feature, error}); throw error; }
-
Debug Information: Log debug info for complex operations:
logDebug('Starting worktree creation', {project, feature, targetPath});
-
Performance Monitoring: Log timing for slow operations:
const start = Date.now(); await longOperation(); logInfo('Operation completed', {duration: Date.now() - start});
npm run build # Compile TypeScript
npm run typecheck # Check types only
npm link # Install globally as 'devteam'- Services are stateless - only fetch and transform data
- Core engines own state - business logic and refresh loops live in
src/cores/ - Contexts are thin wrappers - subscribe to a Core, expose state + operations via hooks
- Clear separation - GitService (local git) vs GitHubService (GitHub API)
- Always use absolute paths in file operations
- Check for existence before file operations
- Silent fail for UI operations to prevent crashes
- Use memory stores in tests for isolation
- Follow existing patterns when adding features
- Test through UI interactions not implementation details
- Use TypeScript strict mode for safety
When adding features:
- Add unit tests for new services
- Add E2E tests for user workflows and cross-service interactions
- Update fake implementations
- Verify TypeScript types compile
- Test error cases and edge conditions
- Show the user a well-researched, high-level plan to implement the feature that uses above guidelines, best design practices and values simplicity in the implementation.
- Implement, and ensure that the feature logic is tested using the above guidelines
- Build and test and do a typecheck
- Make a PR using the current branch