registerProject and updateProject now verify via remoteBranchExists that the specified branch actually exists in the cloned repository before saving.
4.5 KiB
Git, Process, and Logging Modules
Three infrastructure modules supporting agent execution.
Git Module (src/git/)
Manages git worktrees for isolated agent workspaces.
Architecture
- Port:
WorktreeManagerinterface - Adapter:
SimpleGitWorktreeManagerusing 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
- Check out target branch
git merge <source> --no-edit- On success: emit
worktree:merged - On conflict:
git merge --abort, emitworktree:conflictwith conflicting file list - Restore original branch
BranchManager (src/git/branch-manager.ts)
- Port:
BranchManagerinterface - Adapter:
SimpleGitBranchManagerusing 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>) |
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 wrapperensureProjectClone(project, workspaceRoot)— Idempotent: checks if clone exists, clones if notgetProjectCloneDir(name, id)— Canonical path:repos/<sanitized-name>-<id>/
Events Emitted
worktree:created, worktree:removed, worktree:merged, worktree:conflict
Process Module (src/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
execawithdetached: 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 (src/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 (src/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 WriteStreamswriteStdout(data)/writeStderr(data)— prefix each line with[YYYY-MM-DD HH:mm:ss.SSS]- Handles backpressure (waits for drain event)
- Emits
log:entryevent 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();