# Workstation Agent Resource Control Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use /ship or /executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

Audience: AI coding agents first.

**Goal:** Make Claude/Codex-launched builds enforceable, inspectable, and recoverable without treating long-lived Claude sessions as disposable.

**Architecture:** Keep long-running Claude/Codex sessions valid. Enforce heavy build/test work through PATH shims plus deny-gate rules. Add process classifier + reaper that targets stale explicit build/test descendants, not agent sessions by age.

**Tech Stack:** Bash, Node.js built-ins, systemd user scopes, cgroup v2, existing `~/.claude/lib/cpu-guard.sh`, existing `~/.claude/hooks/deny-gate.mjs`.

---

## Wave Plan

| Wave | Tasks | Files touched | Safe to parallelize? |
|------|-------|---------------|----------------------|
| 1 | Task 1 | `~/.claude/bin/agent-process-report`, `~/.claude/docs/process-classification.md` | single task |
| 2 | Task 2 | `~/.profile`, `~/.config/environment.d/50-cpu-guard.conf` | single task |
| 3 | Task 4 | `~/.claude/bin/agent-process-reaper`, `~/.claude/systemd/agent-process-reaper.service`, `~/.claude/systemd/agent-process-reaper.timer` | single task |
| 4 | Task 6 | `~/.claude/bin/local-gate`, `~/.claude/local-gate.json`, `~/.claude/tests/local-gate.test.mjs`, `~/.claude/docs/free-verification-policy.md` | single task |
| 5 | Task 7 | `~/.claude/tools.json`, `~/.claude/tests/deny-gate-build-rules.test.mjs` | single task |
| 6 | Task 8 | `/etc/systemd/system/actions.runner.platform-modules-mod.platform-devbox.service.d/resource-control.conf`, `~/.claude/bin/install-actions-runner-resource-control`, `~/.claude/bin/actions-runner-gate-audit`, `~/.claude/tests/actions-runner-gate-audit.test.mjs` | single task |
| 7 | Task 5 | verification only | single task |

## File Map

- `~/.claude/lib/cpu-guard.sh` exists. Keep as canonical resource wrapper.
- `~/.claude/bin/*` shims exist. Make sure every agent shell sees this directory first in `PATH`.
- `~/.claude/tools.json` is deny-gate registry. Add raw build/test denial rules here.
- `~/.claude/bin/agent-process-report` should classify active agent/build processes.
- `~/.claude/bin/agent-process-reaper` should terminate stale suspect descendants only.
- `~/.claude/systemd/*.service|*.timer` should install optional user timer units copied/symlinked to `~/.config/systemd/user/`.
- `~/.claude/bin/local-gate` should schedule push/PR gates that launch full builds according to live system pressure.
- `/opt/actions-runner-platform` exists. Do not edit vendor `run.sh` unless no systemd override can enforce the same contract.

## Policy

- NEVER kill `claude` solely because it is old. Claude sessions may run for days.
- NEVER kill `codex` solely because it is old. Codex sessions may run for days.
- Treat build/test descendants older than 2 hours as suspect when ancestry includes Claude/Codex and command matches `pnpm|npm|npx|yarn|bun|vite|vitest|astro|tsc|esbuild|playwright|cargo|go`.
- Phase 1 reaper is warning-only. It prints suspect PIDs and waits for human approval.
- Phase 1 apply mode MUST require explicit approved PID list. No implicit auto-kill.
- Enable auto-kill only after repeated live warning-only runs show zero false positives.
- Prefer stopping approved suspect process group with SIGTERM, then SIGKILL only after grace period.
- Maintain allowlist file: `~/.claude/resource-control-allowlist`. Any PID listed there is protected.

## Task 1: Process Classifier

**Wave:** 1  
**Blocks:** Task 4  
**Blocked by:** -

**Files:**
- Create: `~/.claude/bin/agent-process-report` - prints compact table of agent/build processes.
- Create: `~/.claude/docs/process-classification.md` - documents essential/suspect rules.

**Contract:**
- CLI: `agent-process-report [--json] [--suspects-only]`
- Inputs: `/proc`, `ps -eo pid,ppid,etimes,pcpu,rss,comm,args`
- Output table fields: `pid`, `ppid`, `age`, `cpu`, `rss_mb`, `role`, `suspect_reason`, `command`.
- JSON output: array of objects with same field names.

**Behavior:**
- Role values: `claude-session`, `codex-child`, `build-child`, `desktop`, `other`.
- Old Claude session is not suspect by age.
- Old Codex session is not suspect by age.
- Claude/Codex-launched build tool older than 7200 seconds is suspect.
- Allowlisted PID is never suspect.

**Acceptance:**
- Run: `~/.claude/bin/agent-process-report --json`
- Expected: valid JSON; old Claude/Codex sessions have no age-only suspect reason; old explicit build/test descendants have suspect reason `build-child-age>2h`.

- [ ] Write classifier.
- [ ] Write process classification doc.
- [ ] Run acceptance check.

## Task 2: PATH Propagation

**Wave:** 2  
**Blocks:** Task 5  
**Blocked by:** Task 1

**Files:**
- Modify: `~/.profile` - prepend `~/.claude/bin` for login shells.
- Modify: `~/.config/environment.d/50-cpu-guard.conf` - keep systemd user session PATH intent explicit.

**Contract:**
- Every new terminal/agent shell should resolve `pnpm`, `npm`, `npx`, `vitest`, `astro`, `tsc`, `vite`, `esbuild` to `~/.claude/bin/<tool>` first.
- Do not remove existing PATH entries.

**Behavior:**
- Login shell, interactive Bash, and systemd user session all attempt to expose `~/.claude/bin` first.
- Duplicate PATH entries are acceptable if existing shell config already duplicates other entries.

**Acceptance:**
- Run: `bash -lc 'command -v pnpm; command -v npm; command -v vitest || true'`
- Expected: matching installed tools resolve under `/home/user/.claude/bin/` where shim exists.

- [ ] Patch PATH startup files.
- [ ] Run acceptance check in a fresh shell.

## Task 4: Stale Process Reaper

**Wave:** 3  
**Blocks:** Task 5  
**Blocked by:** Task 1

**Files:**
- Create: `~/.claude/bin/agent-process-reaper` - dry-run by default; `--apply` to terminate.
- Create: `~/.claude/systemd/agent-process-reaper.service` - oneshot user service.
- Create: `~/.claude/systemd/agent-process-reaper.timer` - periodic user timer.

**Contract:**
- CLI: `agent-process-reaper [--dry-run] [--apply --approve-pids PID[,PID...]] [--min-age-seconds N] [--grace-seconds N]`
- Defaults: dry run, `min-age=7200`, `grace=20`.
- Uses same classifier logic as `agent-process-report`.

**Behavior:**
- Dry-run prints candidates only.
- Apply refuses to run without explicit `--approve-pids`.
- Apply rechecks approved PIDs against current candidate list before sending any signal.
- Apply sends SIGTERM to approved process group when safe, waits grace, then SIGKILL remaining same approved candidate PIDs.
- Never kills Claude by age.
- Never kills allowlisted PIDs.
- Logs actions to `~/.claude/resource-control-reaper.log`.

**Acceptance:**
- Run: `~/.claude/bin/agent-process-reaper --dry-run`
- Expected: prints candidate list; exits without killing.
- Run: `~/.claude/bin/agent-process-reaper --apply`
- Expected: exits nonzero and kills nothing because `--approve-pids` is missing.
- Run with synthetic mocked `ps` input if implemented with injectable source.
- Expected: Claude/Codex older than days survive; explicit build/test descendant older than 2h is candidate.

- [ ] Implement dry-run reaper.
- [ ] Add apply mode with TERM/KILL grace.
- [ ] Add systemd service/timer templates.
- [ ] Run dry-run acceptance.

## Task 5: End-to-End Verification

**Wave:** 7  
**Blocks:** -  
**Blocked by:** Task 2, Task 4, Task 6, Task 7, Task 8

**Files:**
- No edits unless verification reveals a defect.

**Acceptance:**
- Run: `bash -lc 'echo PATH=$PATH; command -v pnpm; command -v npm'`
- Expected: shim path first for shimmed tools.
- Run sample deny-gate tests from Task 7.
- Expected: raw heavy commands denied, wrapped commands allowed.
- Run: `~/.claude/bin/local-gate --dry-run --key smoke --mode full -- true`
- Expected: prints effective slot decision and wrapped command plan; kills nothing.
- Run: `~/.claude/bin/actions-runner-gate-audit --json`
- Expected: reports runner service exists, PATH containment status, and workflow commands with raw high-concurrency gates.
- Run: `~/.claude/bin/agent-process-report --suspects-only`
- Expected: output identifies suspect explicit build/test descendants, not old Claude/Codex sessions by age.
- Run: `~/.claude/bin/agent-process-reaper --dry-run`
- Expected: no processes killed.

- [ ] Verify PATH.
- [ ] Verify deny-gate.
- [ ] Verify local gate.
- [ ] Verify runner containment audit.
- [ ] Verify classifier.
- [ ] Verify reaper dry-run.
- [ ] Summarize residual risk.

## Task 6: Adaptive Local Gate

**Wave:** 4  
**Blocks:** Task 7, Task 8, Task 5  
**Blocked by:** Task 2

**Files:**
- Create: `~/.claude/bin/local-gate` - foreground resource-aware launcher for heavy push/PR gate commands.
- Create: `~/.claude/local-gate.json` - queue config; default `min_slots=1`, `max_slots=4`.
- Create: `~/.claude/tests/local-gate.test.mjs` - fixture tests for slot calculation, duplicate key handling, and wrapping.
- Create: `~/.claude/docs/free-verification-policy.md` - documents no-paid-CI policy and gate modes.

**Contract:**
- CLI: `local-gate [--key KEY] [--mode targeted|full] [--max-slots N] [--dry-run] -- <command...>`
- Config shape: `{"min_slots":1,"max_slots":4,"load_per_slot":2.0,"min_mem_available_gb":8,"max_swap_used_gb":48,"state_dir":"/home/user/.claude/run/local-gate"}`
- Required behavior: at least `min_slots` heavy gate runs when jobs are queued.
- Required behavior: no more than `max_slots` heavy gates run concurrently.
- Required behavior: scheduler decides effective slots from live load/resources before starting each queued job.
- Required behavior: every queued command executes through `~/.claude/lib/cpu-guard.sh`.
- Required behavior: duplicate active jobs with same `KEY` do not start another copy; duplicate exits 0 and logs `deduped`.
- Required behavior: gate logs write to `~/.claude/local-gate.log`.

**Behavior:**
- Optimize for all-day local work. Do not schedule full verification only for night/idle windows.
- Target push/PR gates first; agents rarely run full builds directly.
- Push/PR gates must call `local-gate`, not raw `pnpm build`, `pnpm test`, `npm test`, `vitest`, `astro build`, `turbo run`, `tsc`, or project gate scripts.
- `targeted` mode runs changed-package or changed-file checks when project supports them.
- `full` mode runs complete local gates through adaptive slots and cgroup wrapping.
- Slot calculation uses current `/proc/loadavg`, CPU count, `/proc/meminfo` `MemAvailable`, swap used, and count of already-running queue jobs.
- If system pressure is high, effective slot count drops toward `min_slots`.
- If system pressure is low and memory is available, effective slot count rises toward `max_slots`.
- Default maximum is 4 concurrent gate slots. Config may lower or raise it after measured clean runs.
- Scheduler never starts a new job when running jobs already meet or exceed current effective slots.
- Scheduler does not kill already-running healthy gate jobs solely because effective slots later shrink. Lower slot count only blocks new starts.
- Duplicate active full gates with same key dedupe. Non-duplicate full gates may run concurrently when effective slots allow.
- No spare-machine, VM, container, or night-only strategy. Same workstation remains primary executor.
- Free external CI is optional only: public-repo free minutes, existing no-cost vendor checks, or trial/free SaaS checks may run if already available, but MUST NOT be required for local progress, branch protection, merge readiness, or final handoff.
- If free external CI quota is exhausted, unavailable, slow, or policy-changed, mark it `opportunistic unavailable` and continue with local queued verification.

**Acceptance:**
- Run two concurrent sample gates with same key.
- Expected: only one starts; the duplicate exits 0 and `~/.claude/local-gate.log` records `deduped`.
- Run four non-duplicate sample gates while system load and memory are low.
- Expected: up to 4 jobs may run concurrently, all through `cpu-guard.sh`.
- Run non-duplicate sample gates while synthetic or real pressure exceeds configured thresholds.
- Expected: new starts throttle down to effective slots, but at least 1 queued job can run.
- Run one sample full gate.
- Expected: process tree enters `build.slice` through `cpu-guard.sh`.
- Review push/PR gate entrypoints.
- Expected: no direct raw full build path remains for push/PR gates that can bypass `local-gate`.

- [x] Implement `local-gate`.
- [x] Add config with `max_slots=4`.
- [x] Add fixture tests.
- [x] Document free verification policy.
- [x] Verify adaptive slot, duplicate, and full-gate behavior.

## Task 7: Enforce Local Gate for Raw Heavy Commands

**Wave:** 5  
**Blocks:** Task 8, Task 5  
**Blocked by:** Task 6

**Files:**
- Modify: `~/.claude/tools.json` - add deny-gate rules for raw full build/test/gate commands.
- Create: `~/.claude/tests/deny-gate-build-rules.test.mjs` - runs `deny-gate.mjs` against sample PreToolUse JSON.

**Contract:**
- Deny raw heavy commands when invoked from Claude Bash: `turbo run`, `pnpm build`, `pnpm test`, `pnpm exec vitest`, `npm run build`, `npm test`, `npx vitest`, `vitest`, `astro build`, `vite build`, `tsc`, `playwright test`, `cargo test`, `cargo build`, `go test`, `go build`.
- Allow wrapped commands containing `/home/user/.claude/bin/local-gate`, `~/.claude/bin/local-gate`, `/home/user/.claude/lib/cpu-guard.sh`, or `~/.claude/lib/cpu-guard.sh`.
- Allow read-only discovery: `command -v`, `which`, `type -a`, `pnpm --version`, `npm --version`, `node --version`.
- Keep `# raw-ok` escape behavior from `deny-gate.mjs`.
- Deny reason MUST name `~/.claude/bin/local-gate --key <key> --mode full -- <command>`.

**Behavior:**
- Fail closed for known heavy raw gate/build signatures.
- Do not block package installs.
- Do not block shell commands that only inspect files/processes.
- Keep manifest valid JSON.

**Acceptance:**
- Run: `node ~/.claude/tests/deny-gate-build-rules.test.mjs`
- Expected: PASS. Raw samples denied; local-gate/cpu-guard/read-only samples allowed.

- [x] Add deny rules.
- [x] Add deny-gate tests.
- [x] Run acceptance check.

## Task 8: Self-Hosted Runner Containment

**Wave:** 6  
**Blocks:** Task 5  
**Blocked by:** Task 6, Task 7

**Files:**
- Create: `/etc/systemd/system/actions.runner.platform-modules-mod.platform-devbox.service.d/resource-control.conf` - systemd override for local GitHub Actions runner.
- Create: `~/.claude/bin/install-actions-runner-resource-control` - sudo installer for exact systemd override when `/etc` is not writable by agent.
- Create: `~/.claude/bin/actions-runner-gate-audit` - reports runner service containment and workflow raw gate risks.
- Create: `~/.claude/tests/actions-runner-gate-audit.test.mjs` - fixture tests for audit parser.

**Contract:**
- CLI: `actions-runner-gate-audit [--json] [--root /home/user/Projects]`
- JSON output: `{service:{name,path,path_has_claude_bin,resource_limits:[...]}, workflows:[{path, raw_commands:[...]}]}`
- Audit raw workflow commands matching `turbo run`, `pnpm build`, `pnpm test`, `npm test`, `vitest`, `astro build`, `vite build`, `tsc`, `playwright test`, and explicit high concurrency flags like `--concurrency=14`.
- systemd override MUST prepend `/home/user/.claude/bin` to `PATH` for runner jobs using exact value:
  `PATH=/home/user/.claude/bin:/home/user/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin`.
- systemd override MUST set resource controls:
  `CPUWeight=40`, `IOWeight=40`, `MemoryHigh=24G`, `MemoryMax=32G`, `TasksMax=512`.
- systemd override MUST set build guard defaults:
  `BUILD_PER_JOB_CPU=200`, `BUILD_MEM_HIGH=12G`, `BUILD_MEM_MAX=16G`, `BUILD_NODE_HEAP=4096`.

**Behavior:**
- Prefer systemd override over editing `/opt/actions-runner-platform/run.sh`.
- After override, runner workflow raw build tools resolve to `~/.claude/bin` shims when PATH lookup is used.
- Audit reports any workflow command that bypasses PATH via absolute tool path or hard-coded high concurrency.
- Do not require GitHub Actions quota. Runner containment is local safety only.

**Acceptance:**
- Run: `node ~/.claude/tests/actions-runner-gate-audit.test.mjs`
- Expected: PASS.
- Run: `~/.claude/bin/actions-runner-gate-audit --json`
- Expected: valid JSON; service status and raw workflow risks are explicit.
- Run: `systemctl cat actions.runner.platform-modules-mod.platform-devbox.service`
- Expected: override contains `/home/user/.claude/bin` in PATH and resource controls.

- [x] Add audit script and tests.
- [x] Add sudo installer for systemd override.
- [ ] Install systemd override live. Blocked here: `/etc/systemd/system` requires sudo password.
- [ ] Reload systemd daemon. Blocked until override installed.
- [x] Run non-root acceptance checks.

## Free-Tier / No-Paid-CI Policy

- Do not depend on GitHub Actions for routine verification. Private-repo free minutes are scarce and can become unavailable within days.
- Do not replace GitHub Actions with another quota-backed required service.
- Default to local queued verification through `local-gate` and `cpu-guard.sh`.
- Build all day when needed. Parallel full gates are allowed only when `local-gate` says live resources can support them.
- Do not rely on night/idle scheduling, spare hardware, VMs, or containers.
- Prefer deterministic local caches: pnpm store, browser cache, build cache where project already supports it.
- Use free external CI only as opportunistic signal. It is not required, not blocking, and not part of branch protection.
