The machine behind Loops
This is the engineering page. For what a loop is and how to use one, start at README.md. A captain's night order book, for a portfolio of small ventures run by one person and one coding agent: judgment written down at dusk, executed by the watch overnight, the captain woken only under named conditions.
This is the machine that lets Claude Code work unattended on ~/ventures while its
operator sleeps, travels, or is simply away from the laptop — without ever being able to
spend money, push code, widen its own permissions, or rewrite its own orders. It was built
by the operator with Claude over about a month of nightly runs; every rule below was
promoted from something that went wrong first.
The five laws
- Separate judgment from labor in time. An order is frozen judgment; the meeting is where judgment lives, the night is where labor lives.
- Trust is structural, not behavioral. Arrange the world so the bad thing is impossible or absent; a branch is a fact you can diff, a rule is a hope.
- Grade evidence, never claims. Reports where lying is detectable; the wrapper's envelope, not the model, is the authority on time and cost.
- The alarm never lives inside the thing it watches. A system cannot report its own absence.
- Learnings compound only at the point of use, and recurring ones get promoted until they become physics.
The six pieces
| Piece | Files | What it does |
|---|---|---|
| The fence | settings.json |
Claude Code project permissions: an explicit allowlist scoped to ~/ventures, read-only git verbs, no shells, no rm, no push, no MCP auth. Deny rules keep the run out of its own charter, orders, loops, ops tooling, and every dotfile. |
| The captain's book | pm/CHARTER.md, pm/LOOPS.md (plus ORDERS.md and LOG.md, not shipped) |
Human-edited only. The charter holds the dials (model, effort, budget caps, WIP limit) and the blast radius. Loops are standing orders that renew themselves nightly under a budget and a TTL. |
| The night runner | ops/night/run-night.sh, night-prompt.md, night-mcp.json |
The wrapper: reads dials from the charter, skips if nothing is runnable, preflights the cost ledger, runs an auth canary, snapshots nested repos onto night/<date> branches, invokes claude -p with zero MCP servers and a hard timeout, persists the envelope, commits the tree, appends the ledger row, writes an alert file on anything but ok. |
| Capture and watchdog | ops/capture/poller.sh, watchdog-absence.sh |
Fires every 10 minutes while the machine is awake; runs a lane at the first window that fits its physics (night needs AC power; day accepts lid-open battery ≥ 50%). The watchdog alarms only when a window existed and nothing ran, never on calendar silence. |
| Health desk | ops/health/fleet-health.py, judge-night.sh, judge-night.md |
Morning ground truth. Deterministic trajectory scorers over the session transcripts the run cannot edit (step budget, repeat loops, denied-then-retried, protected-path writes, git mutations), plus an LLM judge that grades the night's report against the diff of what it actually wrote. |
| The disciplines | skills/{pm,brand,growth,finance,launch-business} |
Five Claude Code skills. pm is the chief of staff and runs the standup; the other four each own one thing (voice and design, the honest signal, the economic verdict, the launch pipeline) and refuse the others'. |
How a night runs
- The poller sees AC power, stamps the window, and invokes the wrapper under
caffeinate. - The wrapper reads the charter dials, counts unchecked
## Tonightorders, and asks a deterministic Python block which loops are runnable (cadence, budget by iteration count, two-consecutive-no-progress auto-park). Nothing runnable means a one-line state file and exit. - Budget preflight against the cost ledger: rolling 7-day and monthly caps, and a pause after two consecutive over-cap nights.
- An auth canary (
Reply with exactly: READY) proves the CLI is logged in before any real spend. - Every nested venture repo gets a pre-night snapshot commit and a
night/<date>branch. claude -pruns the night prompt with--strict-mcp-configagainst an empty MCP config, project-only settings, and a hard timeout. One retry on a non-timeout failure.- The result envelope is persisted to
pm/nights/<date>-envelope.mdbefore anything else can fail. The tree is committed asventures-night. A ledger row is appended. - Anything but
okwritesALERT.mdand fires a macOS notification. The morning standup reads the alert, then the health desk, then the report, then the diff.
Agent adapters
run-night.sh never calls a CLI directly. It reads agent: from the charter (default
claude), sources 00-ops/night/agents/<agent>.sh, and calls three functions:
agent_canary <out> (must leave READY in out), agent_run <out> <promptfile> <model> <effort> <timeout> (must leave a JSON envelope with result and total_cost_usd in
out), and agent_relogin_hint. Everything downstream, the envelope persist, the ledger
row, the NIGHT-BLOCKED check and the health desk, reads that envelope.
claude.sh:claude -p … --output-format json --setting-sources projectwith the empty MCP config. The envelope is Claude Code's own.codex.sh:codex exec --json -o <last-message> --sandbox workspace-writewithsandbox_workspace_write.network_access=falseandapproval_policy="never". The adapter folds the JSONL events into the envelope shape, takesresultfrom the last message, and pricesturn.completedusage with the twocodex_usd_per_m_*dials (cost_is_estimate: true). A failed turn becomes aNIGHT-BLOCKEDresult so the runner recordsblocked, notok.
Known gap: the health desk's transcript scorers read ~/.claude/projects; Codex sessions
live under ~/.codex/sessions and are not scored yet.
Plan quota
00-ops/night/quota.py has four commands. snapshot (called by the poller every tick)
appends a row to quota-ledger.jsonl when cachedUsageUtilization in ~/.claude.json
changes; it accepts both cache shapes the CLI has used (a limits[] list keyed by kind,
or per-window objects) and normalises them to seven_day, five_hour and per-model
seven_day_<model> windows. status returns the freshest snapshot with its age.
estimate --usd X --model M returns the share of the week X dollars is worth, calibrated
as the median points-per-dollar over consecutive snapshot pairs (same reset period, percent
rose, at least one run finished between them) once five pairs exist, else seeded from
plan_weekly_usd_equivalent, else "calibrating". line renders the morning sentence.
The runner gates on status before the auth canary when agent: claude: a cache under
12 hours old with less than plan_floor_percent of the week left, or a five-hour window
over five_hour_max_percent, writes skipped-quota (terminal for the day; the poller and
the health desk treat it as a clean night). Ledger rows now carry finished and model
so calibration can pair them with snapshots. After a run the quota line is appended in
italics to pm/nights/<date>.md if the report exists, and always to the log.
Planner and Decisions
On my machine the planner's decisions file has since been superseded by the self-maintenance lane: the run queues rows it cannot settle, carrying the command or patch, and the morning applies them one by one. The planner below is the earlier design.
plan.py (opt-in with planner: on) loads a structural graph: state × permit-type cells,
feeds and queue from the corpus (PLAN_CORPUS, default $LOOPS_HOME/<NNN>-venture), pending
Decisions from pm/DECISIONS.md, and history from every dated loop section (action:
lines) and prior plan-*.md files. candidates() is the only domain-specific function; it
emits actions with a value, a readiness and a cost and a plain-English why. score() applies
the general rules. plan writes pm/nights/plan-<date>.md and prints the one line the
runner appends to the prompt. simulate --nights N applies each pick’s expected effect in
memory and re-plans, which is how the planner is tested without spending a night.
decisions --from <envelope> appends pending D-NN entries for new "Need from you" lines;
the runner calls it after every run. rank_model() calls claude -p with a JSON schema on
planner_model, tools off, and folds the result under the rules (a zeroed candidate stays
zeroed). retrieve() runs karma’s retrieve_top_seeds inside karma’s own venv against
~/.karma, read-only, and returns [] on any failure so the planner never depends on it.
outcome --date --cost appends to pm/nights/outcomes.jsonl; --both on plan runs
both scorers regardless of the dial, for comparison.
launchd wiring (plists not shipped)
Two user LaunchAgents, both RunAtLoad:
com.ventures.capture: runsops/capture/poller.shwithStartInterval 600.com.ventures.capture-watchdog: runsops/capture/watchdog-absence.shwithStartInterval 3600.
The wrapper itself has no schedule. Opportunistic capture replaced fixed-time firing after fixed times kept landing on a closed laptop.
Adopting it
install.sh in this folder is what curl -fsSL https://productagent.dev/install.sh | bash
runs. It lays the folder out as $LOOPS_HOME/{.claude/settings.json, 00-ops/{night,capture,health}, pm/}
(default ~/loops), fills /Users/YOU and the ventures path into the fence, copies the
templates in pm/templates/ as an empty LOOPS.md and ORDERS.md, initialises a git
repo for the runner to commit into, writes and loads the two LaunchAgents below with
LOOPS_HOME in their environment, and, when a terminal is present, hands off to
init.sh. That script reads answers from /dev/tty (so it works through curl | bash),
appends one ## L-NN section to pm/LOOPS.md, sets agent: and model: in the charter,
raises cost_cap_per_night_usd if the per-run budget exceeds it, sets
projects[<folder>].hasTrustDialogAccepted in ~/.claude.json for Claude Code, runs the
runner with NIGHT_PROBE=1, and reports the probe's note. Without a terminal it prints the
steps instead. Every script reads LOOPS_HOME and falls back to
~/ventures. Re-running it refreshes the machine and never overwrites the charter, loops,
orders or fence you have edited.
By hand, the same steps are:
- Copy this folder's contents so the layout above holds.
- Replace
/Users/YOUinsettings.jsonwith your home directory. Claude Code permission paths are absolute. - Edit
pm/CHARTER.md: presence mode, shadow rate, budget caps. Writepm/ORDERS.mdwith a## Tonightsection of- [ ]orders, each with Definition of done, Out of scope, and If blocked. - Optional env:
NIGHT_GIT_NAME/NIGHT_GIT_EMAILfor snapshot commits,CLAUDE_BINif the CLI is not on PATH,JUDGE_MODELfor the health desk. - Run
NIGHT_PROBE=1 bash 00-ops/night/run-night.shfirst. It exercises every guard and stops before invoking the model. - Load the two LaunchAgents. Read
00-ops/health/REPORT.mdthe next morning.
Everything in here was written with Claude Code; the judgment about what to fence and why is the operator's.