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
This commit is contained in:
Alexander Whitestone
2026-03-14 18:00:32 -04:00
parent 804ffb5cf0
commit 0b066de1cc
10 changed files with 676 additions and 22 deletions

5
.gitignore vendored
View File

@@ -52,6 +52,11 @@ cron/output/
# ── Binaries (except our scripts) ──────────────────────────────────── # ── Binaries (except our scripts) ────────────────────────────────────
bin/* bin/*
!bin/hermes-sync !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 ────────────────────────────────────────────────────────── # ── OS junk ──────────────────────────────────────────────────────────
.DS_Store .DS_Store

54
bin/timmy-loop-prompt.md Normal file
View File

@@ -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 <issue#>
- 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 <issue#>
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.

215
bin/timmy-loop.sh Executable file
View File

@@ -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

267
bin/timmy-status.sh Executable file
View File

@@ -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

63
bin/timmy-tmux.sh Executable file
View File

@@ -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 ""

39
bin/timmy-watchdog.sh Executable file
View File

@@ -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."

View File

@@ -1,5 +1,5 @@
{ {
"updated_at": "2026-03-14T15:08:54.695649", "updated_at": "2026-03-14T17:59:29.552651",
"platforms": { "platforms": {
"discord": [ "discord": [
{ {

View File

@@ -146,8 +146,6 @@ discord:
require_mention: true require_mention: true
free_response_channels: '' free_response_channels: ''
command_allowlist: command_allowlist:
- rm\s+(-[^\s]*\s+)*/
- find
- (python[23]?|perl|ruby|node)\s+-[ec]\s+ - (python[23]?|perl|ruby|node)\s+-[ec]\s+
quick_commands: {} quick_commands: {}
personalities: {} personalities: {}
@@ -183,25 +181,15 @@ custom_providers:
- name: Local (localhost:11434) - name: Local (localhost:11434)
base_url: http://localhost:11434/v1 base_url: http://localhost:11434/v1
api_key: ollama api_key: ollama
model: qwen3.5:latest model: qwen3:30b
- name: Local (localhost:8089) - name: Local (localhost:8089)
base_url: http://localhost:8089/ base_url: http://localhost:8089/
api_key: ollama api_key: ollama
model: NousResearch/Hermes-4.3-36B 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: fallback_model:
- provider: groq - provider: kimi-coding
model: llama-3.3-70b-versatile model: kimi-k2.5
- provider: groq - provider: custom
model: moonshotai/kimi-k2-instruct model: qwen3:30b
- provider: custom base_url: http://localhost:11434/v1
model: qwen3.5:latest api_key: ollama
base_url: "http://localhost:11434/v1"
api_key: ollama

View File

@@ -22,7 +22,30 @@
"last_error": null, "last_error": null,
"deliver": "local", "deliver": "local",
"origin": null "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"
} }

View File

@@ -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. 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. 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.