# Git, Process, and Logging Modules Three infrastructure modules supporting agent execution. ## Git Module (`apps/server/git/`) Manages git worktrees for isolated agent workspaces. ### Architecture - **Port**: `WorktreeManager` interface - **Adapter**: `SimpleGitWorktreeManager` using simple-git library ### WorktreeManager Methods | Method | Purpose | |--------|---------| | `create(id, branch, baseBranch?)` | Create worktree with new branch (default base: 'main') | | `remove(id)` | Clean up worktree directory | | `list()` | All worktrees including main | | `get(id)` | Specific worktree by ID | | `diff(id)` | Changed files vs HEAD | | `merge(id, targetBranch)` | Merge worktree branch into target | ### Worktree Storage Worktrees stored in `.cw-worktrees/` subdirectory of the repo. Each agent gets a worktree at `.cw-worktrees/agent//`. ### Merge Flow 1. Check out target branch 2. `git merge --no-edit` 3. On success: emit `worktree:merged` 4. On conflict: `git merge --abort`, emit `worktree:conflict` with conflicting file list 5. Restore original branch ### BranchManager (`apps/server/git/branch-manager.ts`) - **Port**: `BranchManager` interface - **Adapter**: `SimpleGitBranchManager` using simple-git | Method | Purpose | |--------|---------| | `ensureBranch(repoPath, branch, baseBranch)` | Create branch from base if it doesn't exist (idempotent) | | `mergeBranch(repoPath, source, target)` | Merge via ephemeral worktree, returns conflict info | | `diffBranches(repoPath, base, head)` | Three-dot diff between branches | | `deleteBranch(repoPath, branch)` | Delete local branch (no-op if missing) | | `branchExists(repoPath, branch)` | Check local branches | | `remoteBranchExists(repoPath, branch)` | Check remote tracking branches (`origin/`) | | `listCommits(repoPath, base, head)` | List commits head has that base doesn't (with stats) | | `diffCommit(repoPath, commitHash)` | Get unified diff for a single commit | `remoteBranchExists` is used by `registerProject` and `updateProject` to validate that a project's default branch actually exists in the cloned repository before saving. ### Project Clones - `cloneProject(url, destPath)` — Simple git clone wrapper - `ensureProjectClone(project, workspaceRoot)` — Idempotent: checks if clone exists, clones if not - `getProjectCloneDir(name, id)` — Canonical path: `repos/-/` ### ProjectSyncManager (`apps/server/git/remote-sync.ts`) Fetches remote changes for project clones and fast-forwards the local `defaultBranch`. Safe to run with active worktrees — only updates remote-tracking refs and the base branch (never checked out directly by any worktree). **Constructor**: `ProjectSyncManager(projectRepository, workspaceRoot, eventBus?)` | Method | Purpose | |--------|---------| | `syncProject(projectId)` | `git fetch origin` + `git merge --ff-only origin/`, updates `lastFetchedAt` | | `syncAllProjects()` | Sync all registered projects sequentially | | `getSyncStatus(projectId)` | Returns `{ ahead, behind, lastFetchedAt }` via `rev-list --left-right --count` | | `getInitiativeDivergence(projectId, branch)` | Ahead/behind between `defaultBranch` and an initiative branch | **Sync flow during phase dispatch**: `dispatchNextPhase()` syncs all linked project clones *before* creating initiative/phase branches, so branches fork from up-to-date remote state. Sync is best-effort — failures are logged but don't block dispatch. **CLI**: `cw project sync [name] --all`, `cw project status [name]` **tRPC**: `syncProject`, `syncAllProjects`, `getProjectSyncStatus` ### Events Emitted `worktree:created`, `worktree:removed`, `worktree:merged`, `worktree:conflict`, `project:synced`, `project:sync_failed` --- ## Process Module (`apps/server/process/`) Spawns, tracks, and controls child processes. ### Classes **ProcessRegistry** — In-memory metadata store: - `register(info)`, `unregister(id)`, `get(id)`, `getAll()`, `getByPid(pid)`, `updateStatus(id, status)` **ProcessManager** — Lifecycle management: | Method | Purpose | |--------|---------| | `spawn(options)` | Spawn detached process (survives parent exit) | | `stop(id)` | SIGTERM → wait 5s → SIGKILL | | `stopAll()` | Stop all running processes in parallel | | `restart(id)` | Stop + re-spawn with same options | | `isRunning(id)` | Check with `process.kill(pid, 0)` | ### Spawn Details - Uses `execa` with `detached: true`, `stdio: 'ignore'` - Calls `subprocess.unref()` so parent can exit - Exit handler updates registry and emits events ### Events Emitted `process:spawned`, `process:stopped`, `process:crashed` --- ## Logger Module (`apps/server/logger/`) Structured logging via **pino**. ### Usage ```typescript import { createModuleLogger } from '../logger/index.js'; const log = createModuleLogger('my-module'); log.info({ key: 'value' }, 'message'); ``` ### Configuration | Env Var | Effect | |---------|--------| | `CW_LOG_LEVEL` | Override log level | | `CW_LOG_PRETTY` | Set to `'1'` for human-readable output | | `NODE_ENV=development` | Default to 'debug' level | ### Output - Default: JSON to stderr (fd 2) - Pretty mode: pino-pretty to stdout with colors and timestamps --- ## Logging Module (`apps/server/logging/`) File-based per-process output capture (separate from pino). ### Classes **LogManager** — Directory management: - Base dir: `~/.cw/logs/` - Structure: `{processId}/stdout.log`, `{processId}/stderr.log` - `cleanOldLogs(retainDays)` — removes old directories by mtime **ProcessLogWriter** — File I/O with timestamps: - `open()` — create directories and append-mode WriteStreams - `writeStdout(data)` / `writeStderr(data)` — prefix each line with `[YYYY-MM-DD HH:mm:ss.SSS]` - Handles backpressure (waits for drain event) - Emits `log:entry` event via EventBus ### Factory ```typescript import { createLogger } from './logging/index.js'; const writer = createLogger(processId, eventBus); await writer.open(); await writer.writeStdout('output data'); await writer.close(); ```