Skip to content

Commit c244472

Browse files
tjp2021claude
andcommitted
docs: rewrite README with actionable user documentation
Apply same standard as weights section to entire file: - Quick Start: explain install vs start, two-command recommended path - Scan output: document SIGNALS column and how to interpret it - Add scan -v use case (tuning / debugging false positives) - Config: explain grace_period as SIGTERM→wait→SIGKILL sequence - Config: explain dry_run and how to find results in devreap logs - Patterns: explain max_duration is scoring signal, not hard kill timer - Add Troubleshooting section covering 4 common scenarios - Remove --foreground flag (internal, not user-facing) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 681bf17 commit c244472

1 file changed

Lines changed: 146 additions & 110 deletions

File tree

README.md

Lines changed: 146 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ When your IDE crashes or you force-quit a terminal, child processes survive as o
2121

2222
**84% of developers now use AI tools** (Stack Overflow 2025). Each AI coding session spawns 3-10 background processes. None reliably clean up on crash or force-quit.
2323

24-
macOS makes this worse: there's no `PR_SET_PDEATHSIG` (Linux's mechanism to auto-kill children when parents die), no `PR_SET_CHILD_SUBREAPER`, no kernel-level safety net. When your IDE dies on macOS, orphans survive indefinitely.
24+
macOS makes this worse: there's no `PR_SET_PDEATHSIG` (Linux's mechanism to auto-kill children when parents die), no kernel-level safety net. When your IDE dies on macOS, orphans survive indefinitely.
2525

26-
`kill-port` gets **1.16M weekly npm downloads** — that's a million developers manually killing port-squatting processes every week. `fkill-cli` has 6,900 GitHub stars. Both are reactive (you find the problem, you kill it). Nothing proactively detects and cleans up orphans.
26+
`kill-port` gets **1.16M weekly npm downloads** — that's a million developers manually killing port-squatting processes every week. Both are reactive (you find the problem, you kill it). Nothing proactively detects and cleans up orphans.
2727

2828
devreap does.
2929

@@ -47,25 +47,36 @@ go install github.com/tjp2021/devreap/cmd/devreap@latest
4747

4848
## Quick Start
4949

50+
**Recommended setup — two commands:**
51+
5052
```bash
51-
# See what's orphaned right now
52-
devreap scan
53+
# 1. Install as a LaunchAgent so it runs automatically on login
54+
devreap install
5355

54-
# Start the daemon (scans every 30s, kills orphans automatically)
56+
# 2. Start it now (without waiting for next login)
5557
devreap start
58+
```
5659

57-
# Install as LaunchAgent (auto-start on login)
58-
devreap install
60+
That's it. devreap runs in the background, scans every 30 seconds, and kills orphans automatically. You'll get a macOS notification when something gets killed.
5961

60-
# Check status
61-
devreap status
62+
**Just want to see what's orphaned without running a daemon:**
6263

63-
# Run diagnostics
64-
devreap doctor
64+
```bash
65+
devreap scan # show orphan candidates
66+
devreap scan -v # show all matched processes, including safe ones (useful for debugging)
67+
```
68+
69+
**Check it's running:**
70+
71+
```bash
72+
devreap status
73+
devreap doctor # full diagnostics — checks config, patterns, process enumeration
6574
```
6675

6776
## What It Looks Like
6877

78+
Running `devreap scan` shows matched processes and their scores:
79+
6980
```
7081
$ devreap scan
7182
@@ -79,11 +90,15 @@ PID NAME PATTERN SCORE AGE STATUS SIGNALS
7990
7099 node node-mcp-server 0.65 18h33m ORPHAN ppid_is_init, exceeded_duration, no_tty
8091
```
8192

93+
The **SIGNALS** column shows which signals contributed to the score — what specifically made devreap flag this process. `ppid_is_init` means the parent died. `parent_ide_dead` means no IDE is running. `exceeded_duration` means it's been running longer than this type of process should. If you think something was killed incorrectly, the signals tell you exactly why it was flagged.
94+
95+
Use `devreap scan -v` to see all matched processes, including ones below the kill threshold — useful when tuning or debugging false positives.
96+
97+
When the daemon kills something, it logs the full reason:
98+
8299
```
83-
$ devreap logs --level info
100+
$ devreap logs
84101
85-
14:02:31 INFO daemon starting
86-
14:02:31 INFO found 2 orphan candidates
87102
14:02:31 INFO killed orphan pid=3338 process=node pattern=node-mcp-server score=0.70 signals=[ppid_is_init=0.40,parent_ide_dead=0.30]
88103
14:02:31 INFO killed orphan pid=7099 process=node pattern=node-mcp-server score=0.65 signals=[ppid_is_init=0.40,exceeded_duration=0.25]
89104
```
@@ -92,23 +107,21 @@ $ devreap logs --level info
92107

93108
### Multi-Signal Orphan Scoring
94109

95-
devreap doesn't use binary "is orphan" / "isn't orphan" detection. A process that matches a known pattern gets scored across multiple signals:
110+
devreap doesn't use binary "is orphan" / "isn't orphan" detection. A process that matches a known pattern gets scored across multiple signals. Each signal has a weight — the score is the sum of weights for signals that fire. If the total reaches the kill threshold (default 0.6), the process is killed.
96111

97-
| Signal | Weight | Description |
98-
|--------|--------|-------------|
99-
| `ppid_is_init` | 0.40 | PPID is 1 (parent died, reparented to launchd) |
100-
| `parent_ide_dead` | 0.30 | No IDE process running (VS Code, Cursor, Claude Code, Zed, JetBrains) |
101-
| `exceeded_duration` | 0.25 | Running longer than the pattern's max duration |
102-
| `has_listener` | 0.20 | Bound to a listening port (potential orphaned server) |
103-
| `no_tty` | 0.15 | No controlling terminal attached |
104-
105-
**Default threshold: 0.6** — a process needs multiple signals to be flagged. This eliminates false positives.
112+
| Signal | Weight | When it fires |
113+
|--------|--------|---------------|
114+
| `ppid_is_init` | 0.40 | Parent process died — process was reparented to launchd (PPID = 1) |
115+
| `parent_ide_dead` | 0.30 | No IDE is running anywhere on the machine |
116+
| `exceeded_duration` | 0.25 | Process has been running longer than its pattern's max duration |
117+
| `has_listener` | 0.20 | Process is bound to a listening TCP port |
118+
| `no_tty` | 0.15 | Process has no controlling terminal |
106119

107120
**Examples:**
108-
- MCP server, PPID=1, no Cursor running → **0.70** → killed
109-
- MCP server, PPID=1, Cursor IS running → **0.40** → safe
110-
- Dev server, PPID=1, running 48 hours → **0.65** → killed
111-
- Your Postgres, running as `_postgres` user → **0.00** → ignored (wrong user)
121+
- MCP server, PPID=1, no Cursor running → 0.40 + 0.30 = **0.70** → killed
122+
- MCP server, PPID=1, Cursor IS running → 0.40 only = **0.40** → safe
123+
- Dev server, PPID=1, running 48 hours → 0.40 + 0.25 = **0.65** → killed
124+
- Your Postgres, running as `_postgres` user → **0.00** → ignored (devreap only scores your own processes)
112125

113126
### IDE Detection
114127

@@ -123,103 +136,112 @@ devreap reads your IDE's MCP configuration files:
123136
- `~/.cursor/mcp.json` (Cursor)
124137
- `~/.vscode/mcp.json` (VS Code)
125138

126-
It knows which MCP servers *should* be running. If servers are running but no IDE is active, they're flagged.
139+
It knows which MCP servers *should* be running. If those servers are running but no IDE is active, they're flagged as orphans. `devreap doctor` will warn you if any of these files exist but can't be parsed.
140+
141+
### How Killing Works
142+
143+
When a process hits the threshold, devreap sends signals in sequence and waits between each:
127144

128-
### Pattern-Aware Signal Strategy
145+
1. **First signal** (SIGTERM by default, SIGINT for ffmpeg) — asks the process to exit cleanly
146+
2. **Wait** (grace period, default 5 seconds)
147+
3. **SIGTERM** (if first signal was SIGINT)
148+
4. **Wait** (grace period)
149+
5. **SIGKILL** — force kill if still running
129150

130-
Each process type gets the right shutdown signal:
131-
- **ffmpeg**`SIGINT` first (writes moov atom for clean MP4, then SIGTERM, then SIGKILL)
132-
- **Node.js**`SIGTERM``SIGKILL`
133-
- **Chrome**`SIGTERM` with extended grace period
151+
ffmpeg gets SIGINT first because that's the signal that makes it write the MP4 file headers correctly. SIGKILL on ffmpeg produces a corrupted file.
134152

135153
### Safety
136154

137-
- **PID reuse protection** — verifies process name before sending signals
138-
- **User isolation** — only kills your processes, never another user's
139-
- **Blocklist** — postgres, redis, nginx, sshd, and 20+ system processes are protected
140-
- **PID 1 / self / parent protection** — hardcoded, can't be overridden
141-
- **Config validation** — rejects invalid thresholds, weights, and intervals at startup
155+
devreap will **never** kill:
156+
- PID 1 (launchd/init) — hardcoded
157+
- Its own process — hardcoded
158+
- Its parent process — hardcoded
159+
- Any process owned by a different user
160+
- Anything on the blocklist (postgres, redis, nginx, sshd, and 20+ other system processes by default)
161+
162+
Before sending any signal, devreap re-verifies the process name matches what was scanned. If the PID was reused by a different process in between, it aborts.
142163

143164
## Commands
144165

145166
```
146167
devreap scan # One-shot scan, print orphan candidates
147-
devreap scan --json # Machine-readable output
168+
devreap scan --json # Machine-readable JSON output
148169
devreap scan -v # Show all pattern matches (including safe ones)
149170
devreap start # Start background daemon
150-
devreap start --foreground # Foreground mode (used by LaunchAgent)
151171
devreap stop # Stop daemon
152-
devreap status # Daemon status + config
153-
devreap kill <pid> # Manual graceful kill
154-
devreap kill --port 3000 # Kill by port
155-
devreap logs # View recent daemon log entries
156-
devreap logs -n 100 # Show last 100 entries
157-
devreap logs --level error # Filter by severity
158-
devreap logs --json # Raw JSON (pipe to jq)
159-
devreap install # Install macOS LaunchAgent
172+
devreap status # Daemon status + current config
173+
devreap install # Install macOS LaunchAgent (auto-start on login)
160174
devreap uninstall # Remove LaunchAgent
161-
devreap doctor # Run diagnostics
162-
devreap patterns # List all 18 built-in patterns
163-
devreap version # Print version
175+
devreap kill <pid> # Manually kill a process gracefully
176+
devreap kill --port 3000 # Kill whatever is listening on a port
177+
devreap logs # View recent daemon log entries (last 50)
178+
devreap logs -n 100 # Show last N entries
179+
devreap logs --level error # Filter by severity (debug, info, warn, error)
180+
devreap logs --json # Raw JSON lines — pipe to jq for filtering
181+
devreap doctor # Diagnostics: config, patterns, permissions, MCP configs
182+
devreap patterns # List all 18 built-in patterns with durations and signals
183+
devreap version # Print version, commit, and build date
164184
```
165185

166186
## Configuration
167187

168-
Optional. devreap works with zero config using sensible defaults.
169-
170-
Create `~/.config/devreap/config.yaml` to customize:
188+
devreap works out of the box with no config file. Create `~/.config/devreap/config.yaml` only if you need to change something.
171189

172190
```yaml
173-
scan_interval: 30s # How often to scan (min: 1s, max: 24h)
174-
kill_threshold: 0.6 # Score threshold to kill (0.1 - 1.0)
175-
grace_period: 5s # Time between signals
176-
dry_run: false # Log what would be killed without killing
191+
scan_interval: 30s # How often to scan. Min: 1s. Max: 24h.
192+
kill_threshold: 0.6 # Minimum score to kill a process. Range: 0.1 - 1.0.
193+
# Lower = more aggressive. Higher = more conservative.
194+
grace_period: 5s # How long to wait between signals (SIGTERM → wait → SIGKILL).
195+
# Min: 1s. Give processes time to clean up before force-killing.
196+
dry_run: false # If true, logs what would be killed but doesn't kill anything.
197+
# Useful for testing — run `devreap logs` to see what it caught.
177198

178199
notify:
179-
enabled: true # macOS notifications on kill
200+
enabled: true # macOS notifications when the daemon kills something.
180201

181-
# Tune signal weights (each 0.0 - 1.0)
182-
# Higher weight = that signal contributes more to the orphan score.
183-
# A process is killed when its total score >= kill_threshold (default 0.6).
202+
# Signal weights — how much each signal contributes to the orphan score.
203+
# A process is killed when its total score >= kill_threshold.
204+
# Higher weight = that signal matters more. Each must be 0.0 - 1.0.
184205
#
185-
# Common tuning scenarios:
186-
# Getting false positives on MCP servers while IDE is open?
187-
# → Lower parent_ide_dead (e.g. 0.1) so IDE presence matters less
188-
# Running headless servers with no TTY that aren't orphans?
189-
# → Lower no_tty (e.g. 0.05) so terminal absence matters less
206+
# When to tune weights:
207+
# Getting false positives on MCP servers while your IDE is open?
208+
# → Lower parent_ide_dead (e.g. 0.1)
209+
# Running intentional background servers with no TTY?
210+
# → Lower no_tty (e.g. 0.05)
190211
# Want more aggressive cleanup of long-running processes?
191212
# → Raise exceeded_duration (e.g. 0.4)
192-
# Want to rely almost entirely on PPID detection?
193-
# → Raise ppid_is_init (e.g. 0.7) and lower the others
194-
weights:
195-
ppid_is_init: 0.4 # Parent process died (PPID reparented to launchd)
196-
parent_ide_dead: 0.3 # No IDE running on this machine
197-
exceeded_duration: 0.25 # Process running longer than pattern's max_duration
198-
has_listener: 0.2 # Process is bound to a listening port
199-
no_tty: 0.15 # No controlling terminal
200-
201-
# Note: setting one weight preserves all others at their defaults.
213+
# Want to rely almost entirely on PPID?
214+
# → Raise ppid_is_init (e.g. 0.7)
215+
#
202216
# You only need to specify the weights you want to change.
203-
204-
# Never kill these (in addition to built-in system process protection)
217+
# Unspecified weights keep their defaults.
218+
weights:
219+
ppid_is_init: 0.4 # Parent process died (PPID = 1)
220+
parent_ide_dead: 0.3 # No IDE running on this machine
221+
exceeded_duration: 0.25 # Running longer than pattern's max_duration
222+
has_listener: 0.2 # Bound to a TCP listening port
223+
no_tty: 0.15 # No controlling terminal
224+
225+
# Processes to never kill, by name. Case-insensitive.
226+
# These are in addition to the built-in protection list (postgres, redis, nginx, sshd, etc.)
205227
blocklist:
206-
- postgres
207-
- redis-server
208-
- nginx
228+
- my-database
229+
- my-background-worker
209230

210-
# Always skip these even if they match a pattern and score above threshold.
211-
# Use this for persistent servers you intentionally run in the background.
231+
# Processes to skip even if they score above the threshold.
232+
# Use this for servers you intentionally run persistently in the background.
233+
# Matches against process name and command line. Case-insensitive.
212234
allowlist:
213235
- my-persistent-mcp-server
214236

215-
# Additional pattern files beyond built-ins
237+
# Paths to additional YAML pattern files to load alongside the built-ins.
216238
extra_patterns:
217239
- ~/.config/devreap/my-patterns.yaml
218240
```
219241
220242
## Built-in Patterns
221243
222-
18 patterns across 4 categories:
244+
18 patterns across 4 categories. The **Max Duration** is how long a process of that type is allowed to run before the `exceeded_duration` signal fires — it's not a hard kill timer, it contributes 0.25 to the score.
223245

224246
| Category | Patterns | Max Duration | Signal |
225247
|----------|----------|-------------|--------|
@@ -228,36 +250,50 @@ extra_patterns:
228250
| **Headless browsers** | Chrome (headless + remote debugging), Firefox | 2-4h | SIGTERM |
229251
| **Media tools** | ffmpeg, ffprobe, sox, ImageMagick | 30m-2h | SIGINT/SIGTERM |
230252

231-
See all with `devreap patterns`.
253+
Run `devreap patterns` for the full list with all fields.
254+
255+
## Troubleshooting
256+
257+
**Something got killed that shouldn't have been**
258+
259+
Run `devreap logs --json | tail -20` to see the last kills with full signal breakdown. The `signals` field shows exactly what triggered it. Then either:
260+
- Add it to the `allowlist` in your config to permanently protect it
261+
- Lower the relevant weight if that signal fires too aggressively for your setup
262+
- Raise `kill_threshold` (e.g. to `0.7`) to require stronger evidence before killing
263+
264+
**devreap isn't catching orphans I know exist**
265+
266+
Run `devreap scan -v` — this shows all processes matching a pattern, even ones below the threshold. Look at their scores and which signals are firing. If a process has score 0.40 and you want it caught, either lower `kill_threshold` or raise the weight of a signal that's firing.
267+
268+
**I want to test what it would kill before letting it run for real**
269+
270+
Set `dry_run: true` in your config, then run `devreap start`. It will log everything it *would* kill to `devreap logs` without actually killing anything. Review the logs and adjust config, then set `dry_run: false`.
271+
272+
**A process isn't matching any pattern**
273+
274+
Run `devreap patterns` to see what's covered. If your process isn't there, you can add a custom pattern — see the CONTRIBUTING guide.
275+
276+
**devreap doctor shows a warning**
277+
278+
Run `devreap doctor` — it checks config validity, pattern loading, process enumeration, MCP config parsing, and LaunchAgent status. Warnings include an explanation of what to do.
232279

233280
## Architecture
234281

282+
Single static binary. No runtime dependencies. Cross-compiles to macOS (arm64/amd64) and Linux.
283+
235284
```
236-
cmd/devreap/main.go → entry point
285+
cmd/devreap/main.go → entry point
237286
internal/
238-
cli/ → 10 cobra commands
239-
scanner/
240-
process.go → gopsutil process enumeration + port mapping
241-
scorer.go → multi-signal scoring engine + IDE detection
242-
orphan.go → threshold filtering + parent-first kill ordering
243-
mcp.go → MCP config cross-referencing
244-
patterns/
245-
registry.go → go:embed YAML pattern loading
246-
matcher.go → compiled regex caching (18 compiles, not 12,600)
247-
killer/
248-
killer.go → PID-reuse-safe signal delivery
249-
safety.go → blocklist + ownership checks
250-
signals.go → per-pattern signal sequences
251-
daemon/
252-
daemon.go → scan loop with 30s timeout, sync.Once stop
253-
launchagent.go → macOS plist generation + bootstrap/bootout
254-
config/ → YAML config with validation + partial merge
255-
logger/ → structured JSON logging with rotation
256-
notify/ → macOS notifications via osascript
287+
scanner/ → process enumeration, orphan scoring, MCP cross-referencing
288+
patterns/ → embedded YAML pattern library, regex matching
289+
killer/ → signal delivery, PID reuse protection, safety checks
290+
daemon/ → scan loop, LaunchAgent install/uninstall
291+
config/ → YAML config loading with validation
292+
logger/ → structured JSON logging with rotation
293+
notify/ → macOS notifications
294+
cli/ → all commands
257295
```
258296
259-
Single static binary. No runtime dependencies. Cross-compiles to macOS (arm64/amd64) and Linux.
260-
261297
## License
262298
263299
MIT

0 commit comments

Comments
 (0)