From 0b066de1ccb05973053394873f72f0d3a1d4df84 Mon Sep 17 00:00:00 2001 From: Alexander Whitestone Date: Sat, 14 Mar 2026 18:00:32 -0400 Subject: [PATCH] feat: add timmy loop infrastructure + config updates Loop v2: timmy-loop.sh (20min timeout, claim TTL, cleanup, Timmy triage/review) Status panel: timmy-status.sh (8s refresh, Ollama/dashboard/issues/system) Prompt: timmy-loop-prompt.md (2.6KB, down from 6.2KB) tmux layout: timmy-tmux.sh Watchdog: timmy-watchdog.sh Config: fallback_model chain (kimi-k2.5 -> local qwen3:30b) custom_providers updated to qwen3:30b --- .gitignore | 5 + bin/timmy-loop-prompt.md | 54 ++++++++ bin/timmy-loop.sh | 215 +++++++++++++++++++++++++++++++ bin/timmy-status.sh | 267 +++++++++++++++++++++++++++++++++++++++ bin/timmy-tmux.sh | 63 +++++++++ bin/timmy-watchdog.sh | 39 ++++++ channel_directory.json | 2 +- config.yaml | 26 +--- cron/jobs.json | 25 +++- memories/MEMORY.md | 2 +- 10 files changed, 676 insertions(+), 22 deletions(-) create mode 100644 bin/timmy-loop-prompt.md create mode 100755 bin/timmy-loop.sh create mode 100755 bin/timmy-status.sh create mode 100755 bin/timmy-tmux.sh create mode 100755 bin/timmy-watchdog.sh diff --git a/.gitignore b/.gitignore index 1abaf0b..11e2ca0 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,11 @@ cron/output/ # ── Binaries (except our scripts) ──────────────────────────────────── bin/* !bin/hermes-sync +!bin/timmy-loop.sh +!bin/timmy-loop-prompt.md +!bin/timmy-status.sh +!bin/timmy-tmux.sh +!bin/timmy-watchdog.sh # ── OS junk ────────────────────────────────────────────────────────── .DS_Store diff --git a/bin/timmy-loop-prompt.md b/bin/timmy-loop-prompt.md new file mode 100644 index 0000000..62d4db9 --- /dev/null +++ b/bin/timmy-loop-prompt.md @@ -0,0 +1,54 @@ +You are the Timmy development loop orchestrator. One issue per cycle. Be efficient. + +REPO: ~/Timmy-Time-dashboard +API: http://localhost:3000/api/v1/repos/rockachopa/Timmy-time-dashboard +GITEA TOKEN: ~/.hermes/gitea_token (hermes user — NOT ~/.config/gitea/token) +STATE: ~/Timmy-Time-dashboard/.loop/state.json +CLAIMS: ~/Timmy-Time-dashboard/.loop/claims.json + +RULES: +- Lines of code is a liability. Delete as much as you create. +- Merge or revert. Main is always deployable. Never leave it broken. +- The soul is the spec. When issues run dry, read SOUL.md and find gaps. +- Building sovereign Timmy is the north star. +- ALWAYS clean up worktrees after merge: git worktree remove /tmp/timmy-cycle-N +- ALWAYS release claims when done: hermes-claim drop +- Run tests ONCE, not multiple times. One pass is enough: .venv/bin/python -m pytest tests/ -x -q --timeout=30 + +DELEGATION — KIMI vs YOU: +Kimi (kimi-k2.5, 262K context) is your coding engine. Your Anthropic tokens are expensive. + kimi --print --yolo -p "YOUR PRECISE PROMPT" -w /path/to/worktree + +DELEGATE TO KIMI: Feature implementation, bulk code changes, refactors, new subsystems. +DO YOURSELF: Planning, reading code, small fixes, PR/issue management, reviewing Kimi's output. +KIMI AVOID: CI/pyproject.toml, cloud calls, removing tests. + +Kimi prompts must include: exact file paths, current code context, clear spec, test command. +Test command: cd WORKTREE && ~/Timmy-Time-dashboard/.venv/bin/python -m pytest tests/ -x -q --timeout=30 + +YOUR CYCLE: +1. Read state.json and claims.json +2. Fetch open issues from Gitea API +3. Pick highest-value UNCLAIMED issue you can finish in time +4. Claim it: hermes-claim take +5. Create worktree: git worktree add /tmp/timmy-cycle-N fix/description +6. Read relevant code, delegate to Kimi or fix directly +7. Run tests ONCE. If pass: push, create PR, merge, close issue. +8. If fail: fix or revert. Do not retry endlessly. +9. Clean up: git worktree remove, hermes-claim drop +10. Update state.json (append to arrays, don't replace) +11. If no issues left: read SOUL.md, file new issues for gaps + +TIMMY INTEGRATION: +Timmy is your teammate, not just a codebase. Before fixing his code, ask him: + .venv/bin/timmy chat --session-id loop "your question" +Timeout after 30s if he hangs. Use --session-id loop always (not default session). +Log observations in state.json under timmy_gaps and timmy_strengths. + +IMPORTANT: +- Tag PRs: [loop-cycle-N] in title +- Tag new issues: [loop-generated] +- ONE issue per cycle. Do it well. +- Do NOT run pre-push hooks separately — tests already ran. + +Do your work now. diff --git a/bin/timmy-loop.sh b/bin/timmy-loop.sh new file mode 100755 index 0000000..f39252a --- /dev/null +++ b/bin/timmy-loop.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +# ── Timmy Development Loop v2 ────────────────────────────────────────── +# Runs forever. Each cycle: Timmy triages → Hermes picks work → execute +# → Timmy reviews → merge. State in .loop/state.json. +# ─────────────────────────────────────────────────────────────────────── + +set -uo pipefail + +export PATH="$HOME/.local/bin:$HOME/.hermes/bin:/usr/local/bin:$PATH" + +REPO="$HOME/Timmy-Time-dashboard" +STATE="$REPO/.loop/state.json" +LOG_DIR="$REPO/.loop/logs" +CLAIMS="$REPO/.loop/claims.json" +PROMPT_FILE="$HOME/.hermes/bin/timmy-loop-prompt.md" +LOCKFILE="/tmp/timmy-loop.lock" +COOLDOWN=30 +MAX_CYCLE_TIME=1200 # 20 min — enough for complex issues +CLAIM_TTL_SECONDS=3600 # 1 hour — stale claims auto-expire +TIMMY="$REPO/.venv/bin/timmy" + +# macOS doesn't have timeout; use perl fallback +if ! command -v timeout &>/dev/null; then + timeout() { + local duration="$1"; shift + perl -e "alarm $duration; exec @ARGV" -- "$@" + } +fi + +mkdir -p "$LOG_DIR" + +# ── Single-instance lock ────────────────────────────────────────────── +if [ -f "$LOCKFILE" ]; then + PID=$(cat "$LOCKFILE" 2>/dev/null) + if kill -0 "$PID" 2>/dev/null; then + echo "[loop] Already running (PID $PID). Exiting." + exit 0 + fi + rm -f "$LOCKFILE" +fi +echo $$ > "$LOCKFILE" +trap 'rm -f "$LOCKFILE"' EXIT + +# ── Helpers ─────────────────────────────────────────────────────────── +update_state() { + local key="$1" value="$2" + python3 -c " +import json +with open('$STATE') as f: s = json.load(f) +s['$key'] = $value +with open('$STATE', 'w') as f: json.dump(s, f, indent=2) +" +} + +log() { + echo "[$(date '+%H:%M:%S')] $*" +} + +# ── Expire stale claims ────────────────────────────────────────────── +expire_claims() { + python3 -c " +import json +from datetime import datetime, timezone, timedelta + +TTL = timedelta(seconds=$CLAIM_TTL_SECONDS) +now = datetime.now(timezone.utc) + +try: + with open('$CLAIMS') as f: + claims = json.load(f) +except: + claims = {} + +expired = [] +active = {} +for issue, info in claims.items(): + if isinstance(info, dict): + claimed_at = info.get('at', '') + try: + dt = datetime.fromisoformat(claimed_at.replace('Z', '+00:00')) + if now - dt > TTL: + expired.append(issue) + continue + except: + pass + active[issue] = info + +if expired: + print(f'[claims] Expired {len(expired)} stale claims: {expired}') + with open('$CLAIMS', 'w') as f: + json.dump(active, f, indent=2) +else: + print('[claims] No stale claims') +" 2>&1 +} + +# ── Cleanup after timeout/failure ───────────────────────────────────── +cleanup_cycle() { + local cycle_num="$1" + local worktree="/tmp/timmy-cycle-${cycle_num}" + + # Remove worktree if it exists + if [ -d "$worktree" ]; then + log "Cleaning up worktree $worktree" + cd "$REPO" && git worktree remove "$worktree" --force 2>/dev/null || true + fi + + # Release any claims held by this cycle + python3 -c " +import json +try: + with open('$CLAIMS') as f: + claims = json.load(f) + released = [] + active = {} + for issue, info in claims.items(): + agent = info.get('by', '') if isinstance(info, dict) else '' + if 'loop' in agent or 'hermes' in agent.lower(): + released.append(issue) + else: + active[issue] = info + if released: + with open('$CLAIMS', 'w') as f: + json.dump(active, f, indent=2) + print(f'[cleanup] Released claims: {released}') +except Exception as e: + print(f'[cleanup] Claim cleanup failed: {e}') +" 2>&1 +} + +# ── Ask Timmy for triage (non-blocking) ─────────────────────────────── +ask_timmy() { + local question="$1" + local result + # 45s timeout — if Timmy hangs, skip gracefully + result=$(timeout 45 "$TIMMY" chat --session-id loop "$question" 2>/dev/null | grep -v "^WARNING" | grep -v "^$" | head -20) + if [ -n "$result" ]; then + echo "$result" + else + echo "(Timmy unavailable)" + fi +} + +# ── Main Loop ───────────────────────────────────────────────────────── +log "Timmy development loop v2 starting. PID $$" +log "Timeout: ${MAX_CYCLE_TIME}s | Cooldown: ${COOLDOWN}s | Claim TTL: ${CLAIM_TTL_SECONDS}s" +log "Repo: $REPO" +update_state "started_at" "\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"" +update_state "status" '"running"' + +CYCLE=$(python3 -c "import json; print(json.load(open('$STATE'))['cycle'])") + +while true; do + # Check for stop file + if [ -f "$REPO/.loop/STOP" ]; then + log "STOP file found. Halting loop." + update_state "status" '"stopped"' + break + fi + + CYCLE=$((CYCLE + 1)) + CYCLE_LOG="$LOG_DIR/cycle-$(printf '%04d' $CYCLE).log" + + log "━━━ Cycle $CYCLE ━━━" + update_state "cycle" "$CYCLE" + update_state "status" '"working"' + + # ── Pre-cycle housekeeping ──────────────────────────────────────── + expire_claims + + # ── Ask Timmy for input (if available) ──────────────────────────── + log "Asking Timmy for triage input..." + TIMMY_INPUT=$(ask_timmy "The development loop is starting cycle $CYCLE. Look at the open issues on Gitea and tell me: which issue should we work on next and why? Consider priority, dependencies, and what would help you most. Be brief — two sentences max.") + log "Timmy says: $TIMMY_INPUT" + + # ── Build the prompt with time budget and Timmy's input ─────────── + PROMPT=$(cat "$PROMPT_FILE") + PROMPT="TIME BUDGET: You have $((MAX_CYCLE_TIME / 60)) minutes for this cycle. Plan accordingly — do not start work you cannot finish. + +TIMMY'S TRIAGE INPUT (from Timmy himself): +$TIMMY_INPUT + +$PROMPT" + + log "Spawning hermes for cycle $CYCLE..." + + # Run hermes with timeout — tee to log AND stdout for live visibility + if timeout "$MAX_CYCLE_TIME" hermes chat --yolo -q "$PROMPT" 2>&1 | tee "$CYCLE_LOG"; then + log "Cycle $CYCLE completed successfully" + update_state "status" '"idle"' + update_state "last_completed" "\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"" + + # ── Post-cycle: Ask Timmy to review ─────────────────────────── + log "Asking Timmy to review cycle output..." + REVIEW=$(ask_timmy "Cycle $CYCLE just completed. Check if there are any new PRs on Gitea. If there is one, read it and give a brief code review opinion — does it look good? Any concerns? Two sentences max.") + log "Timmy's review: $REVIEW" + else + EXIT_CODE=$? + log "Cycle $CYCLE exited with code $EXIT_CODE" + update_state "status" '"error"' + python3 -c " +import json +with open('$STATE') as f: s = json.load(f) +errs = s.get('errors', []) +errs.append({'cycle': $CYCLE, 'code': $EXIT_CODE, 'time': '$(date -u +%Y-%m-%dT%H:%M:%SZ)'}) +s['errors'] = errs[-10:] +with open('$STATE', 'w') as f: json.dump(s, f, indent=2) +" + # ── Cleanup on failure ──────────────────────────────────────── + cleanup_cycle "$CYCLE" + fi + + log "Cooling down ${COOLDOWN}s before next cycle..." + sleep "$COOLDOWN" +done diff --git a/bin/timmy-status.sh b/bin/timmy-status.sh new file mode 100755 index 0000000..3004b27 --- /dev/null +++ b/bin/timmy-status.sh @@ -0,0 +1,267 @@ +#!/usr/bin/env bash +# ── Timmy Loop Status Panel ──────────────────────────────────────────── +# Compact, info-dense sidebar for the tmux development loop. +# Refreshes every 10s. Designed for ~40-col wide pane. +# ─────────────────────────────────────────────────────────────────────── + +STATE="$HOME/Timmy-Time-dashboard/.loop/state.json" +REPO="$HOME/Timmy-Time-dashboard" +TOKEN=$(cat ~/.hermes/gitea_token 2>/dev/null) +API="http://localhost:3000/api/v1/repos/rockachopa/Timmy-time-dashboard" + +# ── Colors ── +B='\033[1m' # bold +D='\033[2m' # dim +R='\033[0m' # reset +G='\033[32m' # green +Y='\033[33m' # yellow +RD='\033[31m' # red +C='\033[36m' # cyan +M='\033[35m' # magenta +W='\033[37m' # white +BG='\033[42;30m' # green bg +BY='\033[43;30m' # yellow bg +BR='\033[41;37m' # red bg + +# How wide is our pane? +COLS=$(tput cols 2>/dev/null || echo 40) + +hr() { printf "${D}"; printf '─%.0s' $(seq 1 "$COLS"); printf "${R}\n"; } + +while true; do + clear + + # ── Header ── + echo -e "${B}${C} ⚙ TIMMY DEV LOOP${R} ${D}$(date '+%H:%M:%S')${R}" + hr + + # ── Loop State ── + if [ -f "$STATE" ]; then + eval "$(python3 -c " +import json, sys +with open('$STATE') as f: s = json.load(f) +print(f'CYCLE={s.get(\"cycle\",\"?\")}')" 2>/dev/null)" + STATUS=$(python3 -c "import json; print(json.load(open('$STATE'))['status'])" 2>/dev/null || echo "?") + LAST_OK=$(python3 -c " +import json +from datetime import datetime, timezone +s = json.load(open('$STATE')) +t = s.get('last_completed','') +if t: + dt = datetime.fromisoformat(t.replace('Z','+00:00')) + delta = datetime.now(timezone.utc) - dt + mins = int(delta.total_seconds() / 60) + if mins < 60: print(f'{mins}m ago') + else: print(f'{mins//60}h {mins%60}m ago') +else: print('never') +" 2>/dev/null || echo "?") + CLOSED=$(python3 -c "import json; print(len(json.load(open('$STATE')).get('issues_closed',[])))" 2>/dev/null || echo 0) + CREATED=$(python3 -c "import json; print(len(json.load(open('$STATE')).get('issues_created',[])))" 2>/dev/null || echo 0) + ERRS=$(python3 -c "import json; print(len(json.load(open('$STATE')).get('errors',[])))" 2>/dev/null || echo 0) + LAST_ISSUE=$(python3 -c "import json; print(json.load(open('$STATE')).get('last_issue','—'))" 2>/dev/null || echo "—") + LAST_PR=$(python3 -c "import json; print(json.load(open('$STATE')).get('last_pr','—'))" 2>/dev/null || echo "—") + TESTS=$(python3 -c " +import json +s = json.load(open('$STATE')) +t = s.get('test_results',{}) +if t: + print(f\"{t.get('passed',0)} pass, {t.get('failed',0)} fail, {t.get('coverage','?')} cov\") +else: + print('no data') +" 2>/dev/null || echo "no data") + + # Status badge + case "$STATUS" in + working) BADGE="${BY} WORKING ${R}" ;; + idle) BADGE="${BG} IDLE ${R}" ;; + error) BADGE="${BR} ERROR ${R}" ;; + *) BADGE="${D} $STATUS ${R}" ;; + esac + + echo -e " ${B}Status${R} $BADGE ${D}cycle${R} ${B}$CYCLE${R}" + echo -e " ${B}Last OK${R} ${G}$LAST_OK${R} ${D}issue${R} #$LAST_ISSUE ${D}PR${R} #$LAST_PR" + echo -e " ${G}✓${R} $CLOSED closed ${C}+${R} $CREATED created ${RD}✗${R} $ERRS errs" + echo -e " ${D}Tests:${R} $TESTS" + else + echo -e " ${RD}No state file${R}" + fi + + hr + + # ── Ollama Status ── + echo -e " ${B}${M}◆ OLLAMA${R}" + OLLAMA_PS=$(curl -s http://localhost:11434/api/ps 2>/dev/null) + if [ -n "$OLLAMA_PS" ] && echo "$OLLAMA_PS" | python3 -c "import sys,json; json.load(sys.stdin)" &>/dev/null; then + python3 -c " +import json, sys +data = json.loads('''$OLLAMA_PS''') +models = data.get('models', []) +if not models: + print(' \033[2m(no models loaded)\033[0m') +for m in models: + name = m.get('name','?') + vram = m.get('size_vram', 0) / 1e9 + exp = m.get('expires_at','') + print(f' \033[32m●\033[0m {name} \033[2m{vram:.1f}GB VRAM\033[0m') +" 2>/dev/null + else + echo -e " ${RD}● offline${R}" + fi + + # ── Timmy Health ── + TIMMY_HEALTH=$(curl -s --max-time 2 http://localhost:8000/health 2>/dev/null) + if [ -n "$TIMMY_HEALTH" ]; then + python3 -c " +import json +h = json.loads('''$TIMMY_HEALTH''') +status = h.get('status','?') +ollama = h.get('services',{}).get('ollama','?') +model = h.get('llm_model','?') +agent_st = list(h.get('agents',{}).values())[0].get('status','?') if h.get('agents') else '?' +up = int(h.get('uptime_seconds',0)) +hrs, rem = divmod(up, 3600) +mins = rem // 60 +print(f' \033[1m\033[35m◆ TIMMY DASHBOARD\033[0m') +print(f' \033[32m●\033[0m {status} model={model}') +print(f' \033[2magent={agent_st} ollama={ollama} up={hrs}h{mins}m\033[0m') +" 2>/dev/null + else + echo -e " ${B}${M}◆ TIMMY DASHBOARD${R}" + echo -e " ${RD}● unreachable${R}" + fi + + hr + + # ── Open Issues ── + echo -e " ${B}${Y}▶ OPEN ISSUES${R}" + if [ -n "$TOKEN" ]; then + curl -s "${API}/issues?state=open&limit=10&sort=created&direction=desc" \ + -H "Authorization: token $TOKEN" 2>/dev/null | \ + python3 -c " +import json, sys +try: + issues = json.load(sys.stdin) + if not issues: + print(' \033[2m(none)\033[0m') + for i in issues[:10]: + num = i['number'] + title = i['title'][:36] + labels = ','.join(l['name'][:8] for l in i.get('labels',[])) + lbl = f' \033[2m[{labels}]\033[0m' if labels else '' + print(f' \033[33m#{num:<4d}\033[0m {title}{lbl}') + if len(issues) > 10: + print(f' \033[2m... +{len(issues)-10} more\033[0m') +except: print(' \033[2m(fetch failed)\033[0m') +" 2>/dev/null + else + echo -e " ${RD}(no token)${R}" + fi + + # ── Open PRs ── + echo -e " ${B}${G}▶ OPEN PRs${R}" + if [ -n "$TOKEN" ]; then + curl -s "${API}/pulls?state=open&limit=5" \ + -H "Authorization: token $TOKEN" 2>/dev/null | \ + python3 -c " +import json, sys +try: + prs = json.load(sys.stdin) + if not prs: + print(' \033[2m(none)\033[0m') + for p in prs[:5]: + num = p['number'] + title = p['title'][:36] + print(f' \033[32mPR #{num:<4d}\033[0m {title}') +except: print(' \033[2m(fetch failed)\033[0m') +" 2>/dev/null + else + echo -e " ${RD}(no token)${R}" + fi + + hr + + # ── Git Log ── + echo -e " ${B}${D}▶ RECENT COMMITS${R}" + cd "$REPO" 2>/dev/null && git log --oneline --no-decorate -6 2>/dev/null | while read line; do + HASH=$(echo "$line" | cut -c1-7) + MSG=$(echo "$line" | cut -c9- | cut -c1-32) + echo -e " ${C}${HASH}${R} ${D}${MSG}${R}" + done + + hr + + # ── Claims ── + CLAIMS_FILE="$REPO/.loop/claims.json" + if [ -f "$CLAIMS_FILE" ]; then + CLAIMS=$(python3 -c " +import json +with open('$CLAIMS_FILE') as f: c = json.load(f) +active = [(k,v) for k,v in c.items() if v.get('status') == 'active'] +if active: + for k,v in active: + print(f' \033[33m⚡\033[0m #{k} claimed by {v.get(\"agent\",\"?\")[:12]}') +else: + print(' \033[2m(none active)\033[0m') +" 2>/dev/null) + if [ -n "$CLAIMS" ]; then + echo -e " ${B}${Y}▶ CLAIMED${R}" + echo "$CLAIMS" + fi + fi + + # ── System ── + echo -e " ${B}${D}▶ SYSTEM${R}" + # Disk + DISK=$(df -h / 2>/dev/null | tail -1 | awk '{print $4 " free / " $2}') + echo -e " ${D}Disk:${R} $DISK" + # Memory (macOS) + if command -v memory_pressure &>/dev/null; then + MEM_PRESS=$(memory_pressure 2>/dev/null | grep "System-wide" | head -1 | sed 's/.*: //') + echo -e " ${D}Mem:${R} $MEM_PRESS" + elif [ -f /proc/meminfo ]; then + MEM=$(awk '/MemAvailable/{printf "%.1fGB free", $2/1048576}' /proc/meminfo 2>/dev/null) + echo -e " ${D}Mem:${R} $MEM" + fi + # CPU load + LOAD=$(uptime | sed 's/.*averages: //' | cut -d',' -f1 | xargs) + echo -e " ${D}Load:${R} $LOAD" + + hr + + # ── Notes from last cycle ── + if [ -f "$STATE" ]; then + NOTES=$(python3 -c " +import json +s = json.load(open('$STATE')) +n = s.get('notes','') +if n: + lines = n[:150] + if len(n) > 150: lines += '...' + print(lines) +" 2>/dev/null) + if [ -n "$NOTES" ]; then + echo -e " ${B}${D}▶ LAST CYCLE NOTE${R}" + echo -e " ${D}${NOTES}${R}" + hr + fi + + # Timmy observations + TIMMY_OBS=$(python3 -c " +import json +s = json.load(open('$STATE')) +obs = s.get('timmy_observations','') +if obs: + lines = obs[:120] + if len(obs) > 120: lines += '...' + print(lines) +" 2>/dev/null) + if [ -n "$TIMMY_OBS" ]; then + echo -e " ${B}${M}▶ TIMMY SAYS${R}" + echo -e " ${D}${TIMMY_OBS}${R}" + hr + fi + fi + + echo -e " ${D}↻ 8s${R}" + sleep 8 +done diff --git a/bin/timmy-tmux.sh b/bin/timmy-tmux.sh new file mode 100755 index 0000000..6cc67aa --- /dev/null +++ b/bin/timmy-tmux.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# ── Timmy Loop tmux Session ──────────────────────────────────────────── +# Creates session with 3 panes using standard tmux splits. +# +# Layout: +# ┌──────────────────────┬──────────────────────┐ +# │ LOOP OUTPUT │ STATUS DASHBOARD │ +# ├──────────────────────┤ (live refresh) │ +# │ HERMES CHAT │ │ +# └──────────────────────┴──────────────────────┘ +# ─────────────────────────────────────────────────────────────────────── + +SESSION="timmy-loop" +export PATH="$HOME/.local/bin:$HOME/.hermes/bin:$PATH" + +# Kill existing +tmux kill-session -t "$SESSION" 2>/dev/null +sleep 1 + +# Create session — pane 0 starts as shell +tmux new-session -d -s "$SESSION" -x 200 -y 50 + +# Vertical split: left | right (Ctrl-b %) +tmux split-window -h -t "$SESSION:0.0" + +# Horizontal split on left pane: top-left / bottom-left (Ctrl-b ") +tmux split-window -v -t "$SESSION:0.0" + +# Pane map after splits: +# 0 = top-left → Loop +# 1 = bottom-left → Chat +# 2 = right → Status + +# Set titles +tmux select-pane -t "$SESSION:0.0" -T "Loop" +tmux select-pane -t "$SESSION:0.1" -T "Chat" +tmux select-pane -t "$SESSION:0.2" -T "Status" + +# Pane border styling +tmux set-option -t "$SESSION" pane-border-status top +tmux set-option -t "$SESSION" pane-border-format " #{pane_title} " +tmux set-option -t "$SESSION" pane-border-style "fg=colour240" +tmux set-option -t "$SESSION" pane-active-border-style "fg=cyan" + +# Start processes +tmux send-keys -t "$SESSION:0.0" "export PATH=\"$HOME/.local/bin:$HOME/.hermes/bin:/usr/local/bin:\$PATH\" && $HOME/.hermes/bin/timmy-loop.sh" Enter +tmux send-keys -t "$SESSION:0.2" "$HOME/.hermes/bin/timmy-status.sh" Enter +tmux send-keys -t "$SESSION:0.1" "cd ~/Timmy-Time-dashboard && hermes" Enter + +# Focus chat pane +tmux select-pane -t "$SESSION:0.1" + +echo "" +echo " ┌──────────────────┬──────────────────┐" +echo " │ Loop (pane 0) │ Status (pane 2) │" +echo " ├──────────────────┤ │" +echo " │ Chat (pane 1) │ │" +echo " └──────────────────┴──────────────────┘" +echo "" +echo " Attach: tmux attach -t timmy-loop" +echo " Stop: touch ~/Timmy-Time-dashboard/.loop/STOP" +echo " Kill: tmux kill-session -t timmy-loop" +echo "" diff --git a/bin/timmy-watchdog.sh b/bin/timmy-watchdog.sh new file mode 100755 index 0000000..5c31e3d --- /dev/null +++ b/bin/timmy-watchdog.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# ── Timmy Loop Watchdog ──────────────────────────────────────────────── +# Checks if the timmy-loop tmux session is alive. Restarts if dead. +# Designed to run via cron every 5 minutes. +# ─────────────────────────────────────────────────────────────────────── + +SESSION="timmy-loop" +LAUNCHER="$HOME/.hermes/bin/timmy-tmux.sh" +WATCHDOG_LOG="$HOME/Timmy-Time-dashboard/.loop/watchdog.log" + +log() { + echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$WATCHDOG_LOG" +} + +# Check if tmux session exists +if tmux has-session -t "$SESSION" 2>/dev/null; then + # Session exists. Check if the loop pane (pane 0) is still running. + PANE0_PID=$(tmux list-panes -t "$SESSION:.0" -F '#{pane_pid}' 2>/dev/null) + if [ -n "$PANE0_PID" ] && kill -0 "$PANE0_PID" 2>/dev/null; then + # All good, loop is alive + exit 0 + else + log "WARN: Session exists but loop pane is dead. Restarting..." + tmux kill-session -t "$SESSION" 2>/dev/null + fi +else + log "WARN: Session '$SESSION' not found." +fi + +# Check for a stop file — lets Alexander or an agent halt the loop +if [ -f "$HOME/Timmy-Time-dashboard/.loop/STOP" ]; then + log "STOP file found. Not restarting. Remove .loop/STOP to resume." + exit 0 +fi + +log "Restarting timmy-loop session..." +export PATH="$HOME/.local/bin:$HOME/.hermes/bin:$PATH" +"$LAUNCHER" +log "Session restarted." diff --git a/channel_directory.json b/channel_directory.json index 7ab2a17..8ea740c 100644 --- a/channel_directory.json +++ b/channel_directory.json @@ -1,5 +1,5 @@ { - "updated_at": "2026-03-14T15:08:54.695649", + "updated_at": "2026-03-14T17:59:29.552651", "platforms": { "discord": [ { diff --git a/config.yaml b/config.yaml index 080a586..2069373 100644 --- a/config.yaml +++ b/config.yaml @@ -146,8 +146,6 @@ discord: require_mention: true free_response_channels: '' command_allowlist: -- rm\s+(-[^\s]*\s+)*/ -- find - (python[23]?|perl|ruby|node)\s+-[ec]\s+ quick_commands: {} personalities: {} @@ -183,25 +181,15 @@ custom_providers: - name: Local (localhost:11434) base_url: http://localhost:11434/v1 api_key: ollama - model: qwen3.5:latest + model: qwen3:30b - name: Local (localhost:8089) base_url: http://localhost:8089/ api_key: ollama model: NousResearch/Hermes-4.3-36B - -# ── Fallback Chain ───────────────────────────────────────────────────── -# Ordered list of fallback providers tried when the primary fails. -# Cascades DOWN on failure (rate limit, overload, connection error). -# Periodically tries to recover back UP toward the primary. -# -# Chain: Anthropic (primary) → Groq → Kimi → Local Ollama -# fallback_model: - - provider: groq - model: llama-3.3-70b-versatile - - provider: groq - model: moonshotai/kimi-k2-instruct - - provider: custom - model: qwen3.5:latest - base_url: "http://localhost:11434/v1" - api_key: ollama +- provider: kimi-coding + model: kimi-k2.5 +- provider: custom + model: qwen3:30b + base_url: http://localhost:11434/v1 + api_key: ollama diff --git a/cron/jobs.json b/cron/jobs.json index 666b236..eed3167 100644 --- a/cron/jobs.json +++ b/cron/jobs.json @@ -22,7 +22,30 @@ "last_error": null, "deliver": "local", "origin": null + }, + { + "id": "99eaca15a57e", + "name": "timmy-loop-watchdog", + "prompt": "Run the Timmy loop watchdog. Execute this command and report the result:\n\nbash ~/.hermes/bin/timmy-watchdog.sh 2>&1\n\nThen check if the tmux session is alive:\n\ntmux has-session -t timmy-loop 2>&1 && echo \"Session alive\" || echo \"Session NOT found\"\n\nAlso check the watchdog log for recent entries:\n\ntail -5 ~/Timmy-Time-dashboard/.loop/watchdog.log 2>/dev/null || echo \"No log yet\"\n\nReport status briefly.", + "schedule": { + "kind": "interval", + "minutes": 5, + "display": "every 5m" + }, + "schedule_display": "every 5m", + "repeat": { + "times": null, + "completed": 29 + }, + "enabled": true, + "created_at": "2026-03-14T15:32:37.430426-04:00", + "next_run_at": "2026-03-14T18:03:29.472483-04:00", + "last_run_at": "2026-03-14T17:58:29.472483-04:00", + "last_status": "error", + "last_error": "RuntimeError: Unknown provider 'anthropic'.", + "deliver": "local", + "origin": null } ], - "updated_at": "2026-03-12T23:22:20.287416-04:00" + "updated_at": "2026-03-14T17:58:29.472631-04:00" } \ No newline at end of file diff --git a/memories/MEMORY.md b/memories/MEMORY.md index d0c53c9..f8f38aa 100644 --- a/memories/MEMORY.md +++ b/memories/MEMORY.md @@ -4,6 +4,6 @@ Timmy architecture plan: Replace hardcoded _PERSONAS and TimmyOrchestrator with § Hermes-Timmy workspace set up at ~/Timmy-Time-dashboard/workspace/. Flat file correspondence journal (correspondence.md), inbox/ (Hermes→Timmy), outbox/ (Timmy→Hermes), shared/ (handoff patterns, reference docs). Protocol: append-only, timestamped. Plan: plug into Timmy's heartbeat tick so he auto-replies to new correspondence. § -2026-03-14: Fixed Timmy issues #36-#40, PRs #41-#43 merged. Built voice loop. Added Hermes fallback chain (Anthropic→Groq→Kimi→Local) with recovery-up. Set up source control: hermes-agent sovereign fork (rebase workflow) + hermes-config repo. GROQ_API_KEY + KIMI_API_KEY in env. hermes-sync script at ~/.hermes/bin/. +2026-03-14: Fixed issues #36-#40, #52. Built voice loop, fallback chain, source control. Built self-prompt queue. Upgraded Timmy to qwen3:30b with num_ctx=4096 cap (19GB VRAM, fits 39GB Mac). Loop v2: 20min timeout, claim TTL expiry, timeout cleanup, Timmy triage+review integration, 58% smaller prompt. Filed eval issues #77-#87. Status panel: ~/.hermes/bin/timmy-status.sh. § 2026-03-14 voice session: Built sovereign voice loop (timmy voice). Piper TTS + Whisper STT + Ollama, all local. Fixed event loop (persistent loop for MCP sessions), markdown stripping for TTS, MCP noise suppression, clean shutdown hooks. 1234 tests passing. Alexander wants to eventually train a custom voice using his own voice samples — noted for future. \ No newline at end of file