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) ────────────────────────────────────
|
# ── 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
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": {
|
"platforms": {
|
||||||
"discord": [
|
"discord": [
|
||||||
{
|
{
|
||||||
|
|||||||
26
config.yaml
26
config.yaml
@@ -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
|
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
@@ -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.
|
||||||
Reference in New Issue
Block a user