This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
pnpm build # bundle with tsup → dist/index.js
pnpm dev # tsx watch (no rebuild between changes)
pnpm dev -- start MORG-42 # run a specific command in watch mode
pnpm typecheck # tsc --noEmit
pnpm lint # eslint src
pnpm lint --fix # eslint src --fix (auto-fix formatting and lint errors)
pnpm test # vitest run (all tests)
pnpm test:watch # vitest (interactive)Run a single test file:
pnpm test tests/ticket.test.tsAfter changing src/, rebuild and test the linked binary:
pnpm build && morg <command>Package manager is pnpm. Never use npm or yarn.
ESM-native ("type": "module"). "moduleResolution": "Bundler" — no .js extensions on imports, no await import() inside functions. All imports are static and at the top of each file.
src/utils/errors.ts — error classes only (MorgError, ConfigError, IntegrationError, GitError)
src/config/paths.ts — ~/.morg path constants only
src/config/schemas.ts — all Zod schemas + inferred TS types
src/config/manager.ts — ConfigManager singleton: reads/writes JSON state, nothing else
src/git/index.ts — pure git primitives via execa, no business logic
src/utils/detect.ts — requireConfig(), requireTrackedRepo(), detectTools()
src/ui/ — rendering only (theme, prompts, spinner, output panel)
src/integrations/providers/<domain>/ — provider interfaces (tickets-provider.ts, ai-provider.ts, etc.)
src/integrations/providers/<domain>/implementations/ — concrete clients (JiraClient, GhClient, ClaudeClient, SlackClient)
src/services/registry.ts — ServiceRegistry singleton: only place clients are instantiated
src/commands/ — orchestration: imports from all layers, owns all business logic
src/index.ts — Commander wiring + preAction hook
The key constraint: config/manager.ts and git/index.ts are leaf nodes — they import nothing from the rest of src/. Circular dependencies are prevented by keeping all orchestration in src/commands/.
All integration clients are instantiated via the registry singleton in src/services/registry.ts.
Commands MUST NOT import integration clients (JiraClient, GhClient, ClaudeClient, SlackClient) directly.
Instead:
import { registry } from '../services/registry';
const gh = await registry.gh(); // GhClient — always present
const tickets = await registry.tickets(); // TicketsProvider | null — check before use
const ai = await registry.ai(); // AIProvider | null — check before use
const messaging = await registry.messaging(); // MessagingProvider | nullprojectId is resolved internally by the registry via requireTrackedRepo(). Commands still call
requireTrackedRepo() directly when they need projectId for configManager.getBranches() etc.
- Create
src/integrations/providers/<domain>/implementations/<name>-<domain>-provider.tsand implementTicketsProvider,AIProvider, orMessagingProvider - Add one async method to the
Registryclass insrc/services/registry.ts - Add config schemas in
src/config/schemas.ts - Write tests in
tests/integrations/<name>.test.ts - Update this file
listTicketsalways includesassignee = currentUser()— never returns all project ticketsgetTicketfetches epic children via a secondary JQLparent = <key>query, becausefields.subtasksonly contains Sub-task type issues (not regular Features/Stories under an Epic)- Issue link direction: uses
type.inwardforinwardIssueandtype.outwardforoutwardIssue(e.g. "is blocked by" vs "blocks"), nevertype.namewhich has no direction - ADF descriptions are rendered to Markdown via
adfToMarkdown()with link preservation
- Unit tests: pure functions, mock all I/O
- Service tests (
tests/services/): mockconfigManagerandrequireTrackedRepowithvi.mock() - Provider tests (
tests/utils/): mockregistryand UI helpers - Integration tests (
tests/integrations/): stubfetchorexecato test client parsing/errors - Run all:
pnpm test| Single file:pnpm test tests/integrations/jira.test.ts
config.json— global config (API keys, integration tokens)projects.json— registry of morg-initialized reposprojects/<id>/config.json— per-project config (GitHub repo, Jira project key)projects/<id>/branches.json— branch tracking (branch → ticket, PR status)
All JSON state is read/written exclusively through configManager (never direct fs calls in commands). All external data is parsed through Zod schemas at the boundary.
- Create
src/commands/<name>.tswith aregister<Name>Command(program: Command)export - Implement the logic in a private
async function run<Name>()in the same file - Add static import +
register<Name>Command(program)call insrc/index.ts - Always keep these in sync whenever adding, removing, or renaming commands:
README.md— Commands section.claude/skills/morg/skill.md— relevant section(s)src/commands/shell-init.ts—COMMANDSarray (tab completion)
Commands that require a tracked repo call requireTrackedRepo() to get the projectId, then use configManager.getBranches(projectId) etc.
Worktrees live at ../morg-worktrees/<branch>/ relative to the main repo. Run pnpm install in each worktree to set up dependencies — pnpm's content-addressable store means installs are fast (dependencies are symlinked from the store, not re-downloaded).
Always { reject: false } — check result.exitCode instead of catching exceptions.
All clients are instantiated exclusively via registry in src/services/registry.ts. Commands never call new JiraClient(...) or new ClaudeClient(...) directly.
All commands except config, install-claude-skill, and completion require a valid ~/.morg/config.json. The hook in src/index.ts calls requireConfig() before every action not in NO_CONFIG_COMMANDS.
MorgError→ exit 1 (generic)IntegrationError→ exit 3GitError→ exit 4ConfigError→ exit 5