Files
Codewalkers/docs/git-process-logging.md
Lukas May 84250955d1 fix: Show completed phase diffs in review tab
Completed phases showed "No phases pending review" because:
1. Frontend filtered only pending_review phases
2. Server rejected non-pending_review phases
3. After merge, three-dot diff returned empty (merge base moved)

Fix: store pre-merge merge base hash on phase, use it to reconstruct
diffs for completed phases. Frontend now shows both pending_review and
completed phases with read-only mode (Merged badge) for completed ones.
2026-03-05 22:05:28 +01:00

6.0 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
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

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();