[object Object]

← back to Lifestyle Asset Intel

yolo tick #15: launchd-driven overnight autonomous loop

db84b94b7fb43d0ea94881bec3cf096900e115c7 · 2026-05-10 07:21:00 -0700 · Steve Abrams

Steve is going to sleep and wants the lifestyle-asset-intel build
running autonomously overnight. ScheduleWakeup-based loop dies when
the active Claude Code session closes, so set up a more durable
launchd-driven path that fires every 30 minutes regardless of
session state.

Files (all under .yolo/):
  tick-prompt.md   — self-contained instructions for each fire:
                     working dir, PG env, pm2 process, standing-rule
                     guardrails (no DNS, no email, no eBay scrape,
                     local-only), candidate next-tick items, hard
                     exit conditions ("≥8 turns elapsed → wrap up").
  run-tick.sh      — shell wrapper. mkdir-atomic lock (no flock on
                     macOS), 12-minute stale-lock recovery, claude
                     CLI invoke with --print --dangerously-skip-
                     permissions --max-turns 12, append to log.
                     Unsets ANTHROPIC_API_KEY/AUTH_TOKEN per Steve's
                     standing rule (Max plan only).
  README.md        — operator runbook: where the plist lives, how
                     to pause/resume/tail, why launchd over
                     ScheduleWakeup, safety knobs.

LaunchAgent: ~/Library/LaunchAgents/com.steve.lai-yolo.plist (not in
this repo — per-machine config). StartCalendarInterval = :15 and :45
of every hour. WorkingDirectory pinned to project root. PATH includes
/Users/stevestudio2/.local/bin so the claude binary resolves
correctly.

.gitignore extended to keep .yolo/yolo-loop.log,
.yolo/launchd-{out,err}.log, and .yolo/.lock.d/ out of the repo;
scripts and prompt stay tracked.

Validated: lock acquires + refuses + releases correctly; launchctl
list shows the entry loaded with LastExitStatus 0; next fire 28
minutes from this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit db84b94b7fb43d0ea94881bec3cf096900e115c7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun May 10 07:21:00 2026 -0700

    yolo tick #15: launchd-driven overnight autonomous loop
    
    Steve is going to sleep and wants the lifestyle-asset-intel build
    running autonomously overnight. ScheduleWakeup-based loop dies when
    the active Claude Code session closes, so set up a more durable
    launchd-driven path that fires every 30 minutes regardless of
    session state.
    
    Files (all under .yolo/):
      tick-prompt.md   — self-contained instructions for each fire:
                         working dir, PG env, pm2 process, standing-rule
                         guardrails (no DNS, no email, no eBay scrape,
                         local-only), candidate next-tick items, hard
                         exit conditions ("≥8 turns elapsed → wrap up").
      run-tick.sh      — shell wrapper. mkdir-atomic lock (no flock on
                         macOS), 12-minute stale-lock recovery, claude
                         CLI invoke with --print --dangerously-skip-
                         permissions --max-turns 12, append to log.
                         Unsets ANTHROPIC_API_KEY/AUTH_TOKEN per Steve's
                         standing rule (Max plan only).
      README.md        — operator runbook: where the plist lives, how
                         to pause/resume/tail, why launchd over
                         ScheduleWakeup, safety knobs.
    
    LaunchAgent: ~/Library/LaunchAgents/com.steve.lai-yolo.plist (not in
    this repo — per-machine config). StartCalendarInterval = :15 and :45
    of every hour. WorkingDirectory pinned to project root. PATH includes
    /Users/stevestudio2/.local/bin so the claude binary resolves
    correctly.
    
    .gitignore extended to keep .yolo/yolo-loop.log,
    .yolo/launchd-{out,err}.log, and .yolo/.lock.d/ out of the repo;
    scripts and prompt stay tracked.
    
    Validated: lock acquires + refuses + releases correctly; launchctl
    list shows the entry loaded with LastExitStatus 0; next fire 28
    minutes from this commit.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 .gitignore           |  7 ++++++
 .yolo/README.md      | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 .yolo/run-tick.sh    | 45 +++++++++++++++++++++++++++++++++++++
 .yolo/tick-prompt.md | 53 +++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 168 insertions(+)

diff --git a/.gitignore b/.gitignore
index e703288..111b69d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -12,3 +12,10 @@ build/
 tmp/
 uploads/
 coverage/
+
+# yolo loop runtime artifacts — keep the scripts + prompt, ignore logs/locks
+.yolo/yolo-loop.log
+.yolo/launchd-out.log
+.yolo/launchd-err.log
+.yolo/.lock
+.yolo/.lock.d/
diff --git a/.yolo/README.md b/.yolo/README.md
new file mode 100644
index 0000000..87893f1
--- /dev/null
+++ b/.yolo/README.md
@@ -0,0 +1,63 @@
+# `.yolo/` — overnight autonomous loop
+
+## What this is
+
+A launchd-driven autonomous yolo loop that fires every 30 minutes and runs
+ONE small focused tick on this project. Each tick is an independent
+`claude --print` session — no memory of prior fires; reads `git log` for
+history.
+
+## Files
+
+| Path | Purpose | Tracked |
+|---|---|---|
+| `tick-prompt.md` | What each fire should do — standing rules, candidate tasks, exit conditions | yes |
+| `run-tick.sh` | Shell wrapper: lock + claude CLI + log append | yes |
+| `yolo-loop.log` | Per-fire stdout+stderr + start/end markers | NO (gitignored) |
+| `launchd-out.log` / `launchd-err.log` | launchd-level stdout/err | NO |
+| `.lock.d/` | mkdir-atomic single-fire lock; auto-stolen after 12 min | NO |
+
+## launchd plist
+
+`~/Library/LaunchAgents/com.steve.lai-yolo.plist` (NOT in this repo;
+~/Library is per-machine config).
+
+Fires on `StartCalendarInterval` at minute `15` and `45` of every hour.
+Default cooldown between fires = 30 minutes.
+
+## Operate
+
+```bash
+# Status
+launchctl list com.steve.lai-yolo
+
+# Pause for the night
+launchctl unload ~/Library/LaunchAgents/com.steve.lai-yolo.plist
+
+# Resume
+launchctl load ~/Library/LaunchAgents/com.steve.lai-yolo.plist
+
+# Watch the log live
+tail -f ~/Projects/lifestyle-asset-intel/.yolo/yolo-loop.log
+
+# Force one tick now (respects the lock)
+~/Projects/lifestyle-asset-intel/.yolo/run-tick.sh
+```
+
+## Why launchd over ScheduleWakeup
+
+`ScheduleWakeup` requires the active Claude Code session to be alive —
+closing the terminal kills the loop. launchd survives session close,
+machine reboot (via the LaunchAgent registry), and any pm2 restart.
+Mac sleep still pauses launchd; use `caffeinate -di` if you need
+literal-overnight uninterrupted runs.
+
+## Safety
+
+- `--max-turns 12` per fire caps each tick's compute
+- `unset ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN` per Steve's standing rule
+  (Claude Max plan only, never the metered API)
+- Lock prevents two fires from racing on git
+- Each tick exits before the next fire window so commits don't pile up
+- `tick-prompt.md` enumerates the standing-rule guardrails (no DNS,
+  no email, no scraping non-public sources, etc.)
diff --git a/.yolo/run-tick.sh b/.yolo/run-tick.sh
new file mode 100755
index 0000000..40407e4
--- /dev/null
+++ b/.yolo/run-tick.sh
@@ -0,0 +1,45 @@
+#!/bin/bash
+# LAI yolo tick runner — fired by launchd every 30 minutes.
+# Locks via flock so overlapping fires don't race on git.
+set -u
+
+PROJ="/Users/stevestudio2/Projects/lifestyle-asset-intel"
+LOCK="$PROJ/.yolo/.lock"
+LOG="$PROJ/.yolo/yolo-loop.log"
+PROMPT="$PROJ/.yolo/tick-prompt.md"
+
+cd "$PROJ" || { echo "[$(date -u +%FT%TZ)] cwd FAIL" >> "$LOG"; exit 2; }
+
+# Standing rule: NEVER use Anthropic API key — Claude CLI uses Max plan only.
+unset ANTHROPIC_API_KEY ANTHROPIC_AUTH_TOKEN
+
+# Single-fire lock — bail if a previous tick is still running.
+# macOS has no flock, so we use a mkdir-as-lock pattern (atomic on POSIX).
+LOCK_DIR="$PROJ/.yolo/.lock.d"
+if ! mkdir "$LOCK_DIR" 2>/dev/null; then
+  # Stale lock recovery: if the dir is older than 12 minutes (max-turns 12 +
+  # margin), assume the previous tick is dead and steal the lock.
+  if [ "$(find "$LOCK_DIR" -maxdepth 0 -mmin +12 2>/dev/null | wc -l)" -gt 0 ]; then
+    echo "[$(date -u +%FT%TZ)] STALE lock → steal" >> "$LOG"
+    rmdir "$LOCK_DIR" 2>/dev/null
+    mkdir "$LOCK_DIR" || { echo "[$(date -u +%FT%TZ)] lock steal FAIL" >> "$LOG"; exit 0; }
+  else
+    echo "[$(date -u +%FT%TZ)] SKIP — previous tick still running" >> "$LOG"
+    exit 0
+  fi
+fi
+trap 'rmdir "$LOCK_DIR" 2>/dev/null' EXIT
+
+START=$(date -u +%FT%TZ)
+echo "[$START] tick start" >> "$LOG"
+
+# Pipe the prompt to claude CLI, --print so it exits, --max-turns 12 to cap cost
+/Users/stevestudio2/.local/bin/claude --print --dangerously-skip-permissions --max-turns 12 < "$PROMPT" \
+  >> "$LOG" 2>&1
+EXIT=$?
+
+END=$(date -u +%FT%TZ)
+echo "[$END] tick end exit=$EXIT" >> "$LOG"
+echo "---" >> "$LOG"
+
+exit "$EXIT"
diff --git a/.yolo/tick-prompt.md b/.yolo/tick-prompt.md
new file mode 100644
index 0000000..668eb0b
--- /dev/null
+++ b/.yolo/tick-prompt.md
@@ -0,0 +1,53 @@
+# LAI yolo tick — one focused change, then exit
+
+You are firing inside an autonomous launchd loop on Mac2. Each fire is an
+independent Claude CLI session with NO memory of previous ticks — read the
+git log for history.
+
+WORK DIR: /Users/stevestudio2/Projects/lifestyle-asset-intel
+PG:        PGHOST=/tmp PGDATABASE=lifestyle_asset_intel
+PM2:       lai-server on :9820 (restart with `pm2 restart lai-server`)
+TESTS:     `PGDATABASE=lifestyle_asset_intel npm test`  (currently 44/44 green)
+
+## Standing rules (read MEMORY.md if any doubt)
+
+- Local-only ops. NO DNS, NO email, NO scraping eBay (TOS gated), NO outbound
+  comms, NO touching dw_unified or any non-LAI repo.
+- Always stay legal for scraping — auction houses (Sotheby's/Christie's
+  editorial) are public records and OK; aggregators need TOS check first.
+- Author email steve@designerwallcoverings.com on every commit.
+- `git commit` only if `npm test` passes. One logical change = one commit.
+- Reversible only — no `git push --force`, no destructive ops.
+- Per Steve's "build and look anywhere for data" + "always stay legal":
+  Tier-1 auction house editorial > public datasets > nothing.
+
+## What to do this tick
+
+1. `git log --oneline | head -20` — see what's been done. Don't repeat.
+2. Read ROADMAP.md briefly. Pick ONE achievable item.
+3. Implement. Test. If `npm test` green, commit. Else revert and pick a
+   smaller scope.
+4. Restart pm2 if runtime code changed. Don't restart if only docs/SQL.
+5. Write a one-line status to `.yolo/yolo-loop.log` then exit.
+
+## Reasonable next items (pick whichever fits a single tick)
+
+- Christie's editorial scrape — mirror tick #13 pattern, more comps, new source
+- Phillips / Bonhams / Heritage Auctions — same pattern
+- Asset detail "exotic" badge (1-line CSS + EJS conditional)
+- /api/family (no slug) — list all families
+- Server-side sort param on /api/assets (mirrors q param)
+- Condition_facets render on asset detail page
+- Sparkline Y-axis labels
+- Source.weight-aware similarity weight (replace constant 0.18)
+- Add an /admin/audit HTML view over /api/audit/recent
+- Methodology version bump trigger when breakdown weights change
+
+## Exit conditions
+
+- Tests fail and you can't fix in ≤6 turns → revert, log, exit.
+- Project in broken state → fix-first, no new features.
+- ≥8 turns elapsed → wrap up, commit if green, exit.
+- Done with the tick → exit cleanly so launchd can fire again next interval.
+
+KEEP IT SMALL. Many small ticks > one large stuck tick.

← 012be8e yolo tick #14: auto-generate valuation_snapshots from real c  ·  back to Lifestyle Asset Intel  ·  yolo tick #16: exotic / special-edition badges + audit-test 447537f →