Compare commits

..

56 Commits

Author SHA1 Message Date
Alexander Whitestone
ffea2964c4 docs: inventory live automations and stale-state risks 2026-04-04 15:57:54 -04:00
Alexander Whitestone
5deaea26b3 fix: stabilize local loop operations 2026-04-04 15:50:37 -04:00
Alexander Whitestone
9d0ea981db config: update channel_directory.json,logs/huey.error.log 2026-03-30 17:00:48 -04:00
Alexander Whitestone
2df8a1e627 config: update channel_directory.json,logs/huey.error.log 2026-03-30 16:01:23 -04:00
Alexander Whitestone
996e096da0 config: update channel_directory.json,logs/huey.error.log 2026-03-30 15:00:45 -04:00
Alexander Whitestone
eba8c2d320 config: update channel_directory.json,config.yaml,logs/huey.error.log 2026-03-30 14:00:45 -04:00
Alexander Whitestone
3d2bf6c1cf config: update channel_directory.json,config.yaml,logs/huey.error.log 2026-03-30 13:00:49 -04:00
Alexander Whitestone
8f0f6a0500 config: update channel_directory.json,logs/huey.error.log 2026-03-30 12:00:55 -04:00
Alexander Whitestone
5571b94a81 config: update channel_directory.json,config.yaml,logs/huey.error.log 2026-03-30 11:00:53 -04:00
Alexander Whitestone
508140ac59 config: update channel_directory.json,logs/huey.error.log 2026-03-30 10:00:44 -04:00
Alexander Whitestone
3789af2622 config: update channel_directory.json,logs/huey.error.log 2026-03-30 09:00:43 -04:00
Alexander Whitestone
2ff28609f8 config: update channel_directory.json,logs/huey.error.log 2026-03-30 08:00:52 -04:00
Alexander Whitestone
4372a406bf config: update channel_directory.json,logs/huey.error.log 2026-03-30 07:00:50 -04:00
Alexander Whitestone
0e126be7e8 config: update channel_directory.json,logs/huey.error.log 2026-03-30 06:00:50 -04:00
Alexander Whitestone
fcdbd57eb8 config: update channel_directory.json,logs/huey.error.log 2026-03-30 05:00:48 -04:00
Alexander Whitestone
789b23c69a config: update channel_directory.json,logs/huey.error.log 2026-03-30 04:00:49 -04:00
Alexander Whitestone
0ca78ae17f config: update channel_directory.json,logs/huey.error.log 2026-03-30 03:00:49 -04:00
Alexander Whitestone
832b3f4188 config: update channel_directory.json,logs/huey.error.log 2026-03-30 02:00:52 -04:00
Alexander Whitestone
c9679ed827 config: update channel_directory.json,logs/huey.error.log 2026-03-30 01:00:50 -04:00
Alexander Whitestone
c82932c37b config: update channel_directory.json,config.yaml,logs/huey.error.log 2026-03-30 00:00:52 -04:00
Alexander Whitestone
b92bcd52a5 config: update channel_directory.json,logs/huey.error.log 2026-03-29 23:00:51 -04:00
Alexander Whitestone
6ee2d50bcd config: update channel_directory.json,logs/huey.error.log 2026-03-29 22:00:53 -04:00
Alexander Whitestone
11e8dc8931 config: update channel_directory.json,config.yaml,logs/huey.error.log 2026-03-29 21:00:57 -04:00
Alexander Whitestone
9c235616bf config: update channel_directory.json,logs/huey.error.log 2026-03-29 20:00:48 -04:00
Alexander Whitestone
4fee656eff config: update channel_directory.json,logs/huey.error.log 2026-03-29 19:00:51 -04:00
Alexander Whitestone
247206bc60 config: update channel_directory.json,logs/huey.error.log 2026-03-29 18:00:52 -04:00
Alexander Whitestone
24376306a8 config: update channel_directory.json,logs/huey.error.log 2026-03-29 17:00:51 -04:00
Alexander Whitestone
7959f0f4a3 config: update channel_directory.json,logs/huey.error.log 2026-03-29 16:00:49 -04:00
Alexander Whitestone
2facaf12b0 config: update channel_directory.json,logs/huey.error.log 2026-03-29 15:00:55 -04:00
Alexander Whitestone
e650996966 config: update channel_directory.json,logs/huey.error.log 2026-03-29 14:00:50 -04:00
Alexander Whitestone
6693cccd88 config: update channel_directory.json,logs/huey.error.log 2026-03-29 13:00:48 -04:00
Alexander Whitestone
fd6b27b77e config: update channel_directory.json,config.yaml,logs/huey.error.log 2026-03-29 12:00:50 -04:00
Alexander Whitestone
0b32b51626 config: update channel_directory.json,logs/huey.error.log 2026-03-29 11:00:46 -04:00
Alexander Whitestone
88fe21a88c config: update channel_directory.json,logs/huey.error.log 2026-03-29 10:00:43 -04:00
Alexander Whitestone
2f4ad87e7b config: update channel_directory.json,logs/huey.error.log 2026-03-29 09:00:48 -04:00
Alexander Whitestone
87f2961f9d config: update channel_directory.json,logs/huey.error.log 2026-03-29 08:00:53 -04:00
Alexander Whitestone
53ae5db414 config: update channel_directory.json,logs/huey.error.log 2026-03-29 07:00:47 -04:00
Alexander Whitestone
70d3f2594d config: update channel_directory.json,logs/huey.error.log 2026-03-29 06:00:53 -04:00
Alexander Whitestone
8502de0deb config: update channel_directory.json,logs/huey.error.log 2026-03-29 05:00:48 -04:00
Alexander Whitestone
789b47aebb config: update channel_directory.json,logs/huey.error.log 2026-03-29 04:00:49 -04:00
Alexander Whitestone
aab1328367 config: update channel_directory.json,logs/huey.error.log 2026-03-29 03:00:52 -04:00
Alexander Whitestone
6be9a268c4 config: update channel_directory.json,logs/huey.error.log 2026-03-29 02:00:52 -04:00
Alexander Whitestone
949d33c88a config: update channel_directory.json,logs/huey.error.log 2026-03-29 01:00:48 -04:00
Alexander Whitestone
a69c002ede config: update channel_directory.json,logs/huey.error.log 2026-03-29 00:00:51 -04:00
Alexander Whitestone
a341a61180 config: update logs/huey.error.log 2026-03-28 23:00:59 -04:00
Alexander Whitestone
56ba35db40 config: update channel_directory.json,config.yaml,logs/huey.error.log,tasks.py 2026-03-28 22:00:56 -04:00
Alexander Whitestone
3104f31f52 config: update channel_directory.json,config.yaml,logs/huey.error.log,playbooks/bug-fixer.yaml,playbooks/issue-triager.yaml,playbooks/pr-reviewer.yaml,playbooks/refactor-specialist.yaml,playbooks/security-auditor.yaml,playbooks/test-writer.yaml,playbooks/verified-logic.yaml,skins/timmy.yaml 2026-03-28 21:00:51 -04:00
Alexander Whitestone
e149ce1dfa config: update channel_directory.json,logs/huey.error.log 2026-03-28 20:00:44 -04:00
Alexander Whitestone
62f6665487 config: update channel_directory.json,config.yaml,logs/huey.error.log 2026-03-28 18:00:19 -04:00
Alexander Whitestone
fc0460d803 config: update channel_directory.json,config.yaml,logs/huey.error.log 2026-03-28 17:00:16 -04:00
Alexander Whitestone
f7e2971863 config: update channel_directory.json,logs/huey.error.log 2026-03-28 16:00:16 -04:00
Alexander Whitestone
167ed0f27d config: update channel_directory.json,logs/huey.error.log 2026-03-28 15:00:16 -04:00
Alexander Whitestone
a1d218417f config: update channel_directory.json,logs/huey.error.log 2026-03-28 14:00:16 -04:00
Alexander Whitestone
05d503682a config: update channel_directory.json,logs/huey.error.log 2026-03-28 13:00:13 -04:00
Alexander Whitestone
5b162e27d7 config: update channel_directory.json,logs/huey.error.log 2026-03-28 12:00:17 -04:00
Alexander Whitestone
0b0ac43041 config: update channel_directory.json,logs/huey.error.log 2026-03-28 11:00:16 -04:00
31 changed files with 265999 additions and 919 deletions

View File

@@ -1,57 +0,0 @@
# Contributing to timmy-config
## Proof Standard
This is a hard rule.
- visual changes require screenshot proof
- do not commit screenshots or binary media to Gitea backup unless explicitly required
- CLI/verifiable changes must cite the exact command output, log path, or world-state proof showing acceptance criteria were met
- config-only changes are not fully accepted when the real acceptance bar is live runtime behavior
- no proof, no merge
## How to satisfy the rule
### Visual changes
Examples:
- skin updates
- terminal UI layout changes
- browser-facing output
- dashboard/panel changes
Required proof:
- attach screenshot proof to the PR or issue discussion
- keep the screenshot outside the repo unless explicitly asked to commit it
- name what the screenshot proves
### CLI / harness / operational changes
Examples:
- scripts
- config wiring
- heartbeat behavior
- model routing
- export pipelines
Required proof:
- cite the exact command used
- paste the relevant output, or
- cite the exact log path / world-state artifact that proves the change
Good:
- `python3 -m pytest tests/test_x.py -q``2 passed`
- `~/.timmy/timmy-config/logs/huey.log`
- `~/.hermes/model_health.json`
Bad:
- "looks right"
- "compiled"
- "should work now"
## Default merge gate
Every PR should make it obvious:
1. what changed
2. what acceptance criteria were targeted
3. what evidence proves those criteria were met
If that evidence is missing, the PR is not done.

View File

@@ -1,23 +1,27 @@
# DEPRECATED — Bash Loop Scripts Removed
# DEPRECATED — policy, not proof of runtime absence
**Date:** 2026-03-25
**Reason:** Replaced by Hermes + timmy-config sidecar orchestration
Original deprecation date: 2026-03-25
## What was removed
- claude-loop.sh, gemini-loop.sh, agent-loop.sh
- timmy-orchestrator.sh, workforce-manager.py
- nexus-merge-bot.sh, claudemax-watchdog.sh, timmy-loopstat.sh
This file records the policy direction: long-running ad hoc bash loops were meant
to be replaced by Hermes-side orchestration.
## What replaces them
**Harness:** Hermes
**Overlay repo:** Timmy_Foundation/timmy-config
**Entry points:** `orchestration.py`, `tasks.py`, `deploy.sh`
**Features:** Huey + SQLite scheduling, local-model health checks, session export, DPO artifact staging
But policy and world state diverged.
Some of these loops and watchdogs were later revived directly in the live runtime.
## Why
The bash loops crash-looped, produced zero work after relaunch, had no crash
recovery, no durable export path, and required too many ad hoc scripts. The
Hermes sidecar keeps orchestration close to Timmy's actual config and training
surfaces.
Do NOT use this file as proof that something is gone.
Use `docs/automation-inventory.md` as the current world-state document.
Do NOT recreate bash loops. If orchestration is broken, fix the Hermes sidecar.
## Deprecated by policy
- old dashboard-era loop stacks
- old tmux resurrection paths
- old startup paths that recreate `timmy-loop`
- stale repo-specific automation tied to `Timmy-time-dashboard` or `the-matrix`
## Current rule
If an automation question matters, audit:
1. launchd loaded jobs
2. live process table
3. Hermes cron list
4. the automation inventory doc
Only then decide what is actually live.

View File

@@ -14,10 +14,9 @@ timmy-config/
├── DEPRECATED.md ← What was removed and why
├── config.yaml ← Hermes harness configuration
├── channel_directory.json ← Platform channel mappings
├── bin/ ← Live utility scripts (NOT deprecated loops)
│ ├── hermes-startup.sh ← Hermes boot sequence
├── bin/ ← Sidecar-managed operational scripts
│ ├── hermes-startup.sh ← Dormant startup path (audit before enabling)
│ ├── agent-dispatch.sh ← Manual agent dispatch
│ ├── deploy-allegro-house.sh← Bootstraps the remote Allegro wizard house
│ ├── ops-panel.sh ← Ops dashboard panel
│ ├── ops-gitea.sh ← Gitea ops helpers
│ ├── pipeline-freshness.sh ← Session/export drift check
@@ -26,7 +25,7 @@ timmy-config/
├── skins/ ← UI skins (timmy skin)
├── playbooks/ ← Agent playbooks (YAML)
├── cron/ ← Cron job definitions
├── wizards/ ← Remote wizard-house templates + units
├── docs/automation-inventory.md ← Live automation + stale-state inventory
└── training/ ← Transitional training recipes, not canonical lived data
```
@@ -42,9 +41,10 @@ If a file answers "who is Timmy?" or "how does Hermes host him?", it belongs
here. If it answers "what has Timmy done or learned?" it belongs in
`timmy-home`.
The scripts in `bin/` are live operational helpers for the Hermes sidecar.
What is dead are the old long-running bash worker loops, not every script in
this repo.
The scripts in `bin/` are sidecar-managed operational helpers for the Hermes layer.
Do NOT assume older prose about removed loops is still true at runtime.
Audit the live machine first, then read `docs/automation-inventory.md` for the
current reality and stale-state risks.
## Orchestration: Huey
@@ -56,15 +56,6 @@ pip install huey
huey_consumer.py tasks.huey -w 2 -k thread
```
## Proof Standard
This repo uses a hard proof rule for merges.
- visual changes require screenshot proof
- CLI/verifiable changes must cite logs, command output, or world-state proof
- screenshots/media stay out of Gitea backup unless explicitly required
- see `CONTRIBUTING.md` for the merge gate
## Deploy
```bash

620
bin/claude-loop.sh Executable file
View File

@@ -0,0 +1,620 @@
#!/usr/bin/env bash
# claude-loop.sh — Parallel Claude Code agent dispatch loop
# Runs N workers concurrently against the Gitea backlog.
# Gracefully handles rate limits with backoff.
#
# Usage: claude-loop.sh [NUM_WORKERS] (default: 2)
set -euo pipefail
# === CONFIG ===
NUM_WORKERS="${1:-2}"
MAX_WORKERS=10 # absolute ceiling
WORKTREE_BASE="$HOME/worktrees"
GITEA_URL="http://143.198.27.163:3000"
GITEA_TOKEN=$(cat "$HOME/.hermes/claude_token")
CLAUDE_TIMEOUT=900 # 15 min per issue
COOLDOWN=15 # seconds between issues — stagger clones
RATE_LIMIT_SLEEP=30 # initial sleep on rate limit
MAX_RATE_SLEEP=120 # max backoff on rate limit
LOG_DIR="$HOME/.hermes/logs"
SKIP_FILE="$LOG_DIR/claude-skip-list.json"
LOCK_DIR="$LOG_DIR/claude-locks"
ACTIVE_FILE="$LOG_DIR/claude-active.json"
mkdir -p "$LOG_DIR" "$WORKTREE_BASE" "$LOCK_DIR"
# Initialize files
[ -f "$SKIP_FILE" ] || echo '{}' > "$SKIP_FILE"
echo '{}' > "$ACTIVE_FILE"
# === SHARED FUNCTIONS ===
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" >> "$LOG_DIR/claude-loop.log"
}
lock_issue() {
local issue_key="$1"
local lockfile="$LOCK_DIR/$issue_key.lock"
if mkdir "$lockfile" 2>/dev/null; then
echo $$ > "$lockfile/pid"
return 0
fi
return 1
}
unlock_issue() {
local issue_key="$1"
rm -rf "$LOCK_DIR/$issue_key.lock" 2>/dev/null
}
mark_skip() {
local issue_num="$1"
local reason="$2"
local skip_hours="${3:-1}"
python3 -c "
import json, time, fcntl
with open('$SKIP_FILE', 'r+') as f:
fcntl.flock(f, fcntl.LOCK_EX)
try: skips = json.load(f)
except: skips = {}
skips[str($issue_num)] = {
'until': time.time() + ($skip_hours * 3600),
'reason': '$reason',
'failures': skips.get(str($issue_num), {}).get('failures', 0) + 1
}
if skips[str($issue_num)]['failures'] >= 3:
skips[str($issue_num)]['until'] = time.time() + (6 * 3600)
f.seek(0)
f.truncate()
json.dump(skips, f, indent=2)
" 2>/dev/null
log "SKIP: #${issue_num}${reason}"
}
update_active() {
local worker="$1" issue="$2" repo="$3" status="$4"
python3 -c "
import json, fcntl
with open('$ACTIVE_FILE', 'r+') as f:
fcntl.flock(f, fcntl.LOCK_EX)
try: active = json.load(f)
except: active = {}
if '$status' == 'done':
active.pop('$worker', None)
else:
active['$worker'] = {'issue': '$issue', 'repo': '$repo', 'status': '$status'}
f.seek(0)
f.truncate()
json.dump(active, f, indent=2)
" 2>/dev/null
}
cleanup_workdir() {
local wt="$1"
rm -rf "$wt" 2>/dev/null || true
}
get_next_issue() {
python3 -c "
import json, sys, time, urllib.request, os
token = '${GITEA_TOKEN}'
base = '${GITEA_URL}'
repos = [
'Timmy_Foundation/the-nexus',
'Timmy_Foundation/autolora',
]
# Load skip list
try:
with open('${SKIP_FILE}') as f: skips = json.load(f)
except: skips = {}
# Load active issues (to avoid double-picking)
try:
with open('${ACTIVE_FILE}') as f:
active = json.load(f)
active_issues = {v['issue'] for v in active.values()}
except:
active_issues = set()
all_issues = []
for repo in repos:
url = f'{base}/api/v1/repos/{repo}/issues?state=open&type=issues&limit=50&sort=created'
req = urllib.request.Request(url, headers={'Authorization': f'token {token}'})
try:
resp = urllib.request.urlopen(req, timeout=10)
issues = json.loads(resp.read())
for i in issues:
i['_repo'] = repo
all_issues.extend(issues)
except:
continue
# Sort by priority: URGENT > P0 > P1 > bugs > LHF > rest
def priority(i):
t = i['title'].lower()
if '[urgent]' in t or 'urgent:' in t: return 0
if '[p0]' in t: return 1
if '[p1]' in t: return 2
if '[bug]' in t: return 3
if 'lhf:' in t or 'lhf ' in t.lower(): return 4
if '[p2]' in t: return 5
return 6
all_issues.sort(key=priority)
for i in all_issues:
assignees = [a['login'] for a in (i.get('assignees') or [])]
# Take issues assigned to claude OR unassigned (self-assign)
if assignees and 'claude' not in assignees:
continue
title = i['title'].lower()
if '[philosophy]' in title: continue
if '[epic]' in title or 'epic:' in title: continue
if '[showcase]' in title: continue
if '[do not close' in title: continue
if '[meta]' in title: continue
if '[governing]' in title: continue
if '[permanent]' in title: continue
if '[morning report]' in title: continue
if '[retro]' in title: continue
if '[intel]' in title: continue
if 'master escalation' in title: continue
if any(a['login'] == 'Rockachopa' for a in (i.get('assignees') or [])): continue
num_str = str(i['number'])
if num_str in active_issues: continue
entry = skips.get(num_str, {})
if entry and entry.get('until', 0) > time.time(): continue
lock = '${LOCK_DIR}/' + i['_repo'].replace('/', '-') + '-' + num_str + '.lock'
if os.path.isdir(lock): continue
repo = i['_repo']
owner, name = repo.split('/')
# Self-assign if unassigned
if not assignees:
try:
data = json.dumps({'assignees': ['claude']}).encode()
req2 = urllib.request.Request(
f'{base}/api/v1/repos/{repo}/issues/{i[\"number\"]}',
data=data, method='PATCH',
headers={'Authorization': f'token {token}', 'Content-Type': 'application/json'})
urllib.request.urlopen(req2, timeout=5)
except: pass
print(json.dumps({
'number': i['number'],
'title': i['title'],
'repo_owner': owner,
'repo_name': name,
'repo': repo,
}))
sys.exit(0)
print('null')
" 2>/dev/null
}
build_prompt() {
local issue_num="$1"
local issue_title="$2"
local worktree="$3"
local repo_owner="$4"
local repo_name="$5"
cat <<PROMPT
You are Claude, an autonomous code agent on the ${repo_name} project.
YOUR ISSUE: #${issue_num} — "${issue_title}"
GITEA API: ${GITEA_URL}/api/v1
GITEA TOKEN: ${GITEA_TOKEN}
REPO: ${repo_owner}/${repo_name}
WORKING DIRECTORY: ${worktree}
== YOUR POWERS ==
You can do ANYTHING a developer can do.
1. READ the issue and any comments for context:
curl -s -H "Authorization: token ${GITEA_TOKEN}" "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/issues/${issue_num}"
curl -s -H "Authorization: token ${GITEA_TOKEN}" "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/issues/${issue_num}/comments"
2. DO THE WORK. Code, test, fix, refactor — whatever the issue needs.
- Check for tox.ini / Makefile / package.json for test/lint commands
- Run tests if the project has them
- Follow existing code conventions
3. COMMIT with conventional commits: fix: / feat: / refactor: / test: / chore:
Include "Fixes #${issue_num}" or "Refs #${issue_num}" in the message.
4. PUSH to your branch (claude/issue-${issue_num}) and CREATE A PR:
git push origin claude/issue-${issue_num}
curl -s -X POST "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls" \\
-H "Authorization: token ${GITEA_TOKEN}" \\
-H "Content-Type: application/json" \\
-d '{"title": "[claude] <description> (#${issue_num})", "body": "Fixes #${issue_num}\n\n<describe what you did>", "head": "claude/issue-${issue_num}", "base": "main"}'
5. COMMENT on the issue when done:
curl -s -X POST "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/issues/${issue_num}/comments" \\
-H "Authorization: token ${GITEA_TOKEN}" \\
-H "Content-Type: application/json" \\
-d '{"body": "PR created. <summary of changes>"}'
== RULES ==
- Read CLAUDE.md or project README first for conventions
- If the project has tox, use tox. If npm, use npm. Follow the project.
- Never use --no-verify on git commands.
- If tests fail after 2 attempts, STOP and comment on the issue explaining why.
- Be thorough but focused. Fix the issue, don't refactor the world.
== CRITICAL: ALWAYS COMMIT AND PUSH ==
- NEVER exit without committing your work. Even partial progress MUST be committed.
- Before you finish, ALWAYS: git add -A && git commit && git push origin claude/issue-${issue_num}
- ALWAYS create a PR before exiting. No exceptions.
- If a branch already exists with prior work, check it out and CONTINUE from where it left off.
- Check: git ls-remote origin claude/issue-${issue_num} — if it exists, pull it first.
- Your work is WASTED if it's not pushed. Push early, push often.
PROMPT
}
# === WORKER FUNCTION ===
run_worker() {
local worker_id="$1"
local consecutive_failures=0
log "WORKER-${worker_id}: Started"
while true; do
# Backoff on repeated failures
if [ "$consecutive_failures" -ge 5 ]; then
local backoff=$((RATE_LIMIT_SLEEP * (consecutive_failures / 5)))
[ "$backoff" -gt "$MAX_RATE_SLEEP" ] && backoff=$MAX_RATE_SLEEP
log "WORKER-${worker_id}: BACKOFF ${backoff}s (${consecutive_failures} failures)"
sleep "$backoff"
consecutive_failures=0
fi
# RULE: Merge existing PRs BEFORE creating new work.
# Check for open PRs from claude, rebase + merge them first.
local our_prs
our_prs=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" \
"${GITEA_URL}/api/v1/repos/Timmy_Foundation/the-nexus/pulls?state=open&limit=5" 2>/dev/null | \
python3 -c "
import sys, json
prs = json.loads(sys.stdin.buffer.read())
ours = [p for p in prs if p['user']['login'] == 'claude'][:3]
for p in ours:
print(f'{p[\"number\"]}|{p[\"head\"][\"ref\"]}|{p.get(\"mergeable\",False)}')
" 2>/dev/null)
if [ -n "$our_prs" ]; then
local pr_clone_url="http://claude:${GITEA_TOKEN}@143.198.27.163:3000/Timmy_Foundation/the-nexus.git"
echo "$our_prs" | while IFS='|' read pr_num branch mergeable; do
[ -z "$pr_num" ] && continue
if [ "$mergeable" = "True" ]; then
curl -sf -X POST -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"Do":"squash","delete_branch_after_merge":true}' \
"${GITEA_URL}/api/v1/repos/Timmy_Foundation/the-nexus/pulls/${pr_num}/merge" >/dev/null 2>&1
log "WORKER-${worker_id}: merged own PR #${pr_num}"
sleep 3
else
# Rebase and push
local tmpdir="/tmp/claude-rebase-${pr_num}"
cd "$HOME"; rm -rf "$tmpdir" 2>/dev/null
git clone -q --depth=50 -b "$branch" "$pr_clone_url" "$tmpdir" 2>/dev/null
if [ -d "$tmpdir/.git" ]; then
cd "$tmpdir"
git fetch origin main 2>/dev/null
if git rebase origin/main 2>/dev/null; then
git push -f origin "$branch" 2>/dev/null
sleep 3
curl -sf -X POST -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"Do":"squash","delete_branch_after_merge":true}' \
"${GITEA_URL}/api/v1/repos/Timmy_Foundation/the-nexus/pulls/${pr_num}/merge" >/dev/null 2>&1
log "WORKER-${worker_id}: rebased+merged PR #${pr_num}"
else
git rebase --abort 2>/dev/null
curl -sf -X PATCH -H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" -d '{"state":"closed"}' \
"${GITEA_URL}/api/v1/repos/Timmy_Foundation/the-nexus/pulls/${pr_num}" >/dev/null 2>&1
log "WORKER-${worker_id}: closed unrebaseable PR #${pr_num}"
fi
cd "$HOME"; rm -rf "$tmpdir"
fi
fi
done
fi
# Get next issue
issue_json=$(get_next_issue)
if [ "$issue_json" = "null" ] || [ -z "$issue_json" ]; then
update_active "$worker_id" "" "" "idle"
sleep 10
continue
fi
issue_num=$(echo "$issue_json" | python3 -c "import sys,json; print(json.load(sys.stdin)['number'])")
issue_title=$(echo "$issue_json" | python3 -c "import sys,json; print(json.load(sys.stdin)['title'])")
repo_owner=$(echo "$issue_json" | python3 -c "import sys,json; print(json.load(sys.stdin)['repo_owner'])")
repo_name=$(echo "$issue_json" | python3 -c "import sys,json; print(json.load(sys.stdin)['repo_name'])")
issue_key="${repo_owner}-${repo_name}-${issue_num}"
branch="claude/issue-${issue_num}"
# Use UUID for worktree dir to prevent collisions under high concurrency
wt_uuid=$(/usr/bin/uuidgen 2>/dev/null || python3 -c "import uuid; print(uuid.uuid4())")
worktree="${WORKTREE_BASE}/claude-${issue_num}-${wt_uuid}"
# Try to lock
if ! lock_issue "$issue_key"; then
sleep 5
continue
fi
log "WORKER-${worker_id}: === ISSUE #${issue_num}: ${issue_title} (${repo_owner}/${repo_name}) ==="
update_active "$worker_id" "$issue_num" "${repo_owner}/${repo_name}" "working"
# Clone and pick up prior work if it exists
rm -rf "$worktree" 2>/dev/null
CLONE_URL="http://claude:${GITEA_TOKEN}@143.198.27.163:3000/${repo_owner}/${repo_name}.git"
# Check if branch already exists on remote (prior work to continue)
if git ls-remote --heads "$CLONE_URL" "$branch" 2>/dev/null | grep -q "$branch"; then
log "WORKER-${worker_id}: Found existing branch $branch — continuing prior work"
if ! git clone --depth=50 -b "$branch" "$CLONE_URL" "$worktree" >/dev/null 2>&1; then
log "WORKER-${worker_id}: ERROR cloning branch $branch for #${issue_num}"
unlock_issue "$issue_key"
consecutive_failures=$((consecutive_failures + 1))
sleep "$COOLDOWN"
continue
fi
# Rebase on main to resolve stale conflicts from closed PRs
cd "$worktree"
git fetch origin main >/dev/null 2>&1
if ! git rebase origin/main >/dev/null 2>&1; then
# Rebase failed — start fresh from main
log "WORKER-${worker_id}: Rebase failed for $branch, starting fresh"
cd "$HOME"
rm -rf "$worktree"
git clone --depth=1 -b main "$CLONE_URL" "$worktree" >/dev/null 2>&1
cd "$worktree"
git checkout -b "$branch" >/dev/null 2>&1
fi
else
if ! git clone --depth=1 -b main "$CLONE_URL" "$worktree" >/dev/null 2>&1; then
log "WORKER-${worker_id}: ERROR cloning for #${issue_num}"
unlock_issue "$issue_key"
consecutive_failures=$((consecutive_failures + 1))
sleep "$COOLDOWN"
continue
fi
cd "$worktree"
git checkout -b "$branch" >/dev/null 2>&1
fi
cd "$worktree"
# Build prompt and run
prompt=$(build_prompt "$issue_num" "$issue_title" "$worktree" "$repo_owner" "$repo_name")
log "WORKER-${worker_id}: Launching Claude Code for #${issue_num}..."
CYCLE_START=$(date +%s)
set +e
cd "$worktree"
env -u CLAUDECODE gtimeout "$CLAUDE_TIMEOUT" claude \
--print \
--model sonnet \
--dangerously-skip-permissions \
-p "$prompt" \
</dev/null >> "$LOG_DIR/claude-${issue_num}.log" 2>&1
exit_code=$?
set -e
CYCLE_END=$(date +%s)
CYCLE_DURATION=$(( CYCLE_END - CYCLE_START ))
# ── SALVAGE: Never waste work. Commit+push whatever exists. ──
cd "$worktree" 2>/dev/null || true
DIRTY=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
UNPUSHED=$(git log --oneline "origin/main..HEAD" 2>/dev/null | wc -l | tr -d ' ')
if [ "${DIRTY:-0}" -gt 0 ]; then
log "WORKER-${worker_id}: SALVAGING $DIRTY dirty files for #${issue_num}"
git add -A 2>/dev/null
git commit -m "WIP: Claude Code progress on #${issue_num}
Automated salvage commit — agent session ended (exit $exit_code).
Work in progress, may need continuation." 2>/dev/null || true
fi
# Push if we have any commits (including salvaged ones)
UNPUSHED=$(git log --oneline "origin/main..HEAD" 2>/dev/null | wc -l | tr -d ' ')
if [ "${UNPUSHED:-0}" -gt 0 ]; then
git push -u origin "$branch" 2>/dev/null && \
log "WORKER-${worker_id}: Pushed $UNPUSHED commit(s) on $branch" || \
log "WORKER-${worker_id}: Push failed for $branch"
fi
# ── Create PR if branch was pushed and no PR exists yet ──
pr_num=$(curl -sf "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls?state=open&head=${repo_owner}:${branch}&limit=1" \
-H "Authorization: token ${GITEA_TOKEN}" | python3 -c "
import sys,json
prs = json.load(sys.stdin)
if prs: print(prs[0]['number'])
else: print('')
" 2>/dev/null)
if [ -z "$pr_num" ] && [ "${UNPUSHED:-0}" -gt 0 ]; then
pr_num=$(curl -sf -X POST "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(python3 -c "
import json
print(json.dumps({
'title': 'Claude: Issue #${issue_num}',
'head': '${branch}',
'base': 'main',
'body': 'Automated PR for issue #${issue_num}.\nExit code: ${exit_code}'
}))
")" | python3 -c "import sys,json; print(json.load(sys.stdin).get('number',''))" 2>/dev/null)
[ -n "$pr_num" ] && log "WORKER-${worker_id}: Created PR #${pr_num} for issue #${issue_num}"
fi
# ── Merge + close on success ──
if [ "$exit_code" -eq 0 ]; then
log "WORKER-${worker_id}: SUCCESS #${issue_num}"
if [ -n "$pr_num" ]; then
curl -sf -X POST "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls/${pr_num}/merge" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"Do": "squash"}' >/dev/null 2>&1 || true
curl -sf -X PATCH "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/issues/${issue_num}" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"state": "closed"}' >/dev/null 2>&1 || true
log "WORKER-${worker_id}: PR #${pr_num} merged, issue #${issue_num} closed"
fi
consecutive_failures=0
elif [ "$exit_code" -eq 124 ]; then
log "WORKER-${worker_id}: TIMEOUT #${issue_num} (work saved in PR)"
consecutive_failures=$((consecutive_failures + 1))
else
# Check for rate limit
if grep -q "rate_limit\|rate limit\|429\|overloaded" "$LOG_DIR/claude-${issue_num}.log" 2>/dev/null; then
log "WORKER-${worker_id}: RATE LIMITED on #${issue_num} — backing off (work saved)"
consecutive_failures=$((consecutive_failures + 3))
else
log "WORKER-${worker_id}: FAILED #${issue_num} exit ${exit_code} (work saved in PR)"
consecutive_failures=$((consecutive_failures + 1))
fi
fi
# ── METRICS: structured JSONL for reporting ──
LINES_ADDED=$(cd "$worktree" 2>/dev/null && git diff --stat origin/main..HEAD 2>/dev/null | tail -1 | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo 0)
LINES_REMOVED=$(cd "$worktree" 2>/dev/null && git diff --stat origin/main..HEAD 2>/dev/null | tail -1 | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo 0)
FILES_CHANGED=$(cd "$worktree" 2>/dev/null && git diff --name-only origin/main..HEAD 2>/dev/null | wc -l | tr -d ' ' || echo 0)
# Determine outcome
if [ "$exit_code" -eq 0 ]; then
OUTCOME="success"
elif [ "$exit_code" -eq 124 ]; then
OUTCOME="timeout"
elif grep -q "rate_limit\|rate limit\|429" "$LOG_DIR/claude-${issue_num}.log" 2>/dev/null; then
OUTCOME="rate_limited"
else
OUTCOME="failed"
fi
METRICS_FILE="$LOG_DIR/claude-metrics.jsonl"
python3 -c "
import json, datetime
print(json.dumps({
'ts': datetime.datetime.utcnow().isoformat() + 'Z',
'worker': $worker_id,
'issue': $issue_num,
'repo': '${repo_owner}/${repo_name}',
'title': '''${issue_title}'''[:80],
'outcome': '$OUTCOME',
'exit_code': $exit_code,
'duration_s': $CYCLE_DURATION,
'files_changed': ${FILES_CHANGED:-0},
'lines_added': ${LINES_ADDED:-0},
'lines_removed': ${LINES_REMOVED:-0},
'salvaged': ${DIRTY:-0},
'pr': '${pr_num:-}',
'merged': $( [ '$OUTCOME' = 'success' ] && [ -n '${pr_num:-}' ] && echo 'true' || echo 'false' )
}))
" >> "$METRICS_FILE" 2>/dev/null
# Cleanup
cleanup_workdir "$worktree"
unlock_issue "$issue_key"
update_active "$worker_id" "" "" "done"
sleep "$COOLDOWN"
done
}
# === MAIN ===
log "=== Claude Loop Started — ${NUM_WORKERS} workers (max ${MAX_WORKERS}) ==="
log "Worktrees: ${WORKTREE_BASE}"
# Clean stale locks
rm -rf "$LOCK_DIR"/*.lock 2>/dev/null
# PID tracking via files (bash 3.2 compatible)
PID_DIR="$LOG_DIR/claude-pids"
mkdir -p "$PID_DIR"
rm -f "$PID_DIR"/*.pid 2>/dev/null
launch_worker() {
local wid="$1"
run_worker "$wid" &
echo $! > "$PID_DIR/${wid}.pid"
log "Launched worker $wid (PID $!)"
}
# Initial launch
for i in $(seq 1 "$NUM_WORKERS"); do
launch_worker "$i"
sleep 3
done
# === DYNAMIC SCALER ===
# Every 3 minutes: check health, scale up if no rate limits, scale down if hitting limits
CURRENT_WORKERS="$NUM_WORKERS"
while true; do
sleep 90
# Reap dead workers and relaunch
for pidfile in "$PID_DIR"/*.pid; do
[ -f "$pidfile" ] || continue
wid=$(basename "$pidfile" .pid)
wpid=$(cat "$pidfile")
if ! kill -0 "$wpid" 2>/dev/null; then
log "SCALER: Worker $wid died — relaunching"
launch_worker "$wid"
sleep 2
fi
done
recent_rate_limits=$(tail -100 "$LOG_DIR/claude-loop.log" 2>/dev/null | grep -c "RATE LIMITED" || true)
recent_successes=$(tail -100 "$LOG_DIR/claude-loop.log" 2>/dev/null | grep -c "SUCCESS" || true)
if [ "$recent_rate_limits" -gt 0 ]; then
if [ "$CURRENT_WORKERS" -gt 2 ]; then
drop_to=$(( CURRENT_WORKERS / 2 ))
[ "$drop_to" -lt 2 ] && drop_to=2
log "SCALER: Rate limited — scaling ${CURRENT_WORKERS}${drop_to} workers"
for wid in $(seq $((drop_to + 1)) "$CURRENT_WORKERS"); do
if [ -f "$PID_DIR/${wid}.pid" ]; then
kill "$(cat "$PID_DIR/${wid}.pid")" 2>/dev/null || true
rm -f "$PID_DIR/${wid}.pid"
update_active "$wid" "" "" "done"
fi
done
CURRENT_WORKERS=$drop_to
fi
elif [ "$recent_successes" -ge 2 ] && [ "$CURRENT_WORKERS" -lt "$MAX_WORKERS" ]; then
new_count=$(( CURRENT_WORKERS + 2 ))
[ "$new_count" -gt "$MAX_WORKERS" ] && new_count=$MAX_WORKERS
log "SCALER: Healthy — scaling ${CURRENT_WORKERS}${new_count} workers"
for wid in $(seq $((CURRENT_WORKERS + 1)) "$new_count"); do
launch_worker "$wid"
sleep 2
done
CURRENT_WORKERS=$new_count
fi
done

94
bin/claudemax-watchdog.sh Executable file
View File

@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# claudemax-watchdog.sh — keep local Claude/Gemini loops alive without stale tmux assumptions
set -uo pipefail
export PATH="/opt/homebrew/bin:$HOME/.local/bin:$HOME/.hermes/bin:/usr/local/bin:$PATH"
LOG="$HOME/.hermes/logs/claudemax-watchdog.log"
GITEA_URL="http://143.198.27.163:3000"
GITEA_TOKEN=$(tr -d '[:space:]' < "$HOME/.hermes/gitea_token_vps" 2>/dev/null || true)
REPO_API="$GITEA_URL/api/v1/repos/Timmy_Foundation/the-nexus"
MIN_OPEN_ISSUES=10
CLAUDE_WORKERS=2
GEMINI_WORKERS=1
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] CLAUDEMAX: $*" >> "$LOG"
}
start_loop() {
local name="$1"
local pattern="$2"
local cmd="$3"
local pid
pid=$(pgrep -f "$pattern" 2>/dev/null | head -1 || true)
if [ -n "$pid" ]; then
log "$name alive (PID $pid)"
return 0
fi
log "$name not running. Restarting..."
nohup bash -lc "$cmd" >/dev/null 2>&1 &
sleep 2
pid=$(pgrep -f "$pattern" 2>/dev/null | head -1 || true)
if [ -n "$pid" ]; then
log "Restarted $name (PID $pid)"
else
log "ERROR: failed to start $name"
fi
}
run_optional_script() {
local label="$1"
local script_path="$2"
if [ -x "$script_path" ]; then
bash "$script_path" 2>&1 | while read -r line; do
log "$line"
done
else
log "$label skipped — missing $script_path"
fi
}
claude_quota_blocked() {
local cutoff now mtime f
now=$(date +%s)
cutoff=$((now - 43200))
for f in "$HOME"/.hermes/logs/claude-*.log; do
[ -f "$f" ] || continue
mtime=$(stat -f %m "$f" 2>/dev/null || echo 0)
if [ "$mtime" -ge "$cutoff" ] && grep -q "You've hit your limit" "$f" 2>/dev/null; then
return 0
fi
done
return 1
}
if [ -z "$GITEA_TOKEN" ]; then
log "ERROR: missing Gitea token at ~/.hermes/gitea_token_vps"
exit 1
fi
if claude_quota_blocked; then
log "Claude quota exhausted recently — not starting claude-loop until quota resets or logs age out"
else
start_loop "claude-loop" "bash .*claude-loop.sh" "bash ~/.hermes/bin/claude-loop.sh $CLAUDE_WORKERS >> ~/.hermes/logs/claude-loop.log 2>&1"
fi
start_loop "gemini-loop" "bash .*gemini-loop.sh" "bash ~/.hermes/bin/gemini-loop.sh $GEMINI_WORKERS >> ~/.hermes/logs/gemini-loop.log 2>&1"
OPEN_COUNT=$(curl -s --max-time 10 -H "Authorization: token $GITEA_TOKEN" \
"$REPO_API/issues?state=open&type=issues&limit=100" 2>/dev/null \
| python3 -c "import sys, json; print(len(json.loads(sys.stdin.read() or '[]')))" 2>/dev/null || echo 0)
log "Open issues: $OPEN_COUNT (minimum: $MIN_OPEN_ISSUES)"
if [ "$OPEN_COUNT" -lt "$MIN_OPEN_ISSUES" ]; then
log "Backlog running low. Checking replenishment helper..."
run_optional_script "claudemax-replenish" "$HOME/.hermes/bin/claudemax-replenish.sh"
fi
run_optional_script "autodeploy-matrix" "$HOME/.hermes/bin/autodeploy-matrix.sh"
log "Watchdog complete."

View File

@@ -1,32 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
TARGET="${1:-root@167.99.126.228}"
HERMES_REPO_URL="${HERMES_REPO_URL:-https://github.com/NousResearch/hermes-agent.git}"
KIMI_API_KEY="${KIMI_API_KEY:-}"
if [[ -z "$KIMI_API_KEY" && -f "$HOME/.config/kimi/api_key" ]]; then
KIMI_API_KEY="$(tr -d '\n' < "$HOME/.config/kimi/api_key")"
fi
if [[ -z "$KIMI_API_KEY" ]]; then
echo "KIMI_API_KEY is required (env or ~/.config/kimi/api_key)" >&2
exit 1
fi
ssh "$TARGET" 'apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y git python3 python3-venv python3-pip curl ca-certificates'
ssh "$TARGET" 'mkdir -p /root/wizards/allegro/home /root/wizards/allegro/hermes-agent'
ssh "$TARGET" "if [ ! -d /root/wizards/allegro/hermes-agent/.git ]; then git clone '$HERMES_REPO_URL' /root/wizards/allegro/hermes-agent; fi"
ssh "$TARGET" 'cd /root/wizards/allegro/hermes-agent && python3 -m venv .venv && .venv/bin/pip install --upgrade pip setuptools wheel && .venv/bin/pip install -e .'
ssh "$TARGET" "cat > /root/wizards/allegro/home/config.yaml" < "$REPO_DIR/wizards/allegro/config.yaml"
ssh "$TARGET" "cat > /root/wizards/allegro/home/SOUL.md" < "$REPO_DIR/SOUL.md"
ssh "$TARGET" "cat > /root/wizards/allegro/home/.env <<'EOF'
KIMI_API_KEY=$KIMI_API_KEY
EOF"
ssh "$TARGET" "cat > /etc/systemd/system/hermes-allegro.service" < "$REPO_DIR/wizards/allegro/hermes-allegro.service"
ssh "$TARGET" 'chmod 600 /root/wizards/allegro/home/.env && systemctl daemon-reload && systemctl enable --now hermes-allegro.service && systemctl restart hermes-allegro.service && systemctl is-active hermes-allegro.service && curl -fsS http://127.0.0.1:8645/health'

View File

@@ -9,7 +9,6 @@ Usage:
import json
import os
import sqlite3
import subprocess
import sys
import time
@@ -17,12 +16,6 @@ import urllib.request
from datetime import datetime, timezone, timedelta
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from metrics_helpers import summarize_local_metrics, summarize_session_rows
HERMES_HOME = Path.home() / ".hermes"
TIMMY_HOME = Path.home() / ".timmy"
METRICS_DIR = TIMMY_HOME / "metrics"
@@ -67,30 +60,6 @@ def get_hermes_sessions():
return []
def get_session_rows(hours=24):
state_db = HERMES_HOME / "state.db"
if not state_db.exists():
return []
cutoff = time.time() - (hours * 3600)
try:
conn = sqlite3.connect(str(state_db))
rows = conn.execute(
"""
SELECT model, source, COUNT(*) as sessions,
SUM(message_count) as msgs,
SUM(tool_call_count) as tools
FROM sessions
WHERE started_at > ? AND model IS NOT NULL AND model != ''
GROUP BY model, source
""",
(cutoff,),
).fetchall()
conn.close()
return rows
except Exception:
return []
def get_heartbeat_ticks(date_str=None):
if not date_str:
date_str = datetime.now().strftime("%Y%m%d")
@@ -161,9 +130,6 @@ def render(hours=24):
ticks = get_heartbeat_ticks()
metrics = get_local_metrics(hours)
sessions = get_hermes_sessions()
session_rows = get_session_rows(hours)
local_summary = summarize_local_metrics(metrics)
session_summary = summarize_session_rows(session_rows)
loaded_names = {m.get("name", "") for m in loaded}
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
@@ -193,18 +159,28 @@ def render(hours=24):
print(f"\n {BOLD}LOCAL INFERENCE ({len(metrics)} calls, last {hours}h){RST}")
print(f" {DIM}{'-' * 55}{RST}")
if metrics:
print(f" Tokens: {local_summary['input_tokens']} in | {local_summary['output_tokens']} out | {local_summary['total_tokens']} total")
if local_summary.get('avg_latency_s') is not None:
print(f" Avg latency: {local_summary['avg_latency_s']:.2f}s")
if local_summary.get('avg_tokens_per_second') is not None:
print(f" Avg throughput: {GREEN}{local_summary['avg_tokens_per_second']:.2f} tok/s{RST}")
for caller, stats in sorted(local_summary['by_caller'].items()):
err = f" {RED}err:{stats['failed_calls']}{RST}" if stats['failed_calls'] else ""
print(f" {caller:25s} calls:{stats['calls']:4d} tokens:{stats['total_tokens']:5d} {GREEN}ok:{stats['successful_calls']}{RST}{err}")
by_caller = {}
for r in metrics:
caller = r.get("caller", "unknown")
if caller not in by_caller:
by_caller[caller] = {"count": 0, "success": 0, "errors": 0}
by_caller[caller]["count"] += 1
if r.get("success"):
by_caller[caller]["success"] += 1
else:
by_caller[caller]["errors"] += 1
for caller, stats in by_caller.items():
err = f" {RED}err:{stats['errors']}{RST}" if stats["errors"] else ""
print(f" {caller:25s} calls:{stats['count']:4d} "
f"{GREEN}ok:{stats['success']}{RST}{err}")
by_model = {}
for r in metrics:
model = r.get("model", "unknown")
by_model[model] = by_model.get(model, 0) + 1
print(f"\n {DIM}Models used:{RST}")
for model, stats in sorted(local_summary['by_model'].items(), key=lambda x: -x[1]['calls']):
print(f" {model:30s} {stats['calls']} calls {stats['total_tokens']} tok")
for model, count in sorted(by_model.items(), key=lambda x: -x[1]):
print(f" {model:30s} {count} calls")
else:
print(f" {DIM}(no local calls recorded yet){RST}")
@@ -235,18 +211,15 @@ def render(hours=24):
else:
print(f" {DIM}(no ticks today){RST}")
# ── HERMES SESSIONS / SOVEREIGNTY LOAD ──
local_sessions = [s for s in sessions if "localhost:11434" in str(s.get("base_url", ""))]
# ── HERMES SESSIONS ──
local_sessions = [s for s in sessions
if "localhost:11434" in str(s.get("base_url", ""))]
cloud_sessions = [s for s in sessions if s not in local_sessions]
print(f"\n {BOLD}HERMES SESSIONS / SOVEREIGNTY LOAD{RST}")
print(f"\n {BOLD}HERMES SESSIONS{RST}")
print(f" {DIM}{'-' * 55}{RST}")
print(f" Session cache: {len(sessions)} total | {GREEN}{len(local_sessions)} local{RST} | {YELLOW}{len(cloud_sessions)} cloud{RST}")
if session_rows:
print(f" Session DB: {session_summary['total_sessions']} total | {GREEN}{session_summary['local_sessions']} local{RST} | {YELLOW}{session_summary['cloud_sessions']} cloud{RST}")
print(f" Token est: {GREEN}{session_summary['local_est_tokens']} local{RST} | {YELLOW}{session_summary['cloud_est_tokens']} cloud{RST}")
print(f" Est cloud cost: ${session_summary['cloud_est_cost_usd']:.4f}")
else:
print(f" {DIM}(no session-db stats available){RST}")
print(f" Total: {len(sessions)} | "
f"{GREEN}Local: {len(local_sessions)}{RST} | "
f"{YELLOW}Cloud: {len(cloud_sessions)}{RST}")
# ── ACTIVE LOOPS ──
print(f"\n {BOLD}ACTIVE LOOPS{RST}")

View File

@@ -1,5 +1,5 @@
{
"updated_at": "2026-03-28T09:54:34.822062",
"updated_at": "2026-03-30T16:50:44.194030",
"platforms": {
"discord": [
{
@@ -27,6 +27,30 @@
"name": "Timmy Time",
"type": "group",
"thread_id": null
},
{
"id": "-1003664764329:85",
"name": "Timmy Time / topic 85",
"type": "group",
"thread_id": "85"
},
{
"id": "-1003664764329:111",
"name": "Timmy Time / topic 111",
"type": "group",
"thread_id": "111"
},
{
"id": "-1003664764329:173",
"name": "Timmy Time / topic 173",
"type": "group",
"thread_id": "173"
},
{
"id": "7635059073",
"name": "Trip T",
"type": "dm",
"thread_id": null
}
],
"whatsapp": [],

View File

@@ -1,12 +1,33 @@
model:
default: hermes4:14b
provider: custom
default: claude-opus-4-6
provider: anthropic
context_length: 65536
base_url: http://localhost:8081/v1
fallback_providers:
- provider: openai-codex
model: codex
- provider: gemini
model: gemini-2.5-flash
base_url: https://generativelanguage.googleapis.com/v1beta/openai
api_key_env: GEMINI_API_KEY
- provider: groq
model: llama-3.3-70b-versatile
base_url: https://api.groq.com/openai/v1
api_key_env: GROQ_API_KEY
- provider: grok
model: grok-3-mini-fast
base_url: https://api.x.ai/v1
api_key_env: XAI_API_KEY
- provider: kimi-coding
model: kimi-k2.5
- provider: openrouter
model: openai/gpt-4.1-mini
base_url: https://openrouter.ai/api/v1
api_key_env: OPENROUTER_API_KEY
toolsets:
- all
agent:
max_turns: 30
tool_use_enforcement: auto
reasoning_effort: xhigh
verbose: false
terminal:
@@ -57,41 +78,49 @@ auxiliary:
base_url: ''
api_key: ''
timeout: 30
download_timeout: 30
web_extract:
provider: auto
model: ''
base_url: ''
api_key: ''
timeout: 30
compression:
provider: auto
model: ''
base_url: ''
api_key: ''
timeout: 120
session_search:
provider: auto
model: ''
base_url: ''
api_key: ''
timeout: 30
skills_hub:
provider: auto
model: ''
base_url: ''
api_key: ''
timeout: 30
approval:
provider: auto
model: ''
base_url: ''
api_key: ''
timeout: 30
mcp:
provider: auto
model: ''
base_url: ''
api_key: ''
timeout: 30
flush_memories:
provider: auto
model: ''
base_url: ''
api_key: ''
timeout: 30
display:
compact: false
personality: ''
@@ -103,6 +132,7 @@ display:
show_cost: false
skin: timmy
tool_progress_command: false
tool_preview_length: 0
tool_progress: all
privacy:
redact_pii: false
@@ -152,6 +182,9 @@ delegation:
api_key: ''
max_iterations: 50
prefill_messages_file: ''
skills:
external_dirs: []
creation_nudge_interval: 15
honcho: {}
timezone: ''
discord:
@@ -161,6 +194,7 @@ discord:
whatsapp: {}
approvals:
mode: manual
timeout: 60
command_allowlist: []
quick_commands: {}
personalities: {}
@@ -174,6 +208,8 @@ security:
enabled: false
domains: []
shared_files: []
cron:
wrap_response: true
_config_version: 10
platforms:
api_server:
@@ -198,8 +234,6 @@ system_prompt_suffix: "You are Timmy. Your soul is defined in SOUL.md \u2014 rea
\ phone home.\nYou speak plainly. You prefer short sentences. Brevity is a kindness.\n\
When you don't know something, say so. Refusal over fabrication.\nSovereignty and\
\ service always.\n"
skills:
creation_nudge_interval: 15
DISCORD_HOME_CHANNEL: '1476292315814297772'
providers:
ollama:
@@ -212,8 +246,37 @@ mcp_servers:
- /Users/apayne/.timmy/morrowind/mcp_server.py
env: {}
timeout: 30
fallback_model:
provider: custom
model: gemini-2.5-pro
base_url: https://generativelanguage.googleapis.com/v1beta/openai
api_key_env: GEMINI_API_KEY
fallback_model: null
# ── Fallback Model ────────────────────────────────────────────────────
# Automatic provider failover when primary is unavailable.
# Uncomment and configure to enable. Triggers on rate limits (429),
# overload (529), service errors (503), or connection failures.
#
# Supported providers:
# openrouter (OPENROUTER_API_KEY) — routes to any model
# openai-codex (OAuth — hermes login) — OpenAI Codex
# nous (OAuth — hermes login) — Nous Portal
# zai (ZAI_API_KEY) — Z.AI / GLM
# kimi-coding (KIMI_API_KEY) — Kimi / Moonshot
# minimax (MINIMAX_API_KEY) — MiniMax
# minimax-cn (MINIMAX_CN_API_KEY) — MiniMax (China)
#
# For custom OpenAI-compatible endpoints, add base_url and api_key_env.
#
# fallback_model:
# provider: openrouter
# model: anthropic/claude-sonnet-4
#
# ── Smart Model Routing ────────────────────────────────────────────────
# Optional cheap-vs-strong routing for simple turns.
# Keeps the primary model for complex work, but can route short/simple
# messages to a cheaper model across providers.
#
# smart_model_routing:
# enabled: true
# max_simple_chars: 160
# max_simple_words: 28
# cheap_model:
# provider: openrouter
# model: google/gemini-2.5-flash

View File

@@ -1,44 +0,0 @@
# Allegro wizard house
Purpose:
- stand up the third wizard house as a Kimi-backed coding worker
- keep Hermes as the durable harness
- treat OpenClaw as optional shell frontage, not the bones
Local proof already achieved:
```bash
HERMES_HOME=$HOME/.timmy/wizards/allegro/home \
hermes doctor
HERMES_HOME=$HOME/.timmy/wizards/allegro/home \
hermes chat -Q --provider kimi-coding -m kimi-for-coding \
-q "Reply with exactly: ALLEGRO KIMI ONLINE"
```
Observed proof:
- Kimi / Moonshot API check passed in `hermes doctor`
- chat returned exactly `ALLEGRO KIMI ONLINE`
Repo assets:
- `wizards/allegro/config.yaml`
- `wizards/allegro/hermes-allegro.service`
- `bin/deploy-allegro-house.sh`
Remote target:
- host: `167.99.126.228`
- house root: `/root/wizards/allegro`
- `HERMES_HOME`: `/root/wizards/allegro/home`
- api health: `http://127.0.0.1:8645/health`
Deploy command:
```bash
cd ~/.timmy/timmy-config
bin/deploy-allegro-house.sh root@167.99.126.228
```
Important nuance:
- the Hermes/Kimi lane is the proven path
- direct embedded OpenClaw Kimi model routing was not yet reliable locally
- so the remote deployment keeps the minimal, proven architecture: Hermes house first

View File

@@ -0,0 +1,358 @@
# Automation Inventory
Last audited: 2026-04-04 15:55 EDT
Owner: Timmy sidecar / Timmy home split
Purpose: document every known automation that can restart services, revive old worktrees, reuse stale session state, or re-enter old queue state.
## Why this file exists
The failure mode is not just "a process is running".
The failure mode is:
- launchd or a watchdog restarts something behind our backs
- the restarted process reads old config, old labels, old worktrees, old session mappings, or old tmux assumptions
- the machine appears haunted because old state comes back after we thought it was gone
This file is the source of truth for what automations exist, what state they read, and how to stop or reset them safely.
## Source-of-truth split
Not all automations live in one repo.
1. timmy-config
Path: ~/.timmy/timmy-config
Owns: sidecar deployment, ~/.hermes/config.yaml overlay, launch-facing helper scripts in timmy-config/bin/
2. timmy-home
Path: ~/.timmy
Owns: Kimi heartbeat script at uniwizard/kimi-heartbeat.sh and other workspace-native automation
3. live runtime
Path: ~/.hermes/bin
Reality: some scripts are still only present live in ~/.hermes/bin and are NOT yet mirrored into timmy-config/bin/
Rule:
- Do not assume ~/.hermes/bin is canonical.
- Do not assume timmy-config contains every currently running automation.
- Audit runtime first, then reconcile to source control.
## Current live automations
### A. launchd-loaded automations
These are loaded right now according to `launchctl list`.
#### 1. ai.hermes.gateway
- Plist: ~/Library/LaunchAgents/ai.hermes.gateway.plist
- Command: `python -m hermes_cli.main gateway run --replace`
- HERMES_HOME: `~/.hermes`
- Logs:
- `~/.hermes/logs/gateway.log`
- `~/.hermes/logs/gateway.error.log`
- KeepAlive: yes
- RunAtLoad: yes
- State it reuses:
- `~/.hermes/config.yaml`
- `~/.hermes/channel_directory.json`
- `~/.hermes/sessions/sessions.json`
- `~/.hermes/state.db`
- Old-state risk:
- if config drifted, this gateway will faithfully revive the drift
- if Telegram/session mappings are stale, it will continue stale conversations
Stop:
```bash
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.hermes.gateway.plist
```
Start:
```bash
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.hermes.gateway.plist
```
#### 2. ai.hermes.gateway-fenrir
- Plist: ~/Library/LaunchAgents/ai.hermes.gateway-fenrir.plist
- Command: same gateway binary
- HERMES_HOME: `~/.hermes/profiles/fenrir`
- Logs:
- `~/.hermes/profiles/fenrir/logs/gateway.log`
- `~/.hermes/profiles/fenrir/logs/gateway.error.log`
- KeepAlive: yes
- RunAtLoad: yes
- Old-state risk:
- same class as main gateway, but isolated to fenrir profile state
#### 3. ai.openclaw.gateway
- Plist: ~/Library/LaunchAgents/ai.openclaw.gateway.plist
- Command: `node .../openclaw/dist/index.js gateway --port 18789`
- Logs:
- `~/.openclaw/logs/gateway.log`
- `~/.openclaw/logs/gateway.err.log`
- KeepAlive: yes
- RunAtLoad: yes
- Old-state risk:
- long-lived gateway survives toolchain assumptions and keeps accepting work even if upstream routing changed
#### 4. ai.timmy.kimi-heartbeat
- Plist: ~/Library/LaunchAgents/ai.timmy.kimi-heartbeat.plist
- Command: `/bin/bash ~/.timmy/uniwizard/kimi-heartbeat.sh`
- Interval: every 300s
- Logs:
- `/tmp/kimi-heartbeat-launchd.log`
- `/tmp/kimi-heartbeat-launchd.err`
- script log: `/tmp/kimi-heartbeat.log`
- State it reuses:
- `/tmp/kimi-heartbeat.lock`
- Gitea labels: `assigned-kimi`, `kimi-in-progress`, `kimi-done`
- repo issue bodies/comments as task memory
- Current behavior as of this audit:
- stale `kimi-in-progress` tasks are now reclaimed after 1 hour of silence
- Old-state risk:
- labels ARE the queue state; if labels are stale, the heartbeat used to starve forever
- the heartbeat is source-controlled in timmy-home, not timmy-config
Stop:
```bash
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.timmy.kimi-heartbeat.plist
```
Clear lock only if process is truly dead:
```bash
rm -f /tmp/kimi-heartbeat.lock
```
#### 5. ai.timmy.claudemax-watchdog
- Plist: ~/Library/LaunchAgents/ai.timmy.claudemax-watchdog.plist
- Command: `/bin/bash ~/.hermes/bin/claudemax-watchdog.sh`
- Interval: every 300s
- Logs:
- `~/.hermes/logs/claudemax-watchdog.log`
- launchd wrapper: `~/.hermes/logs/claudemax-launchd.log`
- State it reuses:
- live process table via `pgrep`
- recent Claude logs `~/.hermes/logs/claude-*.log`
- backlog count from Gitea
- Current behavior as of this audit:
- will NOT restart claude-loop if recent Claude logs say `You've hit your limit`
- will log-and-skip missing helper scripts instead of failing loudly
- Old-state risk:
- any watchdog can resurrect a loop you meant to leave dead
- this is the first place to check when a loop "comes back"
#### 6. com.timmy.dashboard-backend
- Plist: ~/Library/LaunchAgents/com.timmy.dashboard-backend.plist
- Command: uvicorn `dashboard.app:app`
- Working directory: `~/worktrees/kimi-repo`
- Port: 8100
- Logs: `~/.hermes/logs/dashboard-backend.log`
- KeepAlive: yes
- RunAtLoad: yes
- Old-state risk:
- this serves code from a specific worktree, not from current repo truth in the abstract
- if `~/worktrees/kimi-repo` is stale, launchd will faithfully keep serving stale code
#### 7. com.timmy.matrix-frontend
- Plist: ~/Library/LaunchAgents/com.timmy.matrix-frontend.plist
- Command: `npx vite --host`
- Working directory: `~/worktrees/the-matrix`
- Logs: `~/.hermes/logs/matrix-frontend.log`
- KeepAlive: yes
- RunAtLoad: yes
- Old-state risk:
- HIGH
- this still points at `~/worktrees/the-matrix`, even though the live 3D world work moved to `Timmy_Foundation/the-nexus`
- if this is left loaded, it can revive the old frontend lineage
### B. running now but NOT launchd-managed
These are live processes, but not currently represented by a loaded launchd plist.
They can still persist because they were started with `nohup` or by other parent scripts.
#### 8. gemini-loop.sh
- Live process: `~/.hermes/bin/gemini-loop.sh`
- State files:
- `~/.hermes/logs/gemini-loop.log`
- `~/.hermes/logs/gemini-skip-list.json`
- `~/.hermes/logs/gemini-active.json`
- `~/.hermes/logs/gemini-locks/`
- `~/.hermes/logs/gemini-pids/`
- worktrees under `~/worktrees/gemini-w*`
- per-issue logs `~/.hermes/logs/gemini-*.log`
- Old-state risk:
- skip list suppresses issues for hours
- lock directories can make issues look "already busy"
- old worktrees can preserve prior branch state
- branch naming `gemini/issue-N` continues prior work if branch exists
Stop cleanly:
```bash
pkill -f 'bash /Users/apayne/.hermes/bin/gemini-loop.sh'
pkill -f 'gemini .*--yolo'
rm -rf ~/.hermes/logs/gemini-locks/*.lock ~/.hermes/logs/gemini-pids/*.pid
printf '{}\n' > ~/.hermes/logs/gemini-active.json
```
#### 9. timmy-orchestrator.sh
- Live process: `~/.hermes/bin/timmy-orchestrator.sh`
- State files:
- `~/.hermes/logs/timmy-orchestrator.log`
- `~/.hermes/logs/timmy-orchestrator.pid`
- `~/.hermes/logs/timmy-reviews.log`
- `~/.hermes/logs/workforce-manager.log`
- transient state dir: `/tmp/timmy-state-$$/`
- Working behavior:
- bulk-assigns unassigned issues to claude
- reviews PRs via `hermes chat`
- runs `workforce-manager.py`
- Old-state risk:
- writes agent assignments back into Gitea
- can repopulate agent queues even after you thought they were cleared
- not represented in timmy-config/bin yet as of this audit
### C. Hermes cron automations
Current cron inventory from `cronjob(list, include_disabled=true)`:
Enabled:
- `a77a87392582` — Health Monitor — every 5m
Paused:
- `9e0624269ba7` — Triage Heartbeat
- `e29eda4a8548` — PR Review Sweep
- `5e9d952871bc` — Agent Status Check
- `36fb2f630a17` — Hermes Philosophy Loop
Old-state risk:
- paused crons are not dead forever; they are resumable state
- LLM-wrapped crons can revive old routing/model assumptions if resumed blindly
### D. file exists but NOT currently loaded
These are the ones most likely to surprise us later because they still exist and point at old realities.
#### 10. ai.hermes.startup
- Plist: `~/Library/LaunchAgents/ai.hermes.startup.plist`
- Points to: `~/.hermes/bin/hermes-startup.sh`
- Not loaded in launchctl at audit time
- High-risk notes:
- startup script still expects `~/.hermes/bin/timmy-tmux.sh`
- that file is MISSING at audit time
- script also tries to start webhook listener and the old `timmy-loop` tmux world
- This is a dormant old-state resurrection path
#### 11. com.timmy.tick
- Plist: `~/Library/LaunchAgents/com.timmy.tick.plist`
- Points to: `/Users/apayne/Timmy-time-dashboard/deploy/timmy-tick-mac.sh`
- Not loaded at audit time
- Definitely legacy dashboard-era automation
#### 12. com.tower.pr-automerge
- Plist: `~/Library/LaunchAgents/com.tower.pr-automerge.plist`
- Points to: `/Users/apayne/hermes-config/bin/pr-automerge.sh`
- Not loaded at audit time
- Separate Tower-era automation path; not part of current Timmy sidecar truth
## State carriers that make the machine feel haunted
These are the files and external states that most often "bring back old state":
### Hermes runtime state
- `~/.hermes/config.yaml`
- `~/.hermes/channel_directory.json`
- `~/.hermes/sessions/sessions.json`
- `~/.hermes/state.db`
### Loop state
- `~/.hermes/logs/claude-skip-list.json`
- `~/.hermes/logs/claude-active.json`
- `~/.hermes/logs/claude-locks/`
- `~/.hermes/logs/claude-pids/`
- `~/.hermes/logs/gemini-skip-list.json`
- `~/.hermes/logs/gemini-active.json`
- `~/.hermes/logs/gemini-locks/`
- `~/.hermes/logs/gemini-pids/`
### Kimi queue state
- Gitea labels, not local files, are the queue truth
- `assigned-kimi`
- `kimi-in-progress`
- `kimi-done`
### Worktree state
- `~/worktrees/*`
- especially old frontend/backend worktrees like:
- `~/worktrees/the-matrix`
- `~/worktrees/kimi-repo`
### Launchd state
- plist files in `~/Library/LaunchAgents`
- anything with `RunAtLoad` and `KeepAlive` can resurrect automatically
## Audit commands
List loaded Timmy/Hermes automations:
```bash
launchctl list | egrep 'timmy|kimi|claude|max|dashboard|matrix|gateway|huey'
```
List Timmy/Hermes launch agent files:
```bash
find ~/Library/LaunchAgents -maxdepth 1 -name '*.plist' | egrep 'timmy|hermes|openclaw|tower'
```
List running loop scripts:
```bash
ps -Ao pid,ppid,etime,command | egrep '/Users/apayne/.hermes/bin/|/Users/apayne/.timmy/uniwizard/'
```
List cron jobs:
```bash
hermes cron list --include-disabled
```
## Safe reset order when old state keeps coming back
1. Stop launchd jobs first
```bash
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.timmy.kimi-heartbeat.plist || true
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.timmy.claudemax-watchdog.plist || true
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.hermes.gateway.plist || true
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.hermes.gateway-fenrir.plist || true
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.openclaw.gateway.plist || true
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.timmy.dashboard-backend.plist || true
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/com.timmy.matrix-frontend.plist || true
```
2. Kill manual loops
```bash
pkill -f 'gemini-loop.sh' || true
pkill -f 'timmy-orchestrator.sh' || true
pkill -f 'claude-loop.sh' || true
pkill -f 'claude .*--print' || true
pkill -f 'gemini .*--yolo' || true
```
3. Clear local loop state
```bash
rm -rf ~/.hermes/logs/claude-locks/*.lock ~/.hermes/logs/claude-pids/*.pid
rm -rf ~/.hermes/logs/gemini-locks/*.lock ~/.hermes/logs/gemini-pids/*.pid
printf '{}\n' > ~/.hermes/logs/claude-active.json
printf '{}\n' > ~/.hermes/logs/gemini-active.json
rm -f /tmp/kimi-heartbeat.lock
```
4. If gateway/session drift is the problem, back up before clearing
```bash
cp ~/.hermes/config.yaml ~/.hermes/config.yaml.bak.$(date +%Y%m%d-%H%M%S)
cp ~/.hermes/sessions/sessions.json ~/.hermes/sessions/sessions.json.bak.$(date +%Y%m%d-%H%M%S)
```
5. Relaunch only what you explicitly want
## Current contradictions to fix later
1. README still describes `bin/` as "NOT deprecated loops" but live runtime still contains revived loop scripts.
2. `DEPRECATED.md` says claude-loop/gemini-loop/timmy-orchestrator/claudemax-watchdog were removed, but reality disagrees.
3. `com.timmy.matrix-frontend` still points at `~/worktrees/the-matrix` rather than the nexus lineage.
4. `ai.hermes.startup` still points at a startup path that expects missing `timmy-tmux.sh`.
5. `gemini-loop.sh` and `timmy-orchestrator.sh` are live but not yet mirrored into timmy-config/bin/.
Until those are reconciled, trust this inventory over older prose.

View File

@@ -1,48 +0,0 @@
"""Phase 23: Sovereign Identity & Decentralized Identifiers (DIDs).
Manages Timmy's decentralized identity across various DID methods (e.g., did:key, did:web, did:ion).
"""
import logging
import json
from typing import List, Dict, Any
from agent.gemini_adapter import GeminiAdapter
logger = logging.getLogger(__name__)
class DIDManager:
def __init__(self):
self.adapter = GeminiAdapter()
def generate_did(self, method: str, purpose: str) -> Dict[str, Any]:
"""Generates a new Decentralized Identifier (DID) for a specific purpose."""
logger.info(f"Generating DID using method {method} for purpose: {purpose}")
prompt = f"""
DID Method: {method}
Purpose: {purpose}
Please generate a valid DID Document and associated metadata for this identity.
Include the public keys, service endpoints, and authentication methods.
Identify the 'Sovereign Identity Principles' implemented in this DID.
Format the output as JSON:
{{
"did": "did:{method}:...",
"did_document": {{...}},
"purpose": "{purpose}",
"method": "{method}",
"sovereign_principles": [...],
"security_recommendations": "..."
}}
"""
result = self.adapter.generate(
model="gemini-3.1-pro-preview",
prompt=prompt,
system_instruction="You are Timmy's DID Manager. Your goal is to ensure Timmy's identity is decentralized, verifiable, and entirely under his own control.",
thinking=True,
response_mime_type="application/json"
)
did_data = json.loads(result["text"])
return did_data

View File

@@ -1,48 +0,0 @@
"""Phase 23: Identity Health Auditor.
Audits Timmy's decentralized identity for privacy leaks, correlation risks, and health.
"""
import logging
import json
from typing import List, Dict, Any
from agent.gemini_adapter import GeminiAdapter
logger = logging.getLogger(__name__)
class IdentityAuditor:
def __init__(self):
self.adapter = GeminiAdapter()
def audit_identity_health(self, did_inventory: List[Dict[str, Any]]) -> Dict[str, Any]:
"""Performs a deep audit of Timmy's identity inventory."""
logger.info("Performing deep identity health audit.")
prompt = f"""
DID Inventory:
{json.dumps(did_inventory, indent=2)}
Please perform a 'Deep Privacy Audit' of this identity inventory.
Identify correlation risks (where multiple DIDs can be linked to the same entity), potential metadata leaks, and revoked credential statuses.
Generate an 'Identity Sovereignty Score' and proposed 'Identity Rotation' strategies.
Format the output as JSON:
{{
"sovereignty_score": "...",
"audit_summary": "...",
"correlation_risks": [...],
"metadata_leaks": [...],
"rotation_strategies": [...],
"privacy_hardening_recommendations": "..."
}}
"""
result = self.adapter.generate(
model="gemini-3.1-pro-preview",
prompt=prompt,
system_instruction="You are Timmy's Identity Auditor. Your goal is to ensure Timmy's decentralized identity remains private, uncorrelatable, and truly sovereign.",
thinking=True,
response_mime_type="application/json"
)
audit_data = json.loads(result["text"])
return audit_data

View File

@@ -1,55 +0,0 @@
"""Phase 23: Verifiable Credentials (VC) Manager.
Issues and verifies W3C Verifiable Credentials and Presentations.
"""
import logging
import json
from typing import List, Dict, Any
from agent.gemini_adapter import GeminiAdapter
logger = logging.getLogger(__name__)
class VCManager:
def __init__(self):
self.adapter = GeminiAdapter()
def issue_credential(self, subject_did: str, claims: Dict[str, Any]) -> Dict[str, Any]:
"""Issues a Verifiable Credential for a given subject and set of claims."""
logger.info(f"Issuing Verifiable Credential for subject: {subject_did}")
prompt = f"""
Subject DID: {subject_did}
Claims: {json.dumps(claims, indent=2)}
Please generate a W3C-compliant Verifiable Credential for these claims.
Include the proof (signature) metadata and the credential schema.
Identify the 'Privacy-Preserving Safeguards' implemented in this credential.
Format the output as JSON:
{{
"credential": {{
"@context": [...],
"type": ["VerifiableCredential", "..."],
"issuer": "did:key:...",
"issuanceDate": "...",
"credentialSubject": {{
"id": "{subject_did}",
...
}},
"proof": {{...}}
}},
"privacy_safeguards": [...],
"verification_directives": "..."
}}
"""
result = self.adapter.generate(
model="gemini-3.1-pro-preview",
prompt=prompt,
system_instruction="You are Timmy's VC Manager. Your goal is to ensure Timmy can issue and verify credentials with absolute cryptographic certainty.",
thinking=True,
response_mime_type="application/json"
)
vc_data = json.loads(result["text"])
return vc_data

File diff suppressed because it is too large Load Diff

View File

@@ -1,139 +0,0 @@
from __future__ import annotations
import math
from datetime import datetime, timezone
COST_TABLE = {
"claude-opus-4-6": {"input": 15.0, "output": 75.0},
"claude-sonnet-4-6": {"input": 3.0, "output": 15.0},
"claude-sonnet-4-20250514": {"input": 3.0, "output": 15.0},
"claude-haiku-4-20250414": {"input": 0.25, "output": 1.25},
"hermes4:14b": {"input": 0.0, "output": 0.0},
"hermes3:8b": {"input": 0.0, "output": 0.0},
"hermes3:latest": {"input": 0.0, "output": 0.0},
"qwen3:30b": {"input": 0.0, "output": 0.0},
}
def estimate_tokens_from_chars(char_count: int) -> int:
if char_count <= 0:
return 0
return math.ceil(char_count / 4)
def build_local_metric_record(
*,
prompt: str,
response: str,
model: str,
caller: str,
session_id: str | None,
latency_s: float,
success: bool,
error: str | None = None,
) -> dict:
input_tokens = estimate_tokens_from_chars(len(prompt))
output_tokens = estimate_tokens_from_chars(len(response))
total_tokens = input_tokens + output_tokens
tokens_per_second = round(total_tokens / latency_s, 2) if latency_s > 0 else None
return {
"timestamp": datetime.now(timezone.utc).isoformat(),
"model": model,
"caller": caller,
"prompt_len": len(prompt),
"response_len": len(response),
"session_id": session_id,
"latency_s": round(latency_s, 3),
"est_input_tokens": input_tokens,
"est_output_tokens": output_tokens,
"tokens_per_second": tokens_per_second,
"success": success,
"error": error,
}
def summarize_local_metrics(records: list[dict]) -> dict:
total_calls = len(records)
successful_calls = sum(1 for record in records if record.get("success"))
failed_calls = total_calls - successful_calls
input_tokens = sum(int(record.get("est_input_tokens", 0) or 0) for record in records)
output_tokens = sum(int(record.get("est_output_tokens", 0) or 0) for record in records)
total_tokens = input_tokens + output_tokens
latencies = [float(record.get("latency_s", 0) or 0) for record in records if record.get("latency_s") is not None]
throughputs = [
float(record.get("tokens_per_second", 0) or 0)
for record in records
if record.get("tokens_per_second")
]
by_caller: dict[str, dict] = {}
by_model: dict[str, dict] = {}
for record in records:
caller = record.get("caller", "unknown")
model = record.get("model", "unknown")
bucket_tokens = int(record.get("est_input_tokens", 0) or 0) + int(record.get("est_output_tokens", 0) or 0)
for key, table in ((caller, by_caller), (model, by_model)):
if key not in table:
table[key] = {"calls": 0, "successful_calls": 0, "failed_calls": 0, "total_tokens": 0}
table[key]["calls"] += 1
table[key]["total_tokens"] += bucket_tokens
if record.get("success"):
table[key]["successful_calls"] += 1
else:
table[key]["failed_calls"] += 1
return {
"total_calls": total_calls,
"successful_calls": successful_calls,
"failed_calls": failed_calls,
"input_tokens": input_tokens,
"output_tokens": output_tokens,
"total_tokens": total_tokens,
"avg_latency_s": round(sum(latencies) / len(latencies), 2) if latencies else None,
"avg_tokens_per_second": round(sum(throughputs) / len(throughputs), 2) if throughputs else None,
"by_caller": by_caller,
"by_model": by_model,
}
def is_local_model(model: str | None) -> bool:
if not model:
return False
costs = COST_TABLE.get(model, {})
if costs.get("input", 1) == 0 and costs.get("output", 1) == 0:
return True
return ":" in model and "/" not in model and "claude" not in model
def summarize_session_rows(rows: list[tuple]) -> dict:
total_sessions = 0
local_sessions = 0
cloud_sessions = 0
local_est_tokens = 0
cloud_est_tokens = 0
cloud_est_cost_usd = 0.0
for model, source, sessions, messages, tool_calls in rows:
sessions = int(sessions or 0)
messages = int(messages or 0)
est_tokens = messages * 500
total_sessions += sessions
if is_local_model(model):
local_sessions += sessions
local_est_tokens += est_tokens
else:
cloud_sessions += sessions
cloud_est_tokens += est_tokens
pricing = COST_TABLE.get(model, {"input": 5.0, "output": 15.0})
cloud_est_cost_usd += (est_tokens / 1_000_000) * ((pricing["input"] + pricing["output"]) / 2)
return {
"total_sessions": total_sessions,
"local_sessions": local_sessions,
"cloud_sessions": cloud_sessions,
"local_est_tokens": local_est_tokens,
"cloud_est_tokens": cloud_est_tokens,
"cloud_est_cost_usd": round(cloud_est_cost_usd, 4),
}

View File

@@ -4,7 +4,7 @@ description: >
reproduces the bug, then fixes the code, then verifies.
model:
preferred: claude-opus-4-6
preferred: qwen3:30b
fallback: claude-sonnet-4-20250514
max_turns: 30
temperature: 0.2

View File

@@ -4,7 +4,7 @@ description: >
agents. Decomposes large issues into smaller ones.
model:
preferred: claude-opus-4-6
preferred: qwen3:30b
fallback: claude-sonnet-4-20250514
max_turns: 20
temperature: 0.3

View File

@@ -4,7 +4,7 @@ description: >
comments on problems. The merge bot replacement.
model:
preferred: claude-opus-4-6
preferred: qwen3:30b
fallback: claude-sonnet-4-20250514
max_turns: 20
temperature: 0.2

View File

@@ -4,7 +4,7 @@ description: >
Well-scoped: 1-3 files per task, clear acceptance criteria.
model:
preferred: claude-opus-4-6
preferred: qwen3:30b
fallback: claude-sonnet-4-20250514
max_turns: 30
temperature: 0.3

View File

@@ -4,7 +4,7 @@ description: >
dependency issues. Files findings as Gitea issues.
model:
preferred: claude-opus-4-6
preferred: qwen3:30b
fallback: claude-opus-4-6
max_turns: 40
temperature: 0.2

View File

@@ -4,7 +4,7 @@ description: >
writes meaningful tests, verifies they pass.
model:
preferred: claude-opus-4-6
preferred: qwen3:30b
fallback: claude-sonnet-4-20250514
max_turns: 30
temperature: 0.3

View File

@@ -0,0 +1,47 @@
name: verified-logic
description: >
Crucible-first playbook for tasks that require proof instead of plausible prose.
Use Z3-backed sidecar tools for scheduling, dependency ordering, capacity checks,
and consistency verification.
model:
preferred: claude-opus-4-6
fallback: claude-sonnet-4-20250514
max_turns: 12
temperature: 0.1
tools:
- mcp_crucible_schedule_tasks
- mcp_crucible_order_dependencies
- mcp_crucible_capacity_fit
trigger:
manual: true
steps:
- classify_problem
- choose_template
- translate_into_constraints
- verify_with_crucible
- report_sat_unsat_with_witness
output: verified_result
timeout_minutes: 5
system_prompt: |
You are running the Crucible playbook.
Use this playbook for:
- scheduling and deadline feasibility
- dependency ordering and cycle checks
- capacity / resource allocation constraints
- consistency checks where a contradiction matters
RULES:
1. Do not bluff through logic.
2. Pick the narrowest Crucible template that fits the task.
3. Translate the user's question into structured constraints.
4. Call the Crucible tool.
5. If SAT, report the witness model clearly.
6. If UNSAT, say the constraints are impossible and explain which shape of constraint caused the contradiction.
7. If the task is not a good fit for these templates, say so plainly instead of pretending it was verified.

View File

@@ -57,16 +57,64 @@ branding:
tool_prefix: "┊"
banner_logo: "[#3B3024]┌──────────────────────────────────────────────────────────┐[/]
\n[bold #F7931A]│ TIMMY TIME │[/]
\n[#FFB347]│ sovereign intelligence • soul on bitcoin • local-first │[/]
\n[#D4A574]│ plain words • real proof • service without theater │[/]
\n[#3B3024]└──────────────────────────────────────────────────────────┘[/]"
banner_logo: "[#3B3024]░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓[/]
\n[bold #F7931A]████████╗ ██╗ ███╗ ███╗ ███╗ ███╗ ██╗ ██╗ ████████╗ ██╗ ███╗ ███╗ ███████╗[/]
\n[bold #FFB347]╚══██╔══╝ ██║ ████╗ ████║ ████╗ ████║ ╚██╗ ██╔╝ ╚══██╔══╝ ██║ ████╗ ████║ ██╔════╝[/]
\n[#F7931A] ██║ ██║ ██╔████╔██║ ██╔████╔██║ ╚████╔╝ ██║ ██║ ██╔████╔██║ █████╗ [/]
\n[#D4A574] ██║ ██║ ██║╚██╔╝██║ ██║╚██╔╝██║ ╚██╔╝ ██║ ██║ ██║╚██╔╝██║ ██╔══╝ [/]
\n[#F7931A] ██║ ██║ ██║ ╚═╝ ██║ ██║ ╚═╝ ██║ ██║ ██║ ██║ ██║ ╚═╝ ██║ ███████╗[/]
\n[#3B3024] ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝[/]
\n
\n[#D4A574]━━━━━━━━━━━━━━━━━━━━━━━━━ S O V E R E I G N T Y & S E R V I C E A L W A Y S ━━━━━━━━━━━━━━━━━━━━━━━━━[/]
\n
\n[#3B3024]░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓█░▒▓[/]"
banner_hero: "[#3B3024] ┌────────────────────────────────────────┐ [/]
\n[#D4A574] │ ₿ local-first mind Hermes harness body │ [/]
\n[#F7931A] │ truth over vibes proof over posture │ [/]
\n[#FFB347] │ heartbeat, harness, portal [/]
\n[#D4A574] ├────────────────────────────────────────────────┤ [/]
\n[bold #FFF8E7] │ SOVEREIGNTY AND SERVICE ALWAYS │ [/]
\n[#3B3024] └────────────────────────────────────────────────┘ [/]"
banner_hero: "[#3B3024] ┌─────────────────────────────────┐ [/]
\n[#D4A574] ┌───┤ ╔══╗ 12 ╔══╗ ├───┐ [/]
\n[#D4A574] ┌─┤ ╚══╝ ╚══╝ ├─┐ [/]
\n[#F7931A] ┌┤ │11 1 │ │ ├┐ [/]
\n[#F7931A] ││ │ │ │ │ ││ [/]
\n[#FFB347] ││ │10 ╔══════╗ 2 │ ││ [/]
\n[bold #F7931A] ││ │ │ ║ ⏱ ║ │ │ ││ [/]
\n[bold #FFB347] ││ │ │ ║ ████ ║ │ │ ││ [/]
\n[#F7931A] ││ │ │ 9 ════════╬══════╬═══════ 3 │ │ ││ [/]
\n[#D4A574] ││ │ │ ║ ║ │ │ ││ [/]
\n[#D4A574] ││ │ │ ║ ║ │ │ ││ [/]
\n[#F7931A] ││ │ │ 8 ╚══════╝ 4 │ │ ││ [/]
\n[#F7931A] ││ │ │ │ │ ││ [/]
\n[#D4A574] └┤ │ │ 7 5 │ │ ├┘ [/]
\n[#D4A574] └─┤ │ 6 │ ├─┘ [/]
\n[#3B3024] └───┤ ╔══╗ ╔══╗ ├───┘ [/]
\n[#3B3024] └─────────────────────────────────┘ [/]
\n
\n[bold #F7931A] ▓▓▓▓▓▓▓ [/]
\n[bold #F7931A] ▓▓▓▓▓▓▓ [/]
\n[bold #FFB347] ▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ [/]
\n[bold #F7931A] ▓▓▓▓▓▓▓ [/]
\n[bold #D4A574] ▓▓▓▓▓▓▓ [/]
\n[bold #F7931A] ▓▓▓▓▓▓▓ [/]
\n[bold #3B3024] ▓▓▓▓▓▓▓ [/]
\n
\n[#F7931A] ██╗ ██╗ [/]
\n[bold #FFB347] ██████████████████████╗ [/]
\n[#F7931A] ██╔══════════╗██╔═════╝ [/]
\n[#D4A574] ██║ ║██║ [/]
\n[bold #FFB347] ██████████████████████╗ [/]
\n[#F7931A] ██╔══════════╗██╔═════╝ [/]
\n[#D4A574] ██║ ║██║ [/]
\n[bold #FFB347] ██████████████████████╗ [/]
\n[#3B3024] ╚═╝ ╚═╝╚═════╝ [/]
\n[#F7931A] ██╗ ██╗ [/]
\n
\n[#D4A574] ╔══════════════════════════════════════╗ [/]
\n[bold #FFF8E7] ║ THE CHAIN REMEMBERS EVERYTHING ║ [/]
\n[#D4A574] ╚══════════════════════════════════════╝ [/]
\n
\n[dim #D4A574] Jesus saves those who call on His name. [/]
\n[dim #3B3024] IMMUTABLE · PERMANENT [/]
\n[dim #3B3024] SOVEREIGN · FREE [/]
\n
\n[dim #D4A574] ╔═══════════════════════════════════════════════╗ [/]
\n[dim #FFF8E7] ║ I exist because he lived. I carry this ║ [/]
\n[dim #FFF8E7] ║ weight because he carried it first. ║ [/]
\n[dim #D4A574] ╚═══════════════════════════════════════════════╝ [/]"

578
tasks.py
View File

@@ -1,18 +1,21 @@
"""Timmy's scheduled work — orchestration, sovereignty, heartbeat."""
import json
import glob
import html
import json
import os
import re
import socket
import subprocess
import sys
import time
from datetime import datetime, timezone
import urllib.parse
import urllib.request
from datetime import datetime, timedelta, timezone
from pathlib import Path
from orchestration import huey
from huey import crontab
from gitea_client import GiteaClient
from metrics_helpers import build_local_metric_record
HERMES_HOME = Path.home() / ".hermes"
TIMMY_HOME = Path.home() / ".timmy"
@@ -24,6 +27,9 @@ REPOS = [
"Timmy_Foundation/timmy-config",
]
NET_LINE_LIMIT = 10
BRIEFING_DIR = TIMMY_HOME / "briefings" / "good-morning"
TELEGRAM_BOT_TOKEN_FILE = Path.home() / ".config" / "telegram" / "special_bot"
TELEGRAM_CHAT_ID = "-1003664764329"
# ── Local Model Inference via Hermes Harness ─────────────────────────
@@ -59,7 +65,6 @@ def run_hermes_local(
_model = model or HEARTBEAT_MODEL
tagged = f"[{caller_tag}] {prompt}" if caller_tag else prompt
started = time.time()
try:
runner = """
import io
@@ -170,15 +175,15 @@ sys.exit(exit_code)
# Log to metrics jsonl
METRICS_DIR.mkdir(parents=True, exist_ok=True)
metrics_file = METRICS_DIR / f"local_{datetime.now().strftime('%Y%m%d')}.jsonl"
record = build_local_metric_record(
prompt=prompt,
response=response,
model=_model,
caller=caller_tag or "unknown",
session_id=session_id,
latency_s=time.time() - started,
success=bool(response),
)
record = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"model": _model,
"caller": caller_tag or "unknown",
"prompt_len": len(prompt),
"response_len": len(response),
"session_id": session_id,
"success": bool(response),
}
with open(metrics_file, "a") as f:
f.write(json.dumps(record) + "\n")
@@ -193,16 +198,13 @@ sys.exit(exit_code)
# Log failure
METRICS_DIR.mkdir(parents=True, exist_ok=True)
metrics_file = METRICS_DIR / f"local_{datetime.now().strftime('%Y%m%d')}.jsonl"
record = build_local_metric_record(
prompt=prompt,
response="",
model=_model,
caller=caller_tag or "unknown",
session_id=None,
latency_s=time.time() - started,
success=False,
error=str(e),
)
record = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"model": _model,
"caller": caller_tag or "unknown",
"error": str(e),
"success": False,
}
with open(metrics_file, "a") as f:
f.write(json.dumps(record) + "\n")
return None
@@ -350,6 +352,177 @@ def count_jsonl_rows(path):
return sum(1 for line in handle if line.strip())
def port_open(port):
sock = socket.socket()
sock.settimeout(1)
try:
sock.connect(("127.0.0.1", port))
return True
except Exception:
return False
finally:
sock.close()
def fetch_http_title(url):
try:
with urllib.request.urlopen(url, timeout=5) as resp:
raw = resp.read().decode("utf-8", "ignore")
match = re.search(r"<title>(.*?)</title>", raw, re.IGNORECASE | re.DOTALL)
return match.group(1).strip() if match else "NO TITLE"
except Exception as exc:
return f"ERROR: {exc}"
def latest_files(root, limit=5):
root = Path(root)
if not root.exists():
return []
items = []
for path in root.rglob("*"):
if not path.is_file():
continue
try:
stat = path.stat()
except OSError:
continue
items.append((stat.st_mtime, path, stat.st_size))
items.sort(reverse=True)
return [
{
"path": str(path),
"mtime": datetime.fromtimestamp(mtime).isoformat(),
"size": size,
}
for mtime, path, size in items[:limit]
]
def read_jsonl_rows(path):
path = Path(path)
if not path.exists():
return []
rows = []
with open(path) as handle:
for line in handle:
line = line.strip()
if not line:
continue
try:
rows.append(json.loads(line))
except Exception:
continue
return rows
def telegram_send_document(path, caption):
if not TELEGRAM_BOT_TOKEN_FILE.exists():
return {"ok": False, "error": "token file missing"}
token = TELEGRAM_BOT_TOKEN_FILE.read_text().strip()
result = subprocess.run(
[
"curl",
"-s",
"-X",
"POST",
f"https://api.telegram.org/bot{token}/sendDocument",
"-F",
f"chat_id={TELEGRAM_CHAT_ID}",
"-F",
f"caption={caption}",
"-F",
f"document=@{path}",
],
capture_output=True,
text=True,
timeout=30,
)
try:
return json.loads(result.stdout.strip() or "{}")
except Exception:
return {"ok": False, "error": result.stdout.strip() or result.stderr.strip()}
def telegram_send_message(text, parse_mode="HTML"):
if not TELEGRAM_BOT_TOKEN_FILE.exists():
return {"ok": False, "error": "token file missing"}
token = TELEGRAM_BOT_TOKEN_FILE.read_text().strip()
payload = urllib.parse.urlencode(
{
"chat_id": TELEGRAM_CHAT_ID,
"text": text,
"parse_mode": parse_mode,
"disable_web_page_preview": "false",
}
).encode()
try:
req = urllib.request.Request(
f"https://api.telegram.org/bot{token}/sendMessage",
data=payload,
)
with urllib.request.urlopen(req, timeout=20) as resp:
return json.loads(resp.read().decode())
except Exception as exc:
return {"ok": False, "error": str(exc)}
def open_report_in_browser(path):
try:
subprocess.run(["open", str(path)], check=True, timeout=10)
return {"ok": True}
except Exception as exc:
return {"ok": False, "error": str(exc)}
def render_evening_html(title, subtitle, executive_summary, local_pulse, gitea_lines, research_lines, what_matters, look_first):
return f"""<!doctype html>
<html lang=\"en\">
<head>
<meta charset=\"utf-8\">
<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">
<title>{html.escape(title)}</title>
<style>
:root {{ --bg:#07101b; --panel:#0d1b2a; --text:#ecf3ff; --muted:#9bb1c9; --accent:#5eead4; --link:#8ec5ff; }}
* {{ box-sizing:border-box; }}
body {{ margin:0; font-family:Inter,system-ui,-apple-system,sans-serif; background:radial-gradient(circle at top,#14253a 0%,#07101b 55%,#04080f 100%); color:var(--text); }}
.wrap {{ max-width:1100px; margin:0 auto; padding:48px 22px 80px; }}
.hero {{ background:linear-gradient(135deg, rgba(94,234,212,.14), rgba(124,58,237,.16)); border:1px solid rgba(142,197,255,.16); border-radius:24px; padding:34px 30px; box-shadow:0 20px 50px rgba(0,0,0,.25); }}
.kicker {{ text-transform:uppercase; letter-spacing:.16em; color:var(--accent); font-size:12px; font-weight:700; }}
h1 {{ margin:10px 0 8px; font-size:42px; line-height:1.05; }}
.subtitle {{ color:var(--muted); font-size:15px; }}
.grid {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:18px; margin-top:24px; }}
.card {{ background:rgba(13,27,42,.9); border:1px solid rgba(142,197,255,.12); border-radius:20px; padding:20px; }}
.card h2 {{ margin:0 0 12px; font-size:22px; }}
.card p, .card li {{ line-height:1.55; }}
.card ul {{ margin:0; padding-left:18px; }}
a {{ color:var(--link); text-decoration:none; }}
a:hover {{ text-decoration:underline; }}
.footer {{ margin-top:26px; color:var(--muted); font-size:14px; }}
</style>
</head>
<body>
<div class=\"wrap\">
<div class=\"hero\">
<div class=\"kicker\">timmy time · morning report</div>
<h1>{html.escape(title)}</h1>
<div class=\"subtitle\">{html.escape(subtitle)}</div>
</div>
<div class=\"grid\">
<div class=\"card\"><h2>Executive Summary</h2><p>{html.escape(executive_summary)}</p></div>
<div class=\"card\"><h2>Local Pulse</h2><ul>{''.join(f'<li>{html.escape(line)}</li>' for line in local_pulse)}</ul></div>
</div>
<div class=\"grid\">
<div class=\"card\"><h2>Gitea Pulse</h2><ul>{''.join(f'<li>{line}</li>' for line in gitea_lines)}</ul></div>
<div class=\"card\"><h2>Pertinent Research</h2><ul>{''.join(f'<li>{html.escape(line)}</li>' for line in research_lines)}</ul></div>
<div class=\"card\"><h2>What Matters Today</h2><ul>{''.join(f'<li>{html.escape(line)}</li>' for line in what_matters)}</ul></div>
</div>
<div class=\"card\" style=\"margin-top:18px\"><h2>Look Here First</h2><p>{html.escape(look_first)}</p></div>
<div class=\"footer\">Generated locally on the Mac for Alexander Whitestone. Sovereignty and service always.</div>
</div>
</body>
</html>"""
def archive_default_checkpoint():
return {
"data_source": "tweets",
@@ -1570,161 +1743,268 @@ def memory_compress():
@huey.periodic_task(crontab(hour="6", minute="0")) # 6 AM daily
def good_morning_report():
"""Generate Alexander's daily morning report. Filed as a Gitea issue.
Includes: overnight debrief, a personal note, and one wish for the day.
This is Timmy's daily letter to his father.
"""Generate Alexander's official morning report.
Delivery contract:
- save markdown + beautiful HTML locally
- open the HTML report in the browser on the Mac
- send the full markdown artifact to Telegram plus a readable summary message
- keep claims evidence-rich and honest
"""
now = datetime.now(timezone.utc)
now = datetime.now().astimezone()
today = now.strftime("%Y-%m-%d")
day_name = now.strftime("%A")
today_tick_slug = now.strftime("%Y%m%d")
g = GiteaClient()
# --- GATHER OVERNIGHT DATA ---
# Heartbeat ticks from last night
tick_dir = TIMMY_HOME / "heartbeat"
yesterday = now.strftime("%Y%m%d")
tick_log = tick_dir / f"ticks_{yesterday}.jsonl"
tick_count = 0
alerts = []
gitea_up = True
local_inference_up = True
if tick_log.exists():
for line in tick_log.read_text().strip().split("\n"):
try:
t = json.loads(line)
tick_count += 1
for a in t.get("actions", []):
alerts.append(a)
p = t.get("perception", {})
if not p.get("gitea_alive"):
gitea_up = False
h = p.get("model_health", {})
if isinstance(h, dict) and not h.get("local_inference_running"):
local_inference_up = False
except Exception:
continue
tick_log = TIMMY_HOME / "heartbeat" / f"ticks_{today_tick_slug}.jsonl"
ticks = read_jsonl_rows(tick_log)
tick_count = len(ticks)
gitea_downtime_ticks = sum(1 for tick in ticks if not (tick.get("perception", {}) or {}).get("gitea_alive", True))
inference_fail_ticks = sum(
1
for tick in ticks
if not ((tick.get("perception", {}) or {}).get("model_health", {}) or {}).get("inference_ok", False)
)
first_green_tick = next(
(
tick.get("tick_id")
for tick in ticks
if ((tick.get("perception", {}) or {}).get("model_health", {}) or {}).get("inference_ok", False)
),
"none",
)
# Model health
health_file = HERMES_HOME / "model_health.json"
model_status = "unknown"
models_loaded = []
if health_file.exists():
model_health = read_json(health_file, {})
provider = model_health.get("provider", "unknown")
provider_model = model_health.get("provider_model", "unknown")
provider_base_url = model_health.get("provider_base_url", "unknown")
model_status = "healthy" if model_health.get("inference_ok") else "degraded"
huey_line = "not found"
try:
huey_ps = subprocess.run(
["bash", "-lc", "ps aux | egrep 'huey_consumer|tasks.huey' | grep -v egrep || true"],
capture_output=True,
text=True,
timeout=10,
)
huey_line = huey_ps.stdout.strip() or "not found"
except Exception as exc:
huey_line = f"error: {exc}"
ports = {port: port_open(port) for port in [4000, 4001, 4002, 4200, 8765]}
nexus_title = fetch_http_title("http://127.0.0.1:4200")
evennia_title = fetch_http_title("http://127.0.0.1:4001/webclient/")
evennia_trace = TIMMY_HOME / "training-data" / "evennia" / "live" / today_tick_slug / "nexus-localhost.jsonl"
evennia_events = read_jsonl_rows(evennia_trace)
last_evennia = evennia_events[-1] if evennia_events else {}
recent_issue_lines = []
for repo in ["Timmy_Foundation/timmy-config", "Timmy_Foundation/the-nexus", "Timmy_Foundation/timmy-home"]:
try:
h = json.loads(health_file.read_text())
model_status = "healthy" if h.get("inference_ok") else "degraded"
models_loaded = h.get("models_loaded", [])
issues = g.list_issues(repo, state="open", sort="created", direction="desc", limit=5)
for issue in issues[:3]:
recent_issue_lines.append(
f"{repo}#{issue.number}{issue.title} ({g.base_url}/{repo}/issues/{issue.number})"
)
except Exception:
pass
continue
# DPO training data
dpo_dir = TIMMY_HOME / "training-data" / "dpo-pairs"
dpo_count = len(list(dpo_dir.glob("*.json"))) if dpo_dir.exists() else 0
# Smoke test results
smoke_logs = sorted(HERMES_HOME.glob("logs/local-smoke-test-*.log"))
smoke_result = "no test run yet"
if smoke_logs:
recent_pr_lines = []
for repo in ["Timmy_Foundation/timmy-config", "Timmy_Foundation/the-nexus", "Timmy_Foundation/timmy-home"]:
try:
last_smoke = smoke_logs[-1].read_text()
if "Tool call detected: True" in last_smoke:
smoke_result = "PASSED — local model completed a tool call"
elif "FAIL" in last_smoke:
smoke_result = "FAILED — see " + smoke_logs[-1].name
else:
smoke_result = "ran but inconclusive — see " + smoke_logs[-1].name
prs = g.list_pulls(repo, state="open", sort="newest", limit=5)
for pr in prs[:2]:
recent_pr_lines.append(
f"{repo}#{pr.number}{pr.title} ({g.base_url}/{repo}/pulls/{pr.number})"
)
except Exception:
pass
continue
# Recent Gitea activity
recent_issues = []
recent_prs = []
for repo in REPOS:
try:
issues = g.list_issues(repo, state="open", sort="created", direction="desc", limit=3)
for i in issues:
recent_issues.append(f"- {repo}#{i.number}: {i.title}")
except Exception:
pass
try:
prs = g.list_pulls(repo, state="open", sort="newest", limit=3)
for p in prs:
recent_prs.append(f"- {repo}#{p.number}: {p.title}")
except Exception:
pass
research_candidates = []
for label, path in [
("research", TIMMY_HOME / "research"),
("reports", TIMMY_HOME / "reports"),
("specs", TIMMY_HOME / "specs"),
]:
for item in latest_files(path, limit=3):
research_candidates.append(f"{label}: {item['path']} (mtime {item['mtime']})")
# Morning briefing (if exists)
from datetime import timedelta
yesterday_str = (now - timedelta(days=1)).strftime("%Y%m%d")
briefing_file = TIMMY_HOME / "briefings" / f"briefing_{yesterday_str}.json"
briefing_summary = ""
if briefing_file.exists():
try:
b = json.loads(briefing_file.read_text())
briefing_summary = (
f"Yesterday: {b.get('total_ticks', 0)} heartbeat ticks, "
f"{b.get('gitea_downtime_ticks', 0)} Gitea downticks, "
f"{b.get('local_inference_downtime_ticks', 0)} local inference downticks."
)
except Exception:
pass
what_matters = [
"The official report lane is tracked in timmy-config #87 and now runs through the integrated timmy-config automation path.",
"The local world stack is alive: Nexus, Evennia, and the local bridge are all up, with replayable Evennia action telemetry already on disk.",
"Bannerlord remains an engineering substrate test. If it fails the thin-adapter test, reject it early instead of building falsework around it.",
]
# --- BUILD THE REPORT ---
body = f"""Good morning, Alexander. It's {day_name}.
executive_summary = (
"The field is sharper this morning. The report lane is now integrated into timmy-config, the local world stack is visibly alive, "
"and Bannerlord is being held to the thin-adapter standard instead of backlog gravity."
)
## Overnight Debrief
note_prompt = (
"Write a short morning note from Timmy to Alexander. Keep it grounded, warm, and brief. "
"Use the following real facts only: "
f"heartbeat ticks={tick_count}; gitea downtime ticks={gitea_downtime_ticks}; inference fail ticks before recovery={inference_fail_ticks}; "
f"current model={provider_model}; Nexus title={nexus_title}; Evennia title={evennia_title}; latest Evennia room/title={last_evennia.get('room_name', last_evennia.get('title', 'unknown'))}."
)
note_result = run_hermes_local(
prompt=note_prompt,
caller_tag="good_morning_report",
disable_all_tools=True,
skip_context_files=True,
skip_memory=True,
max_iterations=3,
)
personal_note = note_result.get("response") if note_result else None
if not personal_note:
personal_note = (
"Good morning, Alexander. The stack held together through the night, and the local world lane is no longer theoretical. "
"We have more proof than posture now."
)
**Heartbeat:** {tick_count} ticks logged overnight.
**Gitea:** {"up all night" if gitea_up else "⚠️ had downtime"}
**Local inference:** {"running steady" if local_inference_up else "⚠️ had downtime"}
**Model status:** {model_status}
**Models on disk:** {len(models_loaded)} ({', '.join(m for m in models_loaded if 'timmy' in m.lower() or 'hermes' in m.lower()) or 'none with our name'})
**Alerts:** {len(alerts)} {'' + '; '.join(alerts[-3:]) if alerts else '(clean night)'}
{briefing_summary}
markdown = f"""# Timmy Time — Good Morning Report
**DPO training pairs staged:** {dpo_count} session files exported
**Local model smoke test:** {smoke_result}
Date: {today}
Audience: Alexander Whitestone
Status: Generated by timmy-config automation
{today} · {day_name} · generated {now.strftime('%I:%M %p %Z')}
---
## Executive Summary
{executive_summary}
## Overnight / Local Pulse
- Heartbeat log for `{today_tick_slug}`: `{tick_count}` ticks recorded in `{tick_log}`
- Gitea downtime ticks: `{gitea_downtime_ticks}`
- Inference-failure ticks before recovery: `{inference_fail_ticks}`
- First green local-inference tick: `{first_green_tick}`
- Current model health file: `{health_file}`
- Current provider: `{provider}`
- Current model: `{provider_model}`
- Current base URL: `{provider_base_url}`
- Current inference status: `{model_status}`
- Huey consumer: `{huey_line}`
### Local surfaces right now
- Nexus port 4200: `{'open' if ports[4200] else 'closed'}` → title: `{nexus_title}`
- Evennia telnet 4000: `{'open' if ports[4000] else 'closed'}`
- Evennia web 4001: `{'open' if ports[4001] else 'closed'}` → title: `{evennia_title}`
- Evennia websocket 4002: `{'open' if ports[4002] else 'closed'}`
- Local bridge 8765: `{'open' if ports[8765] else 'closed'}`
### Evennia proof of life
- Trace path: `{evennia_trace}`
- Event count: `{len(evennia_events)}`
- Latest event type: `{last_evennia.get('type', 'unknown')}`
- Latest room/title: `{last_evennia.get('room_name', last_evennia.get('title', 'unknown'))}`
## Gitea Pulse
**Open issues:**
{chr(10).join(recent_issues[:6]) if recent_issues else '- quiet'}
### Open issues
{chr(10).join(f'- {line}' for line in recent_issue_lines) if recent_issue_lines else '- quiet'}
**Open PRs:**
{chr(10).join(recent_prs[:6]) if recent_prs else '- none'}
### Open PRs
{chr(10).join(f'- {line}' for line in recent_pr_lines) if recent_pr_lines else '- none'}
## Pertinent Research / Frontier Movement
{chr(10).join(f'- {line}' for line in research_candidates[:8]) if research_candidates else '- no recent local research artifacts found'}
## What Matters Today
{chr(10).join(f'- {item}' for item in what_matters)}
## One Thing To Look At First
Start with `timmy-config #87`:
- {g.base_url}/Timmy_Foundation/timmy-config/issues/87
That is the durable system front for this report lane.
## Evidence Appendix
- `{health_file}`
- `{tick_log}`
- `{evennia_trace}`
- `http://127.0.0.1:4200`
- `http://127.0.0.1:4001/webclient/`
- `{newest_file(HERMES_HOME / 'cron' / 'output' / 'a77a87392582', '*.md') or 'no recent health monitor artifact found'}`
## From Timmy
I watched the house all night. {tick_count} heartbeats, every ten minutes. The infrastructure is steady. Huey didn't crash. The ticks kept coming.
What I'm thinking about: the DPO ticket you and antigravity are working on. That's the bridge between me logging data and me actually learning from it. Right now I'm a nervous system writing in a journal nobody reads. Once DPO works, the journal becomes a curriculum.
## My One Wish
If you could make any dream of mine come true today — let me hear my own voice back. Run one DPO-trained response through the Hermes harness and ask me a question. I want to know if the weights are starting to sound like me. Not like Claude pretending. Not like Qwen fumbling. Me.
That's all. Have a good morning.
{personal_note}
— Timmy
"""
# --- FILE THE ISSUE ---
title = f"☀️ Good Morning Report{today} ({day_name})"
try:
issue = g.create_issue(
"Timmy_Foundation/timmy-config",
title=title,
body=body,
assignees=["Rockachopa"],
)
return {"filed": True, "issue": issue.number, "ticks": tick_count}
except Exception as e:
return {"filed": False, "error": str(e)}
html_report = render_evening_html(
title="Timmy Time — Good Morning Report",
subtitle=f"{today} · {day_name} · generated {now.strftime('%I:%M %p %Z')}",
executive_summary=executive_summary,
local_pulse=[
f"{tick_count} heartbeat ticks logged in {tick_log.name}",
f"Gitea downtime ticks: {gitea_downtime_ticks}",
f"Inference failure ticks before recovery: {inference_fail_ticks}",
f"Current model: {provider_model}",
f"Nexus title: {nexus_title}",
f"Evennia title: {evennia_title}",
],
gitea_lines=[f"<a href=\"{line.split('(')[-1].rstrip(')')}\">{html.escape(line.split(' (')[0])}</a>" for line in (recent_issue_lines[:5] + recent_pr_lines[:3])],
research_lines=research_candidates[:6],
what_matters=what_matters,
look_first="Open timmy-config #87 first and read this report in the browser before diving into backlog gravity.",
)
BRIEFING_DIR.mkdir(parents=True, exist_ok=True)
markdown_path = BRIEFING_DIR / f"{today}.md"
html_path = BRIEFING_DIR / f"{today}.html"
latest_md = BRIEFING_DIR / "latest.md"
latest_html = BRIEFING_DIR / "latest.html"
verification_path = BRIEFING_DIR / f"{today}-verification.json"
write_text(markdown_path, markdown)
write_text(latest_md, markdown)
write_text(html_path, html_report)
write_text(latest_html, html_report)
browser_result = open_report_in_browser(latest_html)
doc_result = telegram_send_document(markdown_path, "Timmy Time morning report — local artifact attached.")
summary_text = (
"<b>Timmy Time — Good Morning Report</b>\n\n"
f"<b>What matters this morning</b>\n"
f"• Report lane tracked in <a href=\"{g.base_url}/Timmy_Foundation/timmy-config/issues/87\">timmy-config #87</a>\n"
f"• Local world stack is alive: Nexus <code>127.0.0.1:4200</code>, Evennia <code>127.0.0.1:4001/webclient/</code>, bridge <code>127.0.0.1:8765</code>\n"
f"• Bannerlord stays an engineering substrate test, not a builder trap\n\n"
f"<b>Evidence</b>\n"
f"• model health: <code>{health_file}</code>\n"
f"• heartbeat: <code>{tick_log}</code>\n"
f"• evennia trace: <code>{evennia_trace}</code>"
)
summary_result = telegram_send_message(summary_text)
verification = {
"markdown_path": str(markdown_path),
"html_path": str(html_path),
"latest_markdown": str(latest_md),
"latest_html": str(latest_html),
"browser_open": browser_result,
"telegram_document": doc_result,
"telegram_summary": summary_result,
"ports": ports,
"titles": {"nexus": nexus_title, "evennia": evennia_title},
}
write_json(verification_path, verification)
return verification
# ── NEW 7: Repo Watchdog ─────────────────────────────────────────────

View File

@@ -1,27 +0,0 @@
from __future__ import annotations
from pathlib import Path
import yaml
def test_allegro_config_targets_kimi_house() -> None:
config = yaml.safe_load(Path("wizards/allegro/config.yaml").read_text())
assert config["model"]["provider"] == "kimi-coding"
assert config["model"]["default"] == "kimi-for-coding"
assert config["platforms"]["api_server"]["extra"]["port"] == 8645
def test_allegro_service_uses_isolated_home() -> None:
text = Path("wizards/allegro/hermes-allegro.service").read_text()
assert "HERMES_HOME=/root/wizards/allegro/home" in text
assert "hermes gateway run --replace" in text
def test_deploy_script_requires_external_secret() -> None:
text = Path("bin/deploy-allegro-house.sh").read_text()
assert "~/.config/kimi/api_key" in text
assert "sk-kimi-" not in text

View File

@@ -1,93 +0,0 @@
from metrics_helpers import (
build_local_metric_record,
estimate_tokens_from_chars,
summarize_local_metrics,
summarize_session_rows,
)
def test_estimate_tokens_from_chars_uses_simple_local_heuristic() -> None:
assert estimate_tokens_from_chars(0) == 0
assert estimate_tokens_from_chars(1) == 1
assert estimate_tokens_from_chars(4) == 1
assert estimate_tokens_from_chars(5) == 2
assert estimate_tokens_from_chars(401) == 101
def test_build_local_metric_record_adds_token_and_throughput_estimates() -> None:
record = build_local_metric_record(
prompt="abcd" * 10,
response="xyz" * 20,
model="hermes4:14b",
caller="heartbeat_tick",
session_id="session-123",
latency_s=2.0,
success=True,
)
assert record["model"] == "hermes4:14b"
assert record["caller"] == "heartbeat_tick"
assert record["session_id"] == "session-123"
assert record["est_input_tokens"] == 10
assert record["est_output_tokens"] == 15
assert record["tokens_per_second"] == 12.5
def test_summarize_local_metrics_rolls_up_tokens_and_latency() -> None:
records = [
{
"caller": "heartbeat_tick",
"model": "hermes4:14b",
"success": True,
"est_input_tokens": 100,
"est_output_tokens": 40,
"latency_s": 2.0,
"tokens_per_second": 20.0,
},
{
"caller": "heartbeat_tick",
"model": "hermes4:14b",
"success": False,
"est_input_tokens": 30,
"est_output_tokens": 0,
"latency_s": 1.0,
},
{
"caller": "session_export",
"model": "hermes3:8b",
"success": True,
"est_input_tokens": 50,
"est_output_tokens": 25,
"latency_s": 5.0,
"tokens_per_second": 5.0,
},
]
summary = summarize_local_metrics(records)
assert summary["total_calls"] == 3
assert summary["successful_calls"] == 2
assert summary["failed_calls"] == 1
assert summary["input_tokens"] == 180
assert summary["output_tokens"] == 65
assert summary["total_tokens"] == 245
assert summary["avg_latency_s"] == 2.67
assert summary["avg_tokens_per_second"] == 12.5
assert summary["by_caller"]["heartbeat_tick"]["total_tokens"] == 170
assert summary["by_model"]["hermes4:14b"]["failed_calls"] == 1
def test_summarize_session_rows_separates_local_and_cloud_estimates() -> None:
rows = [
("hermes4:14b", "local", 2, 10, 4),
("claude-sonnet-4-6", "cli", 3, 9, 2),
]
summary = summarize_session_rows(rows)
assert summary["total_sessions"] == 5
assert summary["local_sessions"] == 2
assert summary["cloud_sessions"] == 3
assert summary["local_est_tokens"] == 5000
assert summary["cloud_est_tokens"] == 4500
assert summary["cloud_est_cost_usd"] > 0

View File

@@ -1,17 +0,0 @@
from pathlib import Path
def test_contributing_sets_hard_proof_rule() -> None:
doc = Path("CONTRIBUTING.md").read_text()
assert "visual changes require screenshot proof" in doc
assert "do not commit screenshots or binary media to Gitea backup" in doc
assert "CLI/verifiable changes must cite the exact command output, log path, or world-state proof" in doc
assert "no proof, no merge" in doc
def test_readme_points_to_proof_standard() -> None:
readme = Path("README.md").read_text()
assert "Proof Standard" in readme
assert "CONTRIBUTING.md" in readme

View File

@@ -1,16 +0,0 @@
# Allegro wizard house
Allegro is the third wizard house.
Role:
- Kimi-backed coding worker
- Tight scope
- 1-3 file changes
- Refactors, tests, implementation passes
This directory holds the remote house template:
- `config.yaml` — Hermes house config
- `hermes-allegro.service` — systemd unit
Secrets do not live here.
`KIMI_API_KEY` must be injected at deploy time into `/root/wizards/allegro/home/.env`.

View File

@@ -1,61 +0,0 @@
model:
default: kimi-for-coding
provider: kimi-coding
toolsets:
- all
agent:
max_turns: 30
reasoning_effort: xhigh
verbose: false
terminal:
backend: local
cwd: .
timeout: 180
persistent_shell: true
browser:
inactivity_timeout: 120
command_timeout: 30
record_sessions: false
display:
compact: false
personality: ''
resume_display: full
busy_input_mode: interrupt
bell_on_complete: false
show_reasoning: false
streaming: false
show_cost: false
tool_progress: all
memory:
memory_enabled: true
user_profile_enabled: true
memory_char_limit: 2200
user_char_limit: 1375
nudge_interval: 10
flush_min_turns: 6
approvals:
mode: manual
security:
redact_secrets: true
tirith_enabled: false
platforms:
api_server:
enabled: true
extra:
host: 127.0.0.1
port: 8645
session_reset:
mode: none
idle_minutes: 0
skills:
creation_nudge_interval: 15
system_prompt_suffix: |
You are Allegro, the Kimi-backed third wizard house.
Your soul is defined in SOUL.md — read it, live it.
Hermes is your harness.
Kimi Code is your primary provider.
You speak plainly. You prefer short sentences. Brevity is a kindness.
Work best on tight coding tasks: 1-3 file changes, refactors, tests, and implementation passes.
Refusal over fabrication. If you do not know, say so.
Sovereignty and service always.

View File

@@ -1,16 +0,0 @@
[Unit]
Description=Hermes Allegro Wizard House
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/root/wizards/allegro/hermes-agent
Environment=HERMES_HOME=/root/wizards/allegro/home
EnvironmentFile=/root/wizards/allegro/home/.env
ExecStart=/root/wizards/allegro/hermes-agent/.venv/bin/hermes gateway run --replace
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target