Docs/Contributing/Architecture

Architecture

PurePoint has three components: a CLI, a daemon, and a macOS app. They communicate over a Unix socket using NDJSON.

System diagram

┌──────────┐     NDJSON/Unix Socket     ┌──────────────┐
│  pu CLI  │ <────────────────────────> │  pu-engine   │
└──────────┘                            │  (daemon)    │
                                        │              │
┌──────────┐     NDJSON/Unix Socket     │  PTY Host    │
│  macOS   │ <────────────────────────> │  Sessions    │
│  App     │                            │  Scheduler   │
└──────────┘                            │  Reaper      │
                                        └──────────────┘
  • The CLI auto-starts the daemon if not running
  • The macOS app launches the daemon in --managed mode (exits when parent dies)
  • Protocol version handshake via Health request
  • See IPC API Reference for the full protocol

Rust crate structure

crates/
├── pu-core/     # Shared types library (no async deps)
├── pu-engine/   # Daemon binary (async, tokio)
└── pu-cli/      # CLI binary (thin client)

Dependency graph

pu-cli ──> pu-core
pu-engine ──> pu-core

pu-cli --dev--> pu-engine  (integration tests)

pu-core

Pure data types, serialization, and file I/O. No async runtime.

ModuleResponsibility
types.rsManifest, AgentEntry, WorktreeEntry, Config, AgentStatus
protocol.rsIPC Request/Response enums (49 variants), protocol version
manifest.rsAtomic read/write/update with file locking
config.rsConfig loading from .pu/config.yaml
paths.rsAll filesystem path conventions
template.rsPrompt template parsing, rendering, CRUD
agent_def.rsAgent definition YAML format, CRUD
swarm_def.rsSwarm composition format, CRUD
schedule_def.rsSchedule definitions, recurrence calculation
trigger_def.rsTrigger definitions, event types, gate defs
id.rsID generation (wt-/ag- prefixed, UUID sessions)
validation.rsResource name validation (path traversal protection)
error.rsPuError enum with error codes

pu-engine

Async daemon. Depends on pu-core + tokio + nix + tracing.

ModuleResponsibility
main.rsEntry point, arg parsing, server loop
engine.rsCore business logic, request dispatch, state management
ipc_server.rsUnix socket server, NDJSON framing, connection handling
pty_manager.rsPTY spawn via fork/setsid/execvp, process monitoring
output_buffer.rs4MB circular buffer with idle detection
agent_monitor.rsEffective status computation
attach_handler.rsAPC escape parsing for terminal resize
git.rsWorktree creation, hook installation
gate.rsGate evaluation with timeouts and retries
daemon_lifecycle.rsPID file management, cleanup

pu-cli

Thin client. Depends on pu-core + clap + tokio.

ModuleResponsibility
main.rsClap CLI definitions (21 commands)
client.rsNDJSON-over-Unix-socket request sender
daemon_ctrl.rsDaemon auto-start with health polling
commands/One module per CLI command
output.rsHuman-readable and JSON output formatting
skill.rsClaude Code skill auto-installation

macOS app

SwiftUI + AppKit application.

LayerKey filesResponsibility
Entrypurepoint_macosApp.swiftApp lifecycle, AppState creation
StateAppState, SidebarState, GridStateObservable state management
ModelsManifestModel, AgentModel, WorkspaceModelData models (Codable for manifest)
ServicesWorkspaceService, DaemonClient, CLIInstallerDaemon communication, CLI installation
ViewsViews/ (97 files)SwiftUI views organized by feature
TerminalTerminalViewCache, ScrollableTerminalSwiftTerm integration with LRU caching

Communication: The app uses DaemonClient (NDJSON over Unix socket) mirroring the Rust protocol types.

Key patterns

Manifest as source of truth

.pu/manifest.json is read/written by both Rust and Swift. Atomic writes (temp + rename), file locking (fs4), camelCase JSON keys for Swift Codable compatibility.

Three-state agent model

Agents have exactly 3 states: streaming, waiting, broken. Computed from PTY output patterns and process state. Backward-compatible deserialization from legacy 8-state model.

Local/global scope

Templates, agent defs, swarm defs, schedules, and triggers all follow the same pattern: .pu/{type}/ (local) and ~/.pu/{type}/ (global). Local shadows global. See Concepts: Scope.

Engine state

The Engine struct holds all runtime state:

  • sessions: HashMap of agent handles (pid, master_fd, output_buffer)
  • grid_channels / status_channels: Broadcast channels per project
  • registered_projects: Projects the scheduler monitors
  • Background tasks: session reaper (30s), scheduler (30s)

Data flows

Spawn flow

  1. CLI sends Spawn request to daemon
  2. Engine loads config, resolves agent type and launch args
  3. Engine creates worktree (if needed) via git worktree add
  4. PTY Manager forks, calls setsid + execvp to start agent
  5. Engine injects prompt via stdin (claude/terminal) or CLI arg (codex/opencode)
  6. Manifest updated with new worktree and agent entries
  7. macOS app's ManifestWatcher picks up the change

Output flow

  1. Agent writes to PTY
  2. OutputBuffer captures bytes in 4MB circular buffer
  3. Attach stream sends hex-encoded chunks to connected clients
  4. Logs request reads last N bytes from buffer
  5. Idle detection: shell prompt patterns or 30s silence

Trigger flow

  1. Event fires (agent_idle detected, git hook calls pu gate)
  2. Engine loads matching trigger defs
  3. For each action: evaluate gate (if present), inject text (if present)
  4. Gate retries on failure, sequence stops on final failure
  5. Trigger state tracked in agent's manifest entry