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:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -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
|
||||
|
||||
54
bin/timmy-loop-prompt.md
Normal file
54
bin/timmy-loop-prompt.md
Normal 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
215
bin/timmy-loop.sh
Executable 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
267
bin/timmy-status.sh
Executable 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
63
bin/timmy-tmux.sh
Executable 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
39
bin/timmy-watchdog.sh
Executable 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."
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"updated_at": "2026-03-14T15:08:54.695649",
|
||||
"updated_at": "2026-03-14T17:59:29.552651",
|
||||
"platforms": {
|
||||
"discord": [
|
||||
{
|
||||
|
||||
26
config.yaml
26
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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user