harrisonhjohnson/productagent · /harnesses/loops/init.sh
000%

init.sh

view on github ↗171 lines · bash
#!/bin/bash
# Loops · first-loop setup. Runs at the end of install.sh when a terminal is present,
# or any time by hand:  bash ~/loops/00-ops/init.sh
#
# Asks four things, writes one loop, grants the folder trust the fence needs, dry-runs the
# machine, and tells you what will happen tonight. Reads answers from the terminal device,
# so it works even when the script itself arrives through `curl | bash`.
set -u
LOOPS_HOME="${LOOPS_HOME:-$HOME/loops}"
CHARTER="$LOOPS_HOME/pm/CHARTER.md"; LOOPS="$LOOPS_HOME/pm/LOOPS.md"
RUNNER="$LOOPS_HOME/00-ops/night/run-night.sh"

# ---- terminal ------------------------------------------------------------
if ! { : </dev/tty; } 2>/dev/null; then
  echo "Loops: no terminal to ask questions on. Write a loop by hand in $LOOPS and run:"
  echo "  NIGHT_PROBE=1 LOOPS_HOME=$LOOPS_HOME bash $RUNNER"
  exit 0
fi
[ -f "$CHARTER" ] && [ -f "$LOOPS" ] && [ -f "$RUNNER" ] || { echo "Loops: nothing installed at $LOOPS_HOME. Run the installer first."; exit 1; }

B=$'\033[1m'; D=$'\033[2m'; R=$'\033[0m'
say()  { printf '%s\n' "$*" >/dev/tty; }
ask()  { # var prompt default
  local v="$1" p="$2" d="${3:-}" a
  if [ -n "$d" ]; then printf '%s %s[%s]%s ' "$p" "$D" "$d" "$R" >/dev/tty; else printf '%s ' "$p" >/dev/tty; fi
  IFS= read -r a </dev/tty; a="${a:-$d}"; printf -v "$v" '%s' "$a"
}
setdial() { # key value  — replace or append a "- key: value" line in the charter
  if grep -qE "^- $1:" "$CHARTER"; then
    python3 - "$CHARTER" "$1" "$2" <<'PY'
import re,sys; p,k,v=sys.argv[1:4]; s=open(p).read()
s=re.sub(r"^- %s:.*$"%re.escape(k), "- %s: %s"%(k,v), s, count=1, flags=re.M); open(p,'w').write(s)
PY
  else printf -- '- %s: %s\n' "$1" "$2" >>"$CHARTER"; fi
}

say ""
say "${B}Loops · your first loop${R}"
say "${D}Four questions. One file gets written. Nothing runs until tonight, and only if the laptop is plugged in.${R}"
say ""

# ---- 1. which agent ------------------------------------------------------
HAVE_CLAUDE=0; HAVE_CODEX=0
command -v claude >/dev/null && HAVE_CLAUDE=1
command -v codex  >/dev/null && HAVE_CODEX=1
if [ $HAVE_CLAUDE = 1 ] && [ $HAVE_CODEX = 1 ]; then
  say "${B}1 · Which agent runs the night?${R}  ${D}both are installed${R}"
  say "   1) Claude Code   ${D}per-path fence; can read the web${R}"
  say "   2) Codex         ${D}sandbox with the network off; harder no-push, no web${R}"
  ask A "   choose" 1; case "$A" in 2|codex) AGENT=codex ;; *) AGENT=claude ;; esac
elif [ $HAVE_CODEX = 1 ]; then AGENT=codex; say "${B}1 · Agent:${R} Codex ${D}(the only one on PATH)${R}"
else AGENT=claude; say "${B}1 · Agent:${R} Claude Code"; [ $HAVE_CLAUDE = 1 ] || say "   ${D}claude is not on PATH yet. Install Claude Code before tonight.${R}"; fi
say ""

# ---- 2. the goal ---------------------------------------------------------
S_GOAL=(
  "Watch three competitors' changelogs. Tell me what changed and whether it matters."
  "Ship one small fix from the backlog. Verify it renders before you report it."
  "Read yesterday's support tickets. Cluster them. Name the top three themes."
  "Check every box on the launch checklist against the repo. Flag the ones that lie."
  "Draft the weekly update from this week's reports. I edit. I send."
  "Find ten companies that look like our best customer. Say why, with links."
)
S_TITLE=("Competitor changelogs" "One backlog fix a night" "Support ticket themes" "Launch checklist audit" "Weekly update draft" "Lookalike companies")
S_CAD=(weekly every-2nd-night nightly every-2nd-night weekly every-3rd-night)
S_BRAIN=(fast deep fast fast deep fast)
S_BUD=(2 6 3 2 4 3)
say "${B}2 · What should it work on?${R}  ${D}pick a number, or type your own goal in one sentence${R}"
for i in "${!S_GOAL[@]}"; do say "   $((i+1))) ${S_GOAL[$i]}"; done
ask G "   goal" 2
if [[ "$G" =~ ^[1-6]$ ]]; then i=$((G-1)); GOAL="${S_GOAL[$i]}"; TITLE="${S_TITLE[$i]}"; DCAD="${S_CAD[$i]}"; DBRAIN="${S_BRAIN[$i]}"; DBUD="${S_BUD[$i]}"
else GOAL="$G"; TITLE="$(python3 -c "import sys; t=sys.argv[1].rstrip('.'); print(t if len(t)<=48 else t[:48].rsplit(' ',1)[0])" "$G")"; DCAD=every-2nd-night; DBRAIN=fast; DBUD=3; fi
say ""

# ---- 3. cadence + brain --------------------------------------------------
say "${B}3 · How often, and how much brain?${R}"
say "   cadence: nightly · every-2nd-night · every-3rd-night · weekly"
ask CAD "   cadence" "$DCAD"
case "$CAD" in nightly|every-2nd-night|every-3rd-night|weekly) ;; *) say "   ${D}unknown cadence, using $DCAD${R}"; CAD="$DCAD" ;; esac
if [ "$AGENT" = claude ]; then
  say "   brain: fast (Sonnet) · deep (Opus) · frontier (Fable)"
  ask BRAIN "   brain" "$DBRAIN"
  case "$BRAIN" in deep|opus) MODEL=claude-opus-5 ;; frontier|fable) MODEL=claude-fable-5-1 ;; claude-*) MODEL="$BRAIN" ;; *) MODEL=claude-sonnet-5 ;; esac
else
  CFG_MODEL="$(sed -n 's/^model *= *"\(.*\)"/\1/p' "$HOME/.codex/config.toml" 2>/dev/null | head -1)"
  say "   brain: a Codex model id ${D}(your Codex default is ${CFG_MODEL:-unset})${R}"
  ask BRAIN "   model" "${CFG_MODEL:-gpt-5.5}"; MODEL="$BRAIN"
fi
say ""

# ---- 4. budget, with the math shown --------------------------------------
say "${B}4 · What may it spend?${R}  ${D}dollars per run; the loop stops itself at the total${R}"
ask PER "   per run" "$DBUD"
[[ "$PER" =~ ^[0-9]+([.][0-9]+)?$ ]] || PER="$DBUD"
DTOT="$(python3 -c "print(int(round(float('$PER')*8)))")"
ask TOT "   total for this loop" "$DTOT"
[[ "$TOT" =~ ^[0-9]+([.][0-9]+)?$ ]] || TOT="$DTOT"
case "$CAD" in nightly) PM=30 ;; every-2nd-night) PM=15 ;; every-3rd-night) PM=10 ;; weekly) PM=4 ;; esac
NIGHT_CAP="$(sed -n 's/^- cost_cap_per_night_usd: *\([0-9.]*\).*/\1/p' "$CHARTER" | head -1)"; NIGHT_CAP="${NIGHT_CAP:-15}"
RUNS="$(python3 -c "import math; print(int(float('$TOT')//float('$PER')))")"
say "   ${D}at most \$$(python3 -c "v=float('$PER')*$PM; print(int(v) if v==int(v) else round(v,2))") a month at this cadence; this loop ends after $RUNS runs (\$$TOT); the night cap in the charter is \$$NIGHT_CAP${R}"
if [ "$AGENT" = claude ] && [ -f "$LOOPS_HOME/00-ops/night/quota.py" ]; then
  EST="$(LOOPS_HOME="$LOOPS_HOME" python3 "$LOOPS_HOME/00-ops/night/quota.py" estimate --usd "$PER" --model "$MODEL" 2>/dev/null)"
  say "   ${D}$(python3 -c "
import json,sys; e=json.loads(sys.argv[1] or '{}'); pm=$PM
if e.get('pct') is not None: print(f\"≈ {e['pct']:g}% of your Claude week per run, ≈ {min(100,round(e['pct']*pm/4.3,1)):g}% a week at this cadence ({e['basis']})\")
else: print(f\"share of your Claude week per run: {e.get('basis','unknown')}. The machine learns it from your own runs.\")
" "$EST")${R}"
fi
if python3 -c "import sys; sys.exit(0 if float('$PER')>float('$NIGHT_CAP') else 1)"; then
  say "   ${D}per-run is above the night cap, so the runner would skip it. Raising the night cap to \$$PER.${R}"; setdial cost_cap_per_night_usd "$PER"
fi
REVIEW="$(date -v+45d +%F 2>/dev/null || python3 -c "import datetime;print((datetime.date.today()+datetime.timedelta(days=45)).isoformat())")"
say ""

# ---- write ---------------------------------------------------------------
ID="$(python3 - "$LOOPS" <<'PY'
import re,sys; s=re.sub(r"<!--.*?-->","",open(sys.argv[1]).read(),flags=re.S)
n=max([int(m) for m in re.findall(r"^## L-(\d+)",s,re.M)]+[0])+1; print("L-%02d"%n)
PY
)"
cat >>"$LOOPS" <<EOF

## $ID — $TITLE
- status: active
- goal: $GOAL
- cadence: $CAD
- model: $MODEL
- budget_per_iteration_usd: $PER
- budget_loop_total_usd: $TOT
- review_by: $REVIEW
- scope: work only inside this folder, on branch \`night/<date>\`. Never push, never deploy,
  never touch \`main\`, never add a dependency. Write findings to \`pm/nights/loops/$ID.md\`.
- definition_of_done_per_iteration: a dated section appended to \`pm/nights/loops/$ID.md\`
  ending with \`next:\` and \`progress: yes|no\`, and the four lines in the night report.
- if_blocked: say so plainly in the state file and stop; never guess.
EOF
setdial agent "$AGENT"; setdial model "$MODEL"
say "${B}Written${R}  $LOOPS  ${D}($ID, review by $REVIEW)${R}"

# ---- trust (Claude Code ignores the fence's allow rules in an untrusted folder) ----
if [ "$AGENT" = claude ]; then
  python3 - "$LOOPS_HOME" <<'PY' && say "${B}Trusted${R}  $LOOPS_HOME in ~/.claude.json  ${D}(so the fence's allow rules apply)${R}"
import json,os,sys
p=os.path.expanduser('~/.claude.json'); home=sys.argv[1]
try: d=json.load(open(p))
except FileNotFoundError: d={}
except ValueError: sys.exit(1)
d.setdefault('projects',{}).setdefault(home,{})['hasTrustDialogAccepted']=True
tmp=p+'.tmp'; json.dump(d,open(tmp,'w'),indent=2); os.replace(tmp,p)
PY
fi

# ---- dry run -------------------------------------------------------------
say ""
say "${B}Dry run${R}  ${D}every guard, no model call, no spend${R}"
STATE="$LOOPS_HOME/00-ops/night/run-state.json"; rm -f "$STATE"
NIGHT_PROBE=1 NIGHT_POWER_OVERRIDE=ac LOOPS_HOME="$LOOPS_HOME" bash "$RUNNER" >/dev/null 2>&1
NOTE="$(python3 -c "import json;print(json.load(open('$STATE')).get('note',''))" 2>/dev/null)"
case "$NOTE" in
  *"loops=$ID"*) say "   tonight the machine would run $ID. ✓" ;;
  *) say "   the machine would NOT run $ID tonight: ${NOTE:-no state written}"; say "   ${D}see $LOOPS_HOME/00-ops/night/logs/$(date +%F).log${R}" ;;
esac
rm -f "$STATE"   # a probe must never block the real run

# ---- what happens next ---------------------------------------------------
say ""
say "${B}Tonight${R}   plug the laptop in. Lid open or shut. The checker looks every ten minutes and takes the first window."
say "${B}Morning${R}   read $LOOPS_HOME/pm/nights/$(date +%F).md  ${D}four lines per loop: trying · did · decided · need from you${R}"
say "${B}Any time${R}  edit $LOOPS · pause with 'paused: yes' in the charter · stop the schedule: bash $LOOPS_HOME/00-ops/install.sh --off"
say ""
flow-designflow-designwhat this folder doesA progressive design interview: one question per screen, ending in a clickable HTML prototype of a linear user journey (onboarding, checkout, wizard).global
SKILL.md74 lines
loopsloopswhat this folder doesGive an agent a goal, a budget, a cadence and a model. It works one bounded run at a time while you sleep, inside a fence, and hands you four plain sentences in the morning.system
README.md178 lines
init.sh171 lines
install.sh144 lines
loops-spec.md244 lines
MACHINE.md161 lines
settings.json116 lines
prototype-swarmprototype-swarmwhat this folder doesCrawls a multi-screen HTML prototype for undefined click destinations and spawns one generator agent per missing screen, with a handoff packet written by the source screen, until the click graph is covered.global
SKILL.md162 lines
self-maintenanceself-maintenancewhat this folder doesThe one way an unattended run may touch its own loop registry, and the one place a "need from you" becomes a decision you settle with a single command instead of a paste.system
README.md126 lines
loopctl.py599 lines
loops_registry.py122 lines
pm-strategistpm-strategistwhat this folder doesA product and strategy advisor agent that blends three lenses (build taste, business structure, PM execution) and pressure-tests ideas instead of cheerleading.global
pm-strategist.md51 lines
todotodowhat this folder doesA TODO list for PMs in one markdown file, reachable from Claude Code and from Telegram: capture, see what's due, triage, and hand items to an agent that asks before it acts.system
README.md59 lines
README.md68 lines