This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
CRITICAL: Always maintain consistent, professional one-line style in CHANGELOG.md
- READ EXISTING ENTRIES FIRST - Match the existing style exactly
- One-line entries - Each change is a single, concise bullet point
- No subsections - Don't use "### Features", "### Breaking Changes", etc.
- Professional tone - Direct, technical language without marketing speak
- Specific details - Include technical specifics, not vague descriptions
## [1.8.0] - 2025-08-09
- Target-specific log files in `/tmp/poltergeist/` with plain text format (80% size reduction)
- Separate log file per target matching state file naming: `{projectName}-{hash}-{target}.log`
- Fixed Bun.spawn stdio configuration error - daemon now starts correctlyNOT THIS (verbose, subsectioned style):
### Breaking Changes
- **New logging system**: Separate log files per target...
### Features
- Target-specific log files...BREAKING CHANGE: Poltergeist v1.8.0+ uses separate log files per target with plain text format.
- Location:
/tmp/poltergeist/directory (same as state files) - Naming:
{projectName}-{hash}-{target}.log(matches state file pattern) - Format: Plain text with simple structure:
timestamp level: message - One log per build: Each build creates a fresh log file (no rotation)
- 80% size reduction compared to JSON format
- Zero parsing overhead for reading logs
- Natural filtering - each target has its own file
- No redundancy - target name never written in logs
- NOT MAINTAINED: Old JSON log format is not supported
- Migration is automatic - new builds use new format
- The
logscommand can still read old JSON logs if they exist
CRITICAL: Poltergeist is distributed as a pre-compiled Bun executable, NOT as a Node.js package.
- Build Bun binary: Run
pnpm run build:bunto create standalone executable - Create tarball: Package the binary as
poltergeist-macos-{arch}-v{version}.tar.gz - GitHub Release: Upload the tarball to GitHub releases
- Homebrew Formula: Download from GitHub releases, NOT from npm registry
class Poltergeist < Formula
desc "Universal file watcher with auto-rebuild"
homepage "https://github.com/steipete/poltergeist"
url "https://github.com/steipete/poltergeist/releases/download/v{VERSION}/poltergeist-macos-universal.tar.gz"
sha256 "..."
def install
bin.install "poltergeist"
bin.install "polter"
end
endNO Node.js dependency, NO npm installation, just direct binary installation.
-
Dynamic imports break compilation
await import('@module')fails in compiled binaries- Solution: Use static
require()with try/catch for optional dependencies
-
import.metabreaks bytecode compilationimport.meta.url,import.meta.dir,import.meta.maincause "Failed to generate bytecode"- Solution: Created
utils/paths.tswith runtime detection usingeval('import.meta.url')
-
Bun shell breaks bytecode
import { $ } from "bun"prevents bytecode compilation- Solution: Use
spawnSyncfrom child_process instead
-
Daemon spawning issues
Bun.spawn()doesn't work reliably for daemon processes in standalone binariesprocess.argv[0]is always "bun" in compiled executables- Solution: Use regular
spawn()with detached flag andprocess.execPath
-
Module resolution in compiled binaries
- Dynamically imported modules aren't included in the virtual filesystem
- Solution: Ensure all critical modules use static imports
- Always test with
--bytecodeflag for better startup performance - Use
process.execPathinstead ofprocess.argv[0]for binary path - Avoid top-level await in modules that will be compiled
- Use runtime feature detection instead of compile-time checks
Poltergeist can build itself! The project includes a poltergeist.config.json that watches its own source files.
# First time only - create initial build
pnpm run build
# Start Poltergeist to watch itself
poltergeist start
# That's it! Any changes to src/ will trigger rebuildsWhen working on Poltergeist itself, always use polter to ensure fresh binaries:
# Instead of: ./dist/cli.js
# Use: polter poltergeist-cli
# This ensures you're always running the latest build
polter poltergeist-cli status- NEVER manually run
pnpm run buildwhen Poltergeist is running - ALWAYS use
polter poltergeist-clito run commands - Poltergeist detects its own changes and rebuilds automatically
- The Mac app (poltergeist-mac target) also rebuilds automatically when enabled
NEVER create "v2", "Fixed", "Enhanced", "New", or similar duplicate files. Always work on the existing files directly. When refactoring or improving code:
- Edit files in place
- Make proper refactors to improve the codebase
- Don't create thin wrappers - do complete refactoring
- If a major rewrite is needed, replace the entire file content
- Focus on clean, maintainable code
- Implement proper abstractions, not quick fixes
- When refactoring, improve the entire system, not just patch issues
- Ensure backwards compatibility when possible
- Use minimal file headers without "Created by" or date comments
- Format:
//\n// FileName.swift\n// Poltergeist\n// - Omit author attribution and creation dates
Poltergeist is a file watcher and auto-builder for development projects. It uses Facebook's Watchman for efficient file watching and supports multiple build targets.
- macOS: 14.0+ (for SwiftUI Settings support)
- iOS: Not supported - macOS only app
- Architecture: Universal (Apple Silicon + Intel)
- Use
SettingsLinkfor all settings access (macOS 14+ only) - No legacy Objective-C selectors (
showSettingsWindow:,showPreferencesWindow:) - Settings window is handled entirely by SwiftUI's
Settingsscene
- State Management: Unified state files in
/tmp/poltergeist/ - Builders: Modular build system for different target types
- Watchman Integration: Efficient file watching
- CLI: Command-line interface for user interaction
- Migrated from separate lock/status files to unified state system
- State files now include process info, build status, and app metadata
- Implemented heartbeat mechanism for process liveness detection
- Added atomic file operations for reliability
# Core development workflow
pnpm run build # Compile TypeScript to dist/
npm test # Run Vitest test suite
pnpm run dev # Development mode with hot reload
pnpm run typecheck # TypeScript type checking
pnpm run lint # Biome linting and formatting checks
pnpm run lint:fix # Auto-fix linting issues
pnpm run format # Format code with Biome
# Documentation generation
pnpm run docs:build # Generate all documentation (TypeScript + Swift)
pnpm run docs:api # Generate TypeScript API docs only
pnpm run docs:swift # Generate Swift API docs only
pnpm run docs:serve # Serve documentation on localhost:8080
# Single test execution
npm test -- --run <test-name> # Run specific test file
npm test -- --watch # Watch mode for testscd apps/mac
# Building and testing
./scripts/build.sh # Build the macOS app
./scripts/test.sh # Run Swift tests
./scripts/lint.sh # SwiftLint checks
./scripts/format.sh # swift-format code formatting
# Xcode development
open Poltergeist.xcodeproj
# Or build via command line:
xcodebuild -project Poltergeist.xcodeproj -scheme Poltergeist buildPoltergeist consists of two complementary applications:
- Node.js CLI Tool (
src/): Cross-platform file watcher and build engine - macOS Native App (
apps/mac/): SwiftUI status bar monitor and GUI
Communication happens through shared state files in /tmp/poltergeist/ - not direct IPC.
- Orchestrates file watching, build queue, and target management
- Uses dependency injection pattern with
PoltergeistDependencies - Manages target lifecycle and state coordination
- IntelligentBuildQueue (
src/build-queue.ts): Manages parallel builds with priority scoring - PriorityEngine (
src/priority-engine.ts): Analyzes user focus patterns for smart build ordering - Builder Factory (
src/factories.ts): Creates target-specific builders (executable, app-bundle, library, etc.) - Individual Builders (
src/builders/): Handle target-specific build logic and validation
- WatchmanClient (
src/watchman.ts): Facebook Watchman integration with subscription management - WatchmanConfigManager (
src/watchman-config.ts): Auto-generates.watchmanconfigwith smart exclusions - ConfigLoader (
src/config.ts): Zod-based schema validation and configuration migration
- StateManager (
src/state.ts): Atomic file operations for inter-process coordination - Unified state files:
/tmp/poltergeist/{projectName}-{hash}-{target}.state - Lock-free design: Uses atomic write operations (temp file + rename)
- Heartbeat monitoring: Process liveness detection with automatic cleanup
- ProjectMonitor (
Services/ProjectMonitor.swift): Main actor that watches state directory - @Observable pattern throughout for reactive UI updates
- @MainActor classes ensure thread safety
- StatusBarController: Menu bar integration and user interaction
- FileWatcher: Monitors
/tmp/poltergeist/for state file changes - NotificationManager: Native macOS notifications for build events
- IconLoader: Dynamically loads app icons from project configurations
The system supports 7 target types, each with specialized builders:
- executable: CLI tools, binaries (
ExecutableBuilder) - app-bundle: macOS/iOS apps with bundle management (
AppBundleBuilder) - library: Static/dynamic libraries (
LibraryBuilder) - framework: Apple frameworks (
FrameworkBuilder) - test: Test suites (
TestBuilder) - docker: Container images (
DockerBuilder) - custom: User-defined builds (
CustomBuilder)
Each builder implements BaseBuilder interface with target-specific validation and execution logic.
All state is stored in unified JSON files at /tmp/poltergeist/{projectName}-{hash}-{target}.state:
{
"version": "1.0",
"projectPath": "/path/to/project",
"projectName": "my-project",
"target": "my-target",
"process": {
"pid": 12345,
"isActive": true,
"startTime": "2024-01-01T00:00:00.000Z",
"lastHeartbeat": "2024-01-01T00:01:00.000Z"
},
"lastBuild": {
"status": "success|failure|building|idle",
"timestamp": "2024-01-01T00:00:30.000Z",
"gitHash": "abc123",
"buildTime": 2.5,
"errorSummary": "Optional error message"
},
"appInfo": {
"bundleId": "com.example.myapp",
"outputPath": "/path/to/output",
"iconPath": "/path/to/icon.png"
}
}The Mac app requires a poltergeist.config.json file in your project root. Example for Swift projects:
{
"version": "1.0",
"projectType": "swift",
"targets": [
{
"name": "debug",
"type": "executable",
"enabled": true,
"buildCommand": "xcodebuild -project MyApp.xcodeproj -scheme MyApp -configuration Debug build",
"watchPaths": [
"Sources/**/*.swift",
"Tests/**/*.swift",
"*.xcodeproj/**"
],
"settlingDelay": 1000,
"debounceInterval": 3000
}
],
"notifications": {
"enabled": true,
"buildSuccess": true,
"buildFailed": true,
"icon": "./path/to/your/app/icon.png"
}
}- CLI: Run
poltergeistin your project directory - Mac App: Will automatically detect and monitor configured projects
- Uses Facebook's Watchman for efficient file system monitoring
- Automatically ignores build artifacts,
.DS_Store, Xcode user data - Configurable watch patterns per target
- Debouncing prevents excessive builds from rapid file changes
- Incremental compilation when possible
- Build times typically 1.5-3.5 seconds for Swift projects
- Automatic retry on transient failures
- Real-time build status in Mac app status bar
Poltergeist works with standard project scripts:
scripts/lint.sh- SwiftLint integrationscripts/format.sh- swift-format integrationscripts/test.sh- Swift Testing validation
- State files stored in
/tmp/poltergeist/ - Format:
{projectName}-{hash}-{target}.state - Contains build status, process info, heartbeat data
- Automatic cleanup of stale projects
FUNDAMENTAL PRINCIPLE: In Swift 6, when a callback is annotated with @MainActor, the Swift runtime performs queue assertions to verify that the code is actually running on the main dispatch queue. If the callback is invoked from a background queue (like DispatchSource event handlers), you get _dispatch_assert_queue_fail crashes.
The Problem:
- FileWatcher uses DispatchSource on background queue (
com.poltergeist.filewatcher) - Callback was declared as
@MainActor @Sendable () -> Void - When DispatchSource fires, it tries to call the
@MainActorcallback from background queue - Swift runtime performs queue assertion and fails: "This code should run on main queue but it's running on background queue!"
Wrong Solutions:
- ❌ Using
Task { @MainActor in ... }- Still fails because the Task creation itself triggers the assertion - ❌ Using
DispatchQueue.main.async { Task { @MainActor in ... } }- Overly complex and unnecessary
Correct Solution:
- ✅ Remove
@MainActorfrom callback signature:@Sendable () -> Void - ✅ Use
DispatchQueue.main.async { callback() }to manually ensure main queue execution - ✅ Let the callback implementation handle any
@MainActorrequirements viaTask { @MainActor in ... }
Key Insight: The callback dispatch mechanism must guarantee main queue execution BEFORE any @MainActor annotations take effect. By the time you have @MainActor annotations, Swift expects you're already on the main queue - using Task at that point is too late and triggers the assertion failure.
This is a Swift 6 concurrency isolation boundary issue where mixing dispatch queues with Swift concurrency requires careful queue management at the interface level.