Files
Codewalkers/docs/git-process-logging.md
Lukas May 9894cdd06f feat: add diffBranchesStat and diffFileSingle to BranchManager
Adds FileStatEntry type and two new primitives to the BranchManager
port and SimpleGitBranchManager adapter, enabling split diff
procedures in the tRPC layer without returning raw multi-megabyte diffs.

- FileStatEntry captures path, status, additions/deletions, oldPath
  (renames), and optional projectId for multi-project routing
- diffBranchesStat uses --name-status + --numstat, detects binary
  files (shown as - / - in numstat), handles spaces in filenames
- diffFileSingle returns raw unified diff for a single file path
2026-03-06 19:33:47 +01:00

6.6 KiB

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/<alias>/.

Merge Flow

  1. Check out target branch
  2. git merge <source> --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
diffBranchesStat(repoPath, base, head) Per-file metadata (path, status, additions, deletions) — no hunk content. Binary files included with status: 'binary' and counts of 0. Returns FileStatEntry[].
diffFileSingle(repoPath, base, head, filePath) Raw unified diff for a single file (three-dot diff). filePath must be URL-decoded. Returns empty string for binary files.
deleteBranch(repoPath, branch) Delete local branch (no-op if missing)
branchExists(repoPath, branch) Check local branches
remoteBranchExists(repoPath, branch) Check remote tracking branches (origin/<branch>)
listCommits(repoPath, base, head) List commits head has that base doesn't (with stats)
diffCommit(repoPath, commitHash) Get unified diff for a single commit
getMergeBase(repoPath, branch1, branch2) Get common ancestor commit hash
pushBranch(repoPath, branch, remote?) Push branch to remote (default: 'origin')
checkMergeability(repoPath, source, target) Dry-run merge check via git merge-tree --write-tree (git 2.38+). Returns { mergeable, conflicts? } with no side effects

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/<sanitized-name>-<id>/

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/<defaultBranch>, 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

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

import { createLogger } from './logging/index.js';
const writer = createLogger(processId, eventBus);
await writer.open();
await writer.writeStdout('output data');
await writer.close();