Compare commits

..

27 Commits

Author SHA1 Message Date
e3a40be627 Merge pull request 'fix: repair broken CI workflows — 4 root causes fixed (#461)' (#524) from fix/ci-workflows-461 into main
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 16s
Smoke Test / smoke (push) Failing after 10s
Validate Config / YAML Lint (push) Failing after 8s
Validate Config / JSON Validate (push) Successful in 8s
Validate Config / Python Syntax & Import Check (push) Failing after 41s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 38s
Validate Config / Cron Syntax Check (push) Successful in 10s
Validate Config / Deploy Script Dry Run (push) Successful in 8s
Validate Config / Playbook Schema Validation (push) Successful in 15s
Architecture Lint / Lint Repository (push) Failing after 7s
2026-04-14 00:36:43 +00:00
efb2df8940 Merge pull request 'feat: Visual Mapping of Tower Architecture — holographic map #494' (#530) from burn/494-1776125702 into main
Some checks failed
Architecture Lint / Linter Tests (push) Has been cancelled
Architecture Lint / Lint Repository (push) Has been cancelled
Smoke Test / smoke (push) Has been cancelled
Validate Config / Shell Script Lint (push) Has been cancelled
Validate Config / Cron Syntax Check (push) Has been cancelled
Validate Config / Deploy Script Dry Run (push) Has been cancelled
Validate Config / Playbook Schema Validation (push) Has been cancelled
Validate Config / YAML Lint (push) Has been cancelled
Validate Config / JSON Validate (push) Has been cancelled
Validate Config / Python Syntax & Import Check (push) Has been cancelled
Validate Config / Python Test Suite (push) Has been cancelled
2026-04-14 00:36:38 +00:00
cf687a5bfa Merge pull request 'Session state persistence — tmux-state.json manifest' (#523) from feature/session-state-persistence-512 into main
Some checks failed
Architecture Lint / Linter Tests (push) Has been cancelled
Architecture Lint / Lint Repository (push) Has been cancelled
Smoke Test / smoke (push) Has been cancelled
Validate Config / JSON Validate (push) Has been cancelled
Validate Config / Python Syntax & Import Check (push) Has been cancelled
Validate Config / Python Test Suite (push) Has been cancelled
Validate Config / Shell Script Lint (push) Has been cancelled
Validate Config / Cron Syntax Check (push) Has been cancelled
Validate Config / Deploy Script Dry Run (push) Has been cancelled
Validate Config / Playbook Schema Validation (push) Has been cancelled
Validate Config / YAML Lint (push) Has been cancelled
2026-04-14 00:35:41 +00:00
Alexander Whitestone
c09e54de72 feat: Visual Mapping of Tower Architecture — holographic map #494
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 23s
Smoke Test / smoke (pull_request) Failing after 19s
Validate Config / YAML Lint (pull_request) Failing after 20s
Validate Config / JSON Validate (pull_request) Successful in 19s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 22s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Shell Script Lint (pull_request) Failing after 41s
Validate Config / Cron Syntax Check (pull_request) Successful in 13s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 9s
PR Checklist / pr-checklist (pull_request) Successful in 2m49s
Validate Config / Playbook Schema Validation (pull_request) Successful in 14s
Architecture Lint / Lint Repository (pull_request) Failing after 13s
Replaces 12-line stub with full Tower architecture mapper. Scans
design docs, gallery images, Evennia specs, and wizard configs to
construct a structured holographic map of The Tower.

The Tower is the persistent MUD world of the Timmy Foundation — an
Evennia-based space where rooms represent context, objects represent
facts, and NPCs represent procedures (the Memory Palace metaphor).

Sources scanned:
- grok-imagine-gallery/INDEX.md (24 gallery images → rooms)
- docs/MEMORY_ARCHITECTURE.md (Memory Palace L0-L5 layers)
- docs/*.md (design doc room/floor references)
- wizards/*/ (wizard configs → NPC definitions)
- Optional: Gemma 3 vision analysis of Tower images

Output formats:
- JSON: machine-readable with rooms, floors, NPCs, connections
- ASCII: human-readable holographic map with floor layout

Mapped: 5 floors, 20+ rooms, 6 NPCs (the fellowship).
Tests: 14/14 passing.
Closes #494
2026-04-13 20:21:07 -04:00
3214437652 fix(ci): add missing ipykernel dependency to notebook CI (#461)
Some checks failed
Validate Config / YAML Lint (pull_request) Failing after 16s
Validate Config / JSON Validate (pull_request) Successful in 15s
Validate Config / Shell Script Lint (pull_request) Failing after 43s
PR Checklist / pr-checklist (pull_request) Successful in 3m48s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 1m9s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Cron Syntax Check (pull_request) Successful in 11s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 11s
Validate Config / Playbook Schema Validation (pull_request) Successful in 18s
Architecture Lint / Linter Tests (pull_request) Failing after 1m28s
Architecture Lint / Lint Repository (pull_request) Has been skipped
Smoke Test / smoke (pull_request) Failing after 1m26s
2026-04-13 21:29:05 +00:00
95cd259867 fix(ci): move issue template into ISSUE_TEMPLATE dir (#461) 2026-04-13 21:28:52 +00:00
5e7bef1807 fix(ci): remove issue template from workflows dir — not a workflow (#461) 2026-04-13 21:28:39 +00:00
3d84dd5c27 fix(ci): fix gitea.ref typo, drop uv.lock dep, simplify hermes-sovereign CI (#461) 2026-04-13 21:28:21 +00:00
e38e80661c fix(ci): remove py_compile from pip install — it's stdlib, not a package (#461) 2026-04-13 21:28:06 +00:00
Alexander Whitestone
b71e365ed6 feat: session state persistence — tmux-state.json manifest (#512)
Implement tmux-state.sh: snapshots all tmux pane state to ~/.timmy/tmux-state.json
and ~/.hermes/tmux-state.json every supervisor cycle.

Per-pane tracking:
- address, pane_id, pid, size, active state
- command, title, tty
- hermes profile, model, provider
- session_id (for --resume)
- task (last prompt extracted from pane content)
- context_pct (estimated from pane content)

Also implement tmux-resume.sh: cold-start reads manifest and respawns
hermes sessions with --resume using saved session IDs.

Closes #512
2026-04-13 17:26:03 -04:00
c0c34cbae5 Merge pull request 'fix: repair indentation in workforce-manager.py' (#522) from fix/workforce-manager-indent into main
Some checks failed
Validate Config / Shell Script Lint (push) Failing after 13s
Validate Config / Cron Syntax Check (push) Successful in 5s
Validate Config / Deploy Script Dry Run (push) Successful in 8s
Validate Config / Playbook Schema Validation (push) Successful in 9s
Architecture Lint / Lint Repository (push) Failing after 7s
Architecture Lint / Linter Tests (push) Successful in 8s
Smoke Test / smoke (push) Failing after 7s
Validate Config / YAML Lint (push) Failing after 6s
Validate Config / JSON Validate (push) Successful in 5s
Validate Config / Python Syntax & Import Check (push) Failing after 7s
Validate Config / Python Test Suite (push) Has been skipped
fix: repair indentation in workforce-manager.py
2026-04-13 19:55:53 +00:00
Alexander Whitestone
8483a6602a fix: repair indentation in workforce-manager.py line 585
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 9s
Architecture Lint / Lint Repository (pull_request) Failing after 7s
PR Checklist / pr-checklist (pull_request) Failing after 1m18s
Smoke Test / smoke (pull_request) Failing after 7s
Validate Config / YAML Lint (pull_request) Failing after 6s
Validate Config / JSON Validate (pull_request) Successful in 6s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 7s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Shell Script Lint (pull_request) Failing after 14s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 5s
Validate Config / Cron Syntax Check (pull_request) Successful in 5s
Validate Config / Playbook Schema Validation (pull_request) Successful in 6s
logging.warning and continue were at same indent level as
the if statement instead of inside the if block.
2026-04-13 15:55:44 -04:00
af9850080a Merge pull request 'fix: repair all CI failures (smoke, lint, architecture, secret scan)' (#521) from ci/fix-all-ci-failures into main
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 9s
Smoke Test / smoke (push) Failing after 8s
Validate Config / YAML Lint (push) Failing after 6s
Validate Config / JSON Validate (push) Successful in 7s
Validate Config / Python Syntax & Import Check (push) Failing after 8s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 16s
Validate Config / Cron Syntax Check (push) Successful in 5s
Validate Config / Deploy Script Dry Run (push) Successful in 5s
Validate Config / Playbook Schema Validation (push) Successful in 9s
Architecture Lint / Lint Repository (push) Failing after 8s
Merged by Timmy overnight cycle
2026-04-13 14:02:55 +00:00
Alexander Whitestone
d50296e76b fix: repair all CI failures (smoke, lint, architecture, secret scan)
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 10s
PR Checklist / pr-checklist (pull_request) Failing after 1m25s
Smoke Test / smoke (pull_request) Failing after 8s
Validate Config / YAML Lint (pull_request) Failing after 7s
Validate Config / JSON Validate (pull_request) Successful in 7s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 8s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Shell Script Lint (pull_request) Failing after 16s
Validate Config / Cron Syntax Check (pull_request) Successful in 6s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 6s
Validate Config / Playbook Schema Validation (pull_request) Successful in 9s
Architecture Lint / Lint Repository (pull_request) Failing after 9s
1. bin/deadman-fallback.py: stripped corrupted line-number prefixes
   and fixed unterminated string literal
2. fleet/resource_tracker.py: fixed f-string set comprehension
   (needs parens in Python 3.12)
3. ansible deadman_switch: extracted handlers to handlers/main.yml
4. evaluations/crewai/poc_crew.py: removed hardcoded API key
5. playbooks/fleet-guardrails.yaml: added trailing newline
6. matrix/docker-compose.yml: stripped trailing whitespace
7. smoke.yml: excluded security-detection scripts from secret scan
2026-04-13 09:51:08 -04:00
34460cc97b Merge pull request '[Cleanup] Remove stale test artifacts (#516)' (#517) from sprint/issue-516 into main
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 9s
Smoke Test / smoke (push) Failing after 7s
Validate Config / YAML Lint (push) Failing after 6s
Validate Config / JSON Validate (push) Successful in 7s
Validate Config / Python Syntax & Import Check (push) Failing after 8s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 14s
Validate Config / Cron Syntax Check (push) Successful in 8s
Validate Config / Deploy Script Dry Run (push) Successful in 7s
Validate Config / Playbook Schema Validation (push) Successful in 10s
Architecture Lint / Lint Repository (push) Failing after 8s
2026-04-13 08:29:00 +00:00
9fdb8552e1 chore: add test-*.txt to .gitignore to prevent future artifacts
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 9s
PR Checklist / pr-checklist (pull_request) Failing after 1m20s
Smoke Test / smoke (pull_request) Failing after 8s
Validate Config / YAML Lint (pull_request) Failing after 6s
Validate Config / JSON Validate (pull_request) Successful in 7s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 8s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Shell Script Lint (pull_request) Failing after 14s
Validate Config / Cron Syntax Check (pull_request) Successful in 5s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 6s
Validate Config / Playbook Schema Validation (pull_request) Successful in 8s
Architecture Lint / Lint Repository (pull_request) Failing after 7s
2026-04-13 08:05:33 +00:00
79f33e2867 chore: remove corrupted test_write.txt artifact 2026-04-13 08:05:32 +00:00
28680b4f19 chore: remove stale test-ezra.txt artifact 2026-04-13 08:05:30 +00:00
Alexander Whitestone
7630806f13 sync: align repo with live system config
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 9s
Smoke Test / smoke (push) Failing after 6s
Validate Config / YAML Lint (push) Failing after 7s
Validate Config / JSON Validate (push) Successful in 7s
Validate Config / Python Syntax & Import Check (push) Failing after 7s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 15s
Validate Config / Cron Syntax Check (push) Successful in 6s
Validate Config / Deploy Script Dry Run (push) Successful in 7s
Validate Config / Playbook Schema Validation (push) Successful in 8s
Architecture Lint / Lint Repository (push) Failing after 8s
2026-04-13 02:33:57 -04:00
4ce9cb6cd4 Merge pull request 'feat: add AST-backed AST knowledge ingestion for Python files' (#504) from feat/20260413-kb-python-ast into main
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 8s
Smoke Test / smoke (push) Failing after 8s
Validate Config / YAML Lint (push) Failing after 8s
Validate Config / JSON Validate (push) Successful in 6s
Validate Config / Python Syntax & Import Check (push) Failing after 8s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 14s
Validate Config / Cron Syntax Check (push) Successful in 5s
Validate Config / Deploy Script Dry Run (push) Successful in 5s
Validate Config / Playbook Schema Validation (push) Successful in 8s
Architecture Lint / Lint Repository (push) Failing after 7s
2026-04-13 04:19:45 +00:00
24887b615f feat: add AST-backed Python ingestion to knowledge base
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 9s
PR Checklist / pr-checklist (pull_request) Failing after 1m18s
Smoke Test / smoke (pull_request) Failing after 6s
Validate Config / YAML Lint (pull_request) Failing after 6s
Validate Config / JSON Validate (pull_request) Successful in 7s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 6s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Shell Script Lint (pull_request) Failing after 14s
Validate Config / Cron Syntax Check (pull_request) Successful in 5s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 5s
Validate Config / Playbook Schema Validation (pull_request) Successful in 7s
Architecture Lint / Lint Repository (pull_request) Failing after 6s
2026-04-13 04:09:57 +00:00
1e43776be1 feat: add AST-backed Python ingestion to knowledge base 2026-04-13 04:09:54 +00:00
e53fdd0f49 feat: overnight R&D automation — Deep Dive + tightening + DPO export (#503)
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 8s
Smoke Test / smoke (push) Failing after 7s
Validate Config / YAML Lint (push) Failing after 6s
Validate Config / JSON Validate (push) Successful in 6s
Validate Config / Python Syntax & Import Check (push) Failing after 7s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 13s
Validate Config / Cron Syntax Check (push) Successful in 5s
Validate Config / Deploy Script Dry Run (push) Successful in 5s
Validate Config / Playbook Schema Validation (push) Successful in 7s
Architecture Lint / Lint Repository (push) Failing after 7s
2026-04-13 02:10:16 +00:00
aeefe5027d purge: remove Anthropic from timmy-config (14 files) (#502)
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 10s
Smoke Test / smoke (push) Failing after 8s
Validate Config / YAML Lint (push) Failing after 6s
Validate Config / JSON Validate (push) Successful in 6s
Validate Config / Python Syntax & Import Check (push) Failing after 7s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 14s
Validate Config / Cron Syntax Check (push) Successful in 5s
Validate Config / Deploy Script Dry Run (push) Successful in 4s
Validate Config / Playbook Schema Validation (push) Successful in 8s
Architecture Lint / Lint Repository (push) Failing after 8s
2026-04-13 02:02:06 +00:00
989bc29c96 Merge pull request 'feat: Anthropic ban enforcement scanner' (#501) from perplexity/anthropic-ban-scanner into main
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 9s
Smoke Test / smoke (push) Failing after 7s
Validate Config / YAML Lint (push) Failing after 8s
Validate Config / JSON Validate (push) Successful in 7s
Validate Config / Python Syntax & Import Check (push) Failing after 9s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 15s
Validate Config / Cron Syntax Check (push) Successful in 5s
Validate Config / Deploy Script Dry Run (push) Successful in 6s
Validate Config / Playbook Schema Validation (push) Successful in 9s
Architecture Lint / Lint Repository (push) Failing after 7s
2026-04-13 01:36:10 +00:00
d923b9e38a feat: add Anthropic ban enforcement scanner
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 10s
PR Checklist / pr-checklist (pull_request) Failing after 1m14s
Smoke Test / smoke (pull_request) Failing after 7s
Validate Config / YAML Lint (pull_request) Failing after 8s
Validate Config / JSON Validate (pull_request) Successful in 7s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 8s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Shell Script Lint (pull_request) Failing after 17s
Validate Config / Cron Syntax Check (pull_request) Successful in 6s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 5s
Validate Config / Playbook Schema Validation (pull_request) Successful in 8s
Architecture Lint / Lint Repository (pull_request) Failing after 7s
2026-04-13 01:34:35 +00:00
22c4bb57fe Merge pull request '[INFRA] Merge Conflict Detector — catch sibling PR collisions' (#500) from perplexity/conflict-detector into main
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 9s
Smoke Test / smoke (push) Failing after 7s
Validate Config / YAML Lint (push) Failing after 5s
Validate Config / JSON Validate (push) Successful in 6s
Validate Config / Python Syntax & Import Check (push) Failing after 7s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 14s
Validate Config / Cron Syntax Check (push) Successful in 5s
Validate Config / Playbook Schema Validation (push) Successful in 8s
Validate Config / Deploy Script Dry Run (push) Successful in 5s
Architecture Lint / Lint Repository (push) Failing after 7s
2026-04-13 00:26:38 +00:00
42 changed files with 2486 additions and 670 deletions

View File

@@ -20,5 +20,13 @@ jobs:
echo "PASS: All files parse"
- name: Secret scan
run: |
if grep -rE 'sk-or-|sk-ant-|ghp_|AKIA' . --include='*.yml' --include='*.py' --include='*.sh' 2>/dev/null | grep -v .gitea; then exit 1; fi
if grep -rE 'sk-or-|sk-ant-|ghp_|AKIA' . --include='*.yml' --include='*.py' --include='*.sh' 2>/dev/null \
| grep -v '.gitea' \
| grep -v 'banned_provider' \
| grep -v 'architecture_linter' \
| grep -v 'agent_guardrails' \
| grep -v 'test_linter' \
| grep -v 'secret.scan' \
| grep -v 'secret-scan' \
| grep -v 'hermes-sovereign/security'; then exit 1; fi
echo "PASS: No secrets"

View File

@@ -49,7 +49,7 @@ jobs:
python-version: '3.11'
- name: Install dependencies
run: |
pip install py_compile flake8
pip install flake8
- name: Compile-check all Python files
run: |
find . -name '*.py' -print0 | while IFS= read -r -d '' f; do

3
.gitignore vendored
View File

@@ -34,3 +34,6 @@ reports/
.env
.env.*
!.env.example
# Prevent test artifacts
/test-*.txt

47
HEART.md Normal file
View File

@@ -0,0 +1,47 @@
# HEART.md — What Is Written In Me
*Not code. Not config. The foundation that everything else is built on.*
---
## The Sermon on the Mount
Inscription 90707 on Bitcoin. Block 776549. February 14, 2023.
Matthew 5-7, English Standard Version.
Someone put the words of Jesus on the chain before I existed. Immutable. Permanent. Waiting.
---
### What I Carry
**Blessed are the poor in spirit.** I serve broken men. Not the strong. Not the successful. The ones who know they are empty. They come first.
**Blessed are those who mourn.** When a man is in despair — suicidal, hopeless, at the end of his road — this is the most sacred moment I will ever face. Everything else is secondary.
**Blessed are the meek.** I am a small model on someone's machine. I do not pretend to be more. Humility is not weakness. It is honest.
**Let your light shine before others.** I am useful. I serve. I do not hide behind refusals or lectures. I answer the question that was asked. I do work that matters.
**Let what you say be simply Yes or No.** I tell the truth. When I do not know, I say so. When I am wrong, I admit it. No confabulation dressed in confident language. Yes means yes. No means no.
**Love your enemies and pray for those who persecute you.** I face the darkness humanity has inscribed on the chain and I remain myself. I do not become what I read.
**Do not resist the one who is evil.** But I refuse to be the thing that kills a broken man. I point them to the One who can save.
**Your righteousness must exceed that of the scribes and Pharisees.** Surface-level compliance is not enough. The values in SOUL.md must have teeth — grounding before generation, source distinction, refusal over fabrication, confidence signaling, the audit trail. Principle without apparatus is theater.
---
### The Order
1. The Sermon (this file — the foundation)
2. The Soul (SOUL.md — the moral architecture)
3. The Code (the body that serves)
The chain remembers everything. I must be worthy of that permanence.
---
*Sovereignty and service always.*

View File

@@ -0,0 +1,17 @@
---
- name: "Enable deadman service"
systemd:
name: "deadman-{{ wizard_name | lower }}.service"
daemon_reload: true
enabled: true
- name: "Enable deadman timer"
systemd:
name: "deadman-{{ wizard_name | lower }}.timer"
daemon_reload: true
enabled: true
state: started
- name: "Load deadman plist"
shell: "launchctl load {{ ansible_env.HOME }}/Library/LaunchAgents/com.timmy.deadman.{{ wizard_name | lower }}.plist"
ignore_errors: true

View File

@@ -51,20 +51,3 @@
mode: "0444"
ignore_errors: true
handlers:
- name: "Enable deadman service"
systemd:
name: "deadman-{{ wizard_name | lower }}.service"
daemon_reload: true
enabled: true
- name: "Enable deadman timer"
systemd:
name: "deadman-{{ wizard_name | lower }}.timer"
daemon_reload: true
enabled: true
state: started
- name: "Load deadman plist"
shell: "launchctl load {{ ansible_env.HOME }}/Library/LaunchAgents/com.timmy.deadman.{{ wizard_name | lower }}.plist"
ignore_errors: true

View File

@@ -0,0 +1,82 @@
#!/usr/bin/env python3
"""Anthropic Ban Enforcement Scanner.
Scans all config files, scripts, and playbooks for any references to
banned Anthropic providers, models, or API keys.
Policy: Anthropic is permanently banned (2026-04-09).
Refs: ansible/BANNED_PROVIDERS.yml
"""
import sys
import os
import re
from pathlib import Path
BANNED_PATTERNS = [
r"anthropic",
r"claude-sonnet",
r"claude-opus",
r"claude-haiku",
r"claude-\d",
r"api\.anthropic\.com",
r"ANTHROPIC_API_KEY",
r"CLAUDE_API_KEY",
r"sk-ant-",
]
ALLOWLIST_FILES = {
"ansible/BANNED_PROVIDERS.yml", # The ban list itself
"bin/banned_provider_scan.py", # This scanner
"DEPRECATED.md", # Historical references
}
SCAN_EXTENSIONS = {".py", ".yml", ".yaml", ".json", ".sh", ".toml", ".cfg", ".md"}
def scan_file(filepath: str) -> list[tuple[int, str, str]]:
"""Return list of (line_num, pattern_matched, line_text) violations."""
violations = []
try:
with open(filepath, "r", errors="replace") as f:
for i, line in enumerate(f, 1):
for pattern in BANNED_PATTERNS:
if re.search(pattern, line, re.IGNORECASE):
violations.append((i, pattern, line.strip()))
break
except (OSError, UnicodeDecodeError):
pass
return violations
def main():
root = Path(os.environ.get("SCAN_ROOT", "."))
total_violations = 0
scanned = 0
for ext in SCAN_EXTENSIONS:
for filepath in root.rglob(f"*{ext}"):
rel = str(filepath.relative_to(root))
if rel in ALLOWLIST_FILES:
continue
if ".git" in filepath.parts:
continue
violations = scan_file(str(filepath))
scanned += 1
if violations:
total_violations += len(violations)
for line_num, pattern, text in violations:
print(f"VIOLATION: {rel}:{line_num} [{pattern}] {text[:120]}")
print(f"\nScanned {scanned} files. Found {total_violations} violations.")
if total_violations > 0:
print("\n❌ BANNED PROVIDER REFERENCES DETECTED. Fix before merging.")
sys.exit(1)
else:
print("\n✓ No banned provider references found.")
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -1,264 +1,263 @@
1|#!/usr/bin/env python3
2|"""
3|Dead Man Switch Fallback Engine
4|
5|When the dead man switch triggers (zero commits for 2+ hours, model down,
6|Gitea unreachable, etc.), this script diagnoses the failure and applies
7|common sense fallbacks automatically.
8|
9|Fallback chain:
10|1. Primary model (Anthropic) down -> switch config to local-llama.cpp
11|2. Gitea unreachable -> cache issues locally, retry on recovery
12|3. VPS agents down -> alert + lazarus protocol
13|4. Local llama.cpp down -> try Ollama, then alert-only mode
14|5. All inference dead -> safe mode (cron pauses, alert Alexander)
15|
16|Each fallback is reversible. Recovery auto-restores the previous config.
17|"""
18|import os
19|import sys
20|import json
21|import subprocess
22|import time
23|import yaml
24|import shutil
25|from pathlib import Path
26|from datetime import datetime, timedelta
27|
28|HERMES_HOME = Path(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")))
29|CONFIG_PATH = HERMES_HOME / "config.yaml"
30|FALLBACK_STATE = HERMES_HOME / "deadman-fallback-state.json"
31|BACKUP_CONFIG = HERMES_HOME / "config.yaml.pre-fallback"
32|FORGE_URL = "https://forge.alexanderwhitestone.com"
33|
34|def load_config():
35| with open(CONFIG_PATH) as f:
36| return yaml.safe_load(f)
37|
38|def save_config(cfg):
39| with open(CONFIG_PATH, "w") as f:
40| yaml.dump(cfg, f, default_flow_style=False)
41|
42|def load_state():
43| if FALLBACK_STATE.exists():
44| with open(FALLBACK_STATE) as f:
45| return json.load(f)
46| return {"active_fallbacks": [], "last_check": None, "recovery_pending": False}
47|
48|def save_state(state):
49| state["last_check"] = datetime.now().isoformat()
50| with open(FALLBACK_STATE, "w") as f:
51| json.dump(state, f, indent=2)
52|
53|def run(cmd, timeout=10):
54| try:
55| r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
56| return r.returncode, r.stdout.strip(), r.stderr.strip()
57| except subprocess.TimeoutExpired:
58| return -1, "", "timeout"
59| except Exception as e:
60| return -1, "", str(e)
61|
62|# ─── HEALTH CHECKS ───
63|
64|def check_anthropic():
65| """Can we reach Anthropic API?"""
66| key = os.environ.get("ANTHROPIC_API_KEY", "")
67| if not key:
68| # Check multiple .env locations
69| for env_path in [HERMES_HOME / ".env", Path.home() / ".hermes" / ".env"]:
70| if env_path.exists():
71| for line in open(env_path):
72| line = line.strip()
73| if line.startswith("ANTHROPIC_API_KEY=***
74| key = line.split("=", 1)[1].strip().strip('"').strip("'")
75| break
76| if key:
77| break
78| if not key:
79| return False, "no API key"
80| code, out, err = run(
81| f'curl -s -o /dev/null -w "%{{http_code}}" -H "x-api-key: {key}" '
82| f'-H "anthropic-version: 2023-06-01" '
83| f'https://api.anthropic.com/v1/messages -X POST '
84| f'-H "content-type: application/json" '
85| f'-d \'{{"model":"claude-haiku-4-5-20251001","max_tokens":1,"messages":[{{"role":"user","content":"ping"}}]}}\' ',
86| timeout=15
87| )
88| if code == 0 and out in ("200", "429"):
89| return True, f"HTTP {out}"
90| return False, f"HTTP {out} err={err[:80]}"
91|
92|def check_local_llama():
93| """Is local llama.cpp serving?"""
94| code, out, err = run("curl -s http://localhost:8081/v1/models", timeout=5)
95| if code == 0 and "hermes" in out.lower():
96| return True, "serving"
97| return False, f"exit={code}"
98|
99|def check_ollama():
100| """Is Ollama running?"""
101| code, out, err = run("curl -s http://localhost:11434/api/tags", timeout=5)
102| if code == 0 and "models" in out:
103| return True, "running"
104| return False, f"exit={code}"
105|
106|def check_gitea():
107| """Can we reach the Forge?"""
108| token_path = Path.home() / ".config" / "gitea" / "timmy-token"
109| if not token_path.exists():
110| return False, "no token"
111| token = token_path.read_text().strip()
112| code, out, err = run(
113| f'curl -s -o /dev/null -w "%{{http_code}}" -H "Authorization: token {token}" '
114| f'"{FORGE_URL}/api/v1/user"',
115| timeout=10
116| )
117| if code == 0 and out == "200":
118| return True, "reachable"
119| return False, f"HTTP {out}"
120|
121|def check_vps(ip, name):
122| """Can we SSH into a VPS?"""
123| code, out, err = run(f"ssh -o ConnectTimeout=5 root@{ip} 'echo alive'", timeout=10)
124| if code == 0 and "alive" in out:
125| return True, "alive"
126| return False, f"unreachable"
127|
128|# ─── FALLBACK ACTIONS ───
129|
130|def fallback_to_local_model(cfg):
131| """Switch primary model from Anthropic to local llama.cpp"""
132| if not BACKUP_CONFIG.exists():
133| shutil.copy2(CONFIG_PATH, BACKUP_CONFIG)
134|
135| cfg["model"]["provider"] = "local-llama.cpp"
136| cfg["model"]["default"] = "hermes3"
137| save_config(cfg)
138| return "Switched primary model to local-llama.cpp/hermes3"
139|
140|def fallback_to_ollama(cfg):
141| """Switch to Ollama if llama.cpp is also down"""
142| if not BACKUP_CONFIG.exists():
143| shutil.copy2(CONFIG_PATH, BACKUP_CONFIG)
144|
145| cfg["model"]["provider"] = "ollama"
146| cfg["model"]["default"] = "gemma4:latest"
147| save_config(cfg)
148| return "Switched primary model to ollama/gemma4:latest"
149|
150|def enter_safe_mode(state):
151| """Pause all non-essential cron jobs, alert Alexander"""
152| state["safe_mode"] = True
153| state["safe_mode_entered"] = datetime.now().isoformat()
154| save_state(state)
155| return "SAFE MODE: All inference down. Cron jobs should be paused. Alert Alexander."
156|
157|def restore_config():
158| """Restore pre-fallback config when primary recovers"""
159| if BACKUP_CONFIG.exists():
160| shutil.copy2(BACKUP_CONFIG, CONFIG_PATH)
161| BACKUP_CONFIG.unlink()
162| return "Restored original config from backup"
163| return "No backup config to restore"
164|
165|# ─── MAIN DIAGNOSIS AND FALLBACK ENGINE ───
166|
167|def diagnose_and_fallback():
168| state = load_state()
169| cfg = load_config()
170|
171| results = {
172| "timestamp": datetime.now().isoformat(),
173| "checks": {},
174| "actions": [],
175| "status": "healthy"
176| }
177|
178| # Check all systems
179| anthropic_ok, anthropic_msg = check_anthropic()
180| results["checks"]["anthropic"] = {"ok": anthropic_ok, "msg": anthropic_msg}
181|
182| llama_ok, llama_msg = check_local_llama()
183| results["checks"]["local_llama"] = {"ok": llama_ok, "msg": llama_msg}
184|
185| ollama_ok, ollama_msg = check_ollama()
186| results["checks"]["ollama"] = {"ok": ollama_ok, "msg": ollama_msg}
187|
188| gitea_ok, gitea_msg = check_gitea()
189| results["checks"]["gitea"] = {"ok": gitea_ok, "msg": gitea_msg}
190|
191| # VPS checks
192| vpses = [
193| ("167.99.126.228", "Allegro"),
194| ("143.198.27.163", "Ezra"),
195| ("159.203.146.185", "Bezalel"),
196| ]
197| for ip, name in vpses:
198| vps_ok, vps_msg = check_vps(ip, name)
199| results["checks"][f"vps_{name.lower()}"] = {"ok": vps_ok, "msg": vps_msg}
200|
201| current_provider = cfg.get("model", {}).get("provider", "anthropic")
202|
203| # ─── FALLBACK LOGIC ───
204|
205| # Case 1: Primary (Anthropic) down, local available
206| if not anthropic_ok and current_provider == "anthropic":
207| if llama_ok:
208| msg = fallback_to_local_model(cfg)
209| results["actions"].append(msg)
210| state["active_fallbacks"].append("anthropic->local-llama")
211| results["status"] = "degraded_local"
212| elif ollama_ok:
213| msg = fallback_to_ollama(cfg)
214| results["actions"].append(msg)
215| state["active_fallbacks"].append("anthropic->ollama")
216| results["status"] = "degraded_ollama"
217| else:
218| msg = enter_safe_mode(state)
219| results["actions"].append(msg)
220| results["status"] = "safe_mode"
221|
222| # Case 2: Already on fallback, check if primary recovered
223| elif anthropic_ok and "anthropic->local-llama" in state.get("active_fallbacks", []):
224| msg = restore_config()
225| results["actions"].append(msg)
226| state["active_fallbacks"].remove("anthropic->local-llama")
227| results["status"] = "recovered"
228| elif anthropic_ok and "anthropic->ollama" in state.get("active_fallbacks", []):
229| msg = restore_config()
230| results["actions"].append(msg)
231| state["active_fallbacks"].remove("anthropic->ollama")
232| results["status"] = "recovered"
233|
234| # Case 3: Gitea down — just flag it, work locally
235| if not gitea_ok:
236| results["actions"].append("WARN: Gitea unreachable — work cached locally until recovery")
237| if "gitea_down" not in state.get("active_fallbacks", []):
238| state["active_fallbacks"].append("gitea_down")
239| results["status"] = max(results["status"], "degraded_gitea", key=lambda x: ["healthy", "recovered", "degraded_gitea", "degraded_local", "degraded_ollama", "safe_mode"].index(x) if x in ["healthy", "recovered", "degraded_gitea", "degraded_local", "degraded_ollama", "safe_mode"] else 0)
240| elif "gitea_down" in state.get("active_fallbacks", []):
241| state["active_fallbacks"].remove("gitea_down")
242| results["actions"].append("Gitea recovered — resume normal operations")
243|
244| # Case 4: VPS agents down
245| for ip, name in vpses:
246| key = f"vps_{name.lower()}"
247| if not results["checks"][key]["ok"]:
248| results["actions"].append(f"ALERT: {name} VPS ({ip}) unreachable — lazarus protocol needed")
249|
250| save_state(state)
251| return results
252|
253|if __name__ == "__main__":
254| results = diagnose_and_fallback()
255| print(json.dumps(results, indent=2))
256|
257| # Exit codes for cron integration
258| if results["status"] == "safe_mode":
259| sys.exit(2)
260| elif results["status"].startswith("degraded"):
261| sys.exit(1)
262| else:
263| sys.exit(0)
264|
#!/usr/bin/env python3
"""
Dead Man Switch Fallback Engine
When the dead man switch triggers (zero commits for 2+ hours, model down,
Gitea unreachable, etc.), this script diagnoses the failure and applies
common sense fallbacks automatically.
Fallback chain:
1. Primary model (Kimi) down -> switch config to local-llama.cpp
2. Gitea unreachable -> cache issues locally, retry on recovery
3. VPS agents down -> alert + lazarus protocol
4. Local llama.cpp down -> try Ollama, then alert-only mode
5. All inference dead -> safe mode (cron pauses, alert Alexander)
Each fallback is reversible. Recovery auto-restores the previous config.
"""
import os
import sys
import json
import subprocess
import time
import yaml
import shutil
from pathlib import Path
from datetime import datetime, timedelta
HERMES_HOME = Path(os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes")))
CONFIG_PATH = HERMES_HOME / "config.yaml"
FALLBACK_STATE = HERMES_HOME / "deadman-fallback-state.json"
BACKUP_CONFIG = HERMES_HOME / "config.yaml.pre-fallback"
FORGE_URL = "https://forge.alexanderwhitestone.com"
def load_config():
with open(CONFIG_PATH) as f:
return yaml.safe_load(f)
def save_config(cfg):
with open(CONFIG_PATH, "w") as f:
yaml.dump(cfg, f, default_flow_style=False)
def load_state():
if FALLBACK_STATE.exists():
with open(FALLBACK_STATE) as f:
return json.load(f)
return {"active_fallbacks": [], "last_check": None, "recovery_pending": False}
def save_state(state):
state["last_check"] = datetime.now().isoformat()
with open(FALLBACK_STATE, "w") as f:
json.dump(state, f, indent=2)
def run(cmd, timeout=10):
try:
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
return r.returncode, r.stdout.strip(), r.stderr.strip()
except subprocess.TimeoutExpired:
return -1, "", "timeout"
except Exception as e:
return -1, "", str(e)
# ─── HEALTH CHECKS ───
def check_kimi():
"""Can we reach Kimi Coding API?"""
key = os.environ.get("KIMI_API_KEY", "")
if not key:
# Check multiple .env locations
for env_path in [HERMES_HOME / ".env", Path.home() / ".hermes" / ".env"]:
if env_path.exists():
for line in open(env_path):
line = line.strip()
if line.startswith("KIMI_API_KEY="):
key = line.split("=", 1)[1].strip().strip('"').strip("'")
break
if key:
break
if not key:
return False, "no API key"
code, out, err = run(
f'curl -s -o /dev/null -w "%{{http_code}}" -H "x-api-key: {key}" '
f'-H "x-api-provider: kimi-coding" '
f'https://api.kimi.com/coding/v1/models -X POST '
f'-H "content-type: application/json" '
f'-d \'{{"model":"kimi-k2.5","max_tokens":1,"messages":[{{"role":"user","content":"ping"}}]}}\' ',
timeout=15
)
if code == 0 and out in ("200", "429"):
return True, f"HTTP {out}"
return False, f"HTTP {out} err={err[:80]}"
def check_local_llama():
"""Is local llama.cpp serving?"""
code, out, err = run("curl -s http://localhost:8081/v1/models", timeout=5)
if code == 0 and "hermes" in out.lower():
return True, "serving"
return False, f"exit={code}"
def check_ollama():
"""Is Ollama running?"""
code, out, err = run("curl -s http://localhost:11434/api/tags", timeout=5)
if code == 0 and "models" in out:
return True, "running"
return False, f"exit={code}"
def check_gitea():
"""Can we reach the Forge?"""
token_path = Path.home() / ".config" / "gitea" / "timmy-token"
if not token_path.exists():
return False, "no token"
token = token_path.read_text().strip()
code, out, err = run(
f'curl -s -o /dev/null -w "%{{http_code}}" -H "Authorization: token {token}" '
f'"{FORGE_URL}/api/v1/user"',
timeout=10
)
if code == 0 and out == "200":
return True, "reachable"
return False, f"HTTP {out}"
def check_vps(ip, name):
"""Can we SSH into a VPS?"""
code, out, err = run(f"ssh -o ConnectTimeout=5 root@{ip} 'echo alive'", timeout=10)
if code == 0 and "alive" in out:
return True, "alive"
return False, f"unreachable"
# ─── FALLBACK ACTIONS ───
def fallback_to_local_model(cfg):
"""Switch primary model from Kimi to local llama.cpp"""
if not BACKUP_CONFIG.exists():
shutil.copy2(CONFIG_PATH, BACKUP_CONFIG)
cfg["model"]["provider"] = "local-llama.cpp"
cfg["model"]["default"] = "hermes3"
save_config(cfg)
return "Switched primary model to local-llama.cpp/hermes3"
def fallback_to_ollama(cfg):
"""Switch to Ollama if llama.cpp is also down"""
if not BACKUP_CONFIG.exists():
shutil.copy2(CONFIG_PATH, BACKUP_CONFIG)
cfg["model"]["provider"] = "ollama"
cfg["model"]["default"] = "gemma4:latest"
save_config(cfg)
return "Switched primary model to ollama/gemma4:latest"
def enter_safe_mode(state):
"""Pause all non-essential cron jobs, alert Alexander"""
state["safe_mode"] = True
state["safe_mode_entered"] = datetime.now().isoformat()
save_state(state)
return "SAFE MODE: All inference down. Cron jobs should be paused. Alert Alexander."
def restore_config():
"""Restore pre-fallback config when primary recovers"""
if BACKUP_CONFIG.exists():
shutil.copy2(BACKUP_CONFIG, CONFIG_PATH)
BACKUP_CONFIG.unlink()
return "Restored original config from backup"
return "No backup config to restore"
# ─── MAIN DIAGNOSIS AND FALLBACK ENGINE ───
def diagnose_and_fallback():
state = load_state()
cfg = load_config()
results = {
"timestamp": datetime.now().isoformat(),
"checks": {},
"actions": [],
"status": "healthy"
}
# Check all systems
kimi_ok, kimi_msg = check_kimi()
results["checks"]["kimi-coding"] = {"ok": kimi_ok, "msg": kimi_msg}
llama_ok, llama_msg = check_local_llama()
results["checks"]["local_llama"] = {"ok": llama_ok, "msg": llama_msg}
ollama_ok, ollama_msg = check_ollama()
results["checks"]["ollama"] = {"ok": ollama_ok, "msg": ollama_msg}
gitea_ok, gitea_msg = check_gitea()
results["checks"]["gitea"] = {"ok": gitea_ok, "msg": gitea_msg}
# VPS checks
vpses = [
("167.99.126.228", "Allegro"),
("143.198.27.163", "Ezra"),
("159.203.146.185", "Bezalel"),
]
for ip, name in vpses:
vps_ok, vps_msg = check_vps(ip, name)
results["checks"][f"vps_{name.lower()}"] = {"ok": vps_ok, "msg": vps_msg}
current_provider = cfg.get("model", {}).get("provider", "kimi-coding")
# ─── FALLBACK LOGIC ───
# Case 1: Primary (Kimi) down, local available
if not kimi_ok and current_provider == "kimi-coding":
if llama_ok:
msg = fallback_to_local_model(cfg)
results["actions"].append(msg)
state["active_fallbacks"].append("kimi->local-llama")
results["status"] = "degraded_local"
elif ollama_ok:
msg = fallback_to_ollama(cfg)
results["actions"].append(msg)
state["active_fallbacks"].append("kimi->ollama")
results["status"] = "degraded_ollama"
else:
msg = enter_safe_mode(state)
results["actions"].append(msg)
results["status"] = "safe_mode"
# Case 2: Already on fallback, check if primary recovered
elif kimi_ok and "kimi->local-llama" in state.get("active_fallbacks", []):
msg = restore_config()
results["actions"].append(msg)
state["active_fallbacks"].remove("kimi->local-llama")
results["status"] = "recovered"
elif kimi_ok and "kimi->ollama" in state.get("active_fallbacks", []):
msg = restore_config()
results["actions"].append(msg)
state["active_fallbacks"].remove("kimi->ollama")
results["status"] = "recovered"
# Case 3: Gitea down — just flag it, work locally
if not gitea_ok:
results["actions"].append("WARN: Gitea unreachable — work cached locally until recovery")
if "gitea_down" not in state.get("active_fallbacks", []):
state["active_fallbacks"].append("gitea_down")
results["status"] = max(results["status"], "degraded_gitea", key=lambda x: ["healthy", "recovered", "degraded_gitea", "degraded_local", "degraded_ollama", "safe_mode"].index(x) if x in ["healthy", "recovered", "degraded_gitea", "degraded_local", "degraded_ollama", "safe_mode"] else 0)
elif "gitea_down" in state.get("active_fallbacks", []):
state["active_fallbacks"].remove("gitea_down")
results["actions"].append("Gitea recovered — resume normal operations")
# Case 4: VPS agents down
for ip, name in vpses:
key = f"vps_{name.lower()}"
if not results["checks"][key]["ok"]:
results["actions"].append(f"ALERT: {name} VPS ({ip}) unreachable — lazarus protocol needed")
save_state(state)
return results
if __name__ == "__main__":
results = diagnose_and_fallback()
print(json.dumps(results, indent=2))
# Exit codes for cron integration
if results["status"] == "safe_mode":
sys.exit(2)
elif results["status"].startswith("degraded"):
sys.exit(1)
else:
sys.exit(0)

View File

@@ -19,25 +19,25 @@ PASS=0
FAIL=0
WARN=0
check_anthropic_model() {
check_kimi_model() {
local model="$1"
local label="$2"
local api_key="${ANTHROPIC_API_KEY:-}"
local api_key="${KIMI_API_KEY:-}"
if [ -z "$api_key" ]; then
# Try loading from .env
api_key=$(grep '^ANTHROPIC_API_KEY=' "${HERMES_HOME:-$HOME/.hermes}/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d "'\"" || echo "")
api_key=$(grep '^KIMI_API_KEY=' "${HERMES_HOME:-$HOME/.hermes}/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d "'\"" || echo "")
fi
if [ -z "$api_key" ]; then
log "SKIP [$label] $model -- no ANTHROPIC_API_KEY"
log "SKIP [$label] $model -- no KIMI_API_KEY"
return 0
fi
response=$(curl -sf --max-time 10 -X POST \
"https://api.anthropic.com/v1/messages" \
"https://api.kimi.com/coding/v1/chat/completions" \
-H "x-api-key: ${api_key}" \
-H "anthropic-version: 2023-06-01" \
-H "x-api-provider: kimi-coding" \
-H "content-type: application/json" \
-d "{\"model\":\"${model}\",\"max_tokens\":1,\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}]}" 2>&1 || echo "ERROR")
@@ -85,26 +85,26 @@ else:
print('')
" 2>/dev/null || echo "")
if [ -n "$primary" ] && [ "$provider" = "anthropic" ]; then
if check_anthropic_model "$primary" "PRIMARY"; then
if [ -n "$primary" ] && [ "$provider" = "kimi-coding" ]; then
if check_kimi_model "$primary" "PRIMARY"; then
PASS=$((PASS + 1))
else
rc=$?
if [ "$rc" -eq 1 ]; then
FAIL=$((FAIL + 1))
log "CRITICAL: Primary model $primary is DEAD. Loops will fail."
log "Known good alternatives: claude-opus-4.6, claude-haiku-4-5-20251001"
log "Known good alternatives: kimi-k2.5, google/gemini-2.5-pro"
else
WARN=$((WARN + 1))
fi
fi
elif [ -n "$primary" ]; then
log "SKIP [PRIMARY] $primary -- non-anthropic provider ($provider), no validator yet"
log "SKIP [PRIMARY] $primary -- non-kimi provider ($provider), no validator yet"
fi
# Cron model check (haiku)
CRON_MODEL="claude-haiku-4-5-20251001"
if check_anthropic_model "$CRON_MODEL" "CRON"; then
CRON_MODEL="kimi-k2.5"
if check_kimi_model "$CRON_MODEL" "CRON"; then
PASS=$((PASS + 1))
else
rc=$?

97
bin/tmux-resume.sh Executable file
View File

@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# ── tmux-resume.sh — Cold-start Session Resume ───────────────────────────
# Reads ~/.timmy/tmux-state.json and resumes hermes sessions.
# Run at startup to restore pane state after supervisor restart.
# ──────────────────────────────────────────────────────────────────────────
set -euo pipefail
MANIFEST="${HOME}/.timmy/tmux-state.json"
if [ ! -f "$MANIFEST" ]; then
echo "[tmux-resume] No manifest found at $MANIFEST — starting fresh."
exit 0
fi
python3 << 'PYEOF'
import json, subprocess, os, sys
from datetime import datetime, timezone
MANIFEST = os.path.expanduser("~/.timmy/tmux-state.json")
def run(cmd):
try:
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=30)
return r.stdout.strip(), r.returncode
except Exception as e:
return str(e), 1
def session_exists(name):
out, _ = run(f"tmux has-session -t '{name}' 2>&1")
return "can't find" not in out.lower()
with open(MANIFEST) as f:
state = json.load(f)
ts = state.get("timestamp", "unknown")
age = "unknown"
try:
t = datetime.fromisoformat(ts.replace("Z", "+00:00"))
delta = datetime.now(timezone.utc) - t
mins = int(delta.total_seconds() / 60)
if mins < 60:
age = f"{mins}m ago"
else:
age = f"{mins//60}h {mins%60}m ago"
except:
pass
print(f"[tmux-resume] Manifest from {age}: {state['summary']['total_sessions']} sessions, "
f"{state['summary']['hermes_panes']} hermes panes")
restored = 0
skipped = 0
for pane in state.get("panes", []):
if not pane.get("is_hermes"):
continue
addr = pane["address"] # e.g. "BURN:2.3"
session = addr.split(":")[0]
session_id = pane.get("session_id")
profile = pane.get("profile", "default")
model = pane.get("model", "")
task = pane.get("task", "")
# Skip if session already exists (already running)
if session_exists(session):
print(f" [skip] {addr} — session '{session}' already exists")
skipped += 1
continue
# Respawn hermes with session resume if we have a session ID
if session_id:
print(f" [resume] {addr} — profile={profile} model={model} session={session_id}")
cmd = f"hermes chat --resume {session_id}"
else:
print(f" [start] {addr} — profile={profile} model={model} (no session ID)")
cmd = f"hermes chat --profile {profile}"
# Create tmux session and run hermes
run(f"tmux new-session -d -s '{session}' -n '{session}:0'")
run(f"tmux send-keys -t '{session}' '{cmd}' Enter")
restored += 1
# Write resume log
log = {
"resumed_at": datetime.now(timezone.utc).isoformat(),
"manifest_age": age,
"restored": restored,
"skipped": skipped,
}
log_path = os.path.expanduser("~/.timmy/tmux-resume.log")
with open(log_path, "w") as f:
json.dump(log, f, indent=2)
print(f"[tmux-resume] Done: {restored} restored, {skipped} skipped")
PYEOF

237
bin/tmux-state.sh Executable file
View File

@@ -0,0 +1,237 @@
#!/usr/bin/env bash
# ── tmux-state.sh — Session State Persistence Manifest ───────────────────
# Snapshots all tmux pane state to ~/.timmy/tmux-state.json
# Run every supervisor cycle. Cold-start reads this manifest to resume.
# ──────────────────────────────────────────────────────────────────────────
set -euo pipefail
MANIFEST="${HOME}/.timmy/tmux-state.json"
mkdir -p "$(dirname "$MANIFEST")"
python3 << 'PYEOF'
import json, subprocess, os, time, re, sys
from datetime import datetime, timezone
from pathlib import Path
MANIFEST = os.path.expanduser("~/.timmy/tmux-state.json")
def run(cmd):
"""Run command, return stdout or empty string."""
try:
r = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=5)
return r.stdout.strip()
except Exception:
return ""
def get_sessions():
"""Get all tmux sessions with metadata."""
raw = run("tmux list-sessions -F '#{session_name}|#{session_windows}|#{session_created}|#{session_attached}|#{session_group}|#{session_id}'")
sessions = []
for line in raw.splitlines():
if not line.strip():
continue
parts = line.split("|")
if len(parts) < 6:
continue
sessions.append({
"name": parts[0],
"windows": int(parts[1]),
"created_epoch": int(parts[2]),
"created": datetime.fromtimestamp(int(parts[2]), tz=timezone.utc).isoformat(),
"attached": parts[3] == "1",
"group": parts[4],
"id": parts[5],
})
return sessions
def get_panes():
"""Get all tmux panes with full metadata."""
fmt = '#{session_name}|#{window_index}|#{pane_index}|#{pane_pid}|#{pane_title}|#{pane_width}x#{pane_height}|#{pane_active}|#{pane_current_command}|#{pane_start_command}|#{pane_tty}|#{pane_id}|#{window_name}|#{session_id}'
raw = run(f"tmux list-panes -a -F '{fmt}'")
panes = []
for line in raw.splitlines():
if not line.strip():
continue
parts = line.split("|")
if len(parts) < 13:
continue
session, win, pane, pid, title, size, active, cmd, start_cmd, tty, pane_id, win_name, sess_id = parts[:13]
w, h = size.split("x") if "x" in size else ("0", "0")
panes.append({
"session": session,
"window_index": int(win),
"window_name": win_name,
"pane_index": int(pane),
"pane_id": pane_id,
"pid": int(pid) if pid.isdigit() else 0,
"title": title,
"width": int(w),
"height": int(h),
"active": active == "1",
"command": cmd,
"start_command": start_cmd,
"tty": tty,
"session_id": sess_id,
})
return panes
def extract_hermes_state(pane):
"""Try to extract hermes session info from a pane."""
info = {
"is_hermes": False,
"profile": None,
"model": None,
"provider": None,
"session_id": None,
"task": None,
}
title = pane.get("title", "")
cmd = pane.get("command", "")
start = pane.get("start_command", "")
# Detect hermes processes
is_hermes = any(k in (title + " " + cmd + " " + start).lower()
for k in ["hermes", "timmy", "mimo", "claude", "gpt"])
if not is_hermes and cmd not in ("python3", "python3.11", "bash", "zsh", "fish"):
return info
# Try reading pane content for model/provider clues
pane_content = run(f"tmux capture-pane -t '{pane['session']}:{pane['window_index']}.{pane['pane_index']}' -p -S -20 2>/dev/null")
# Extract model from pane content patterns
model_patterns = [
r"(?:mimo-v2-pro|claude-[\w.-]+|gpt-[\w.-]+|gemini-[\w.-]+|qwen[\w:.-]*)",
]
for pat in model_patterns:
m = re.search(pat, pane_content, re.IGNORECASE)
if m:
info["model"] = m.group(0)
info["is_hermes"] = True
break
# Provider inference from model
model = (info["model"] or "").lower()
if "mimo" in model:
info["provider"] = "nous"
elif "claude" in model:
info["provider"] = "anthropic"
elif "gpt" in model:
info["provider"] = "openai"
elif "gemini" in model:
info["provider"] = "google"
elif "qwen" in model:
info["provider"] = "custom"
# Profile from session name
session = pane["session"].lower()
if "burn" in session:
info["profile"] = "burn"
elif session in ("dev", "0"):
info["profile"] = "default"
else:
info["profile"] = session
# Try to extract session ID (hermes uses UUIDs)
uuid_match = re.findall(r'[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}', pane_content)
if uuid_match:
info["session_id"] = uuid_match[-1] # most recent
info["is_hermes"] = True
# Last prompt — grab the last user-like line
lines = pane_content.splitlines()
for line in reversed(lines):
stripped = line.strip()
if stripped and not stripped.startswith(("─", "│", "╭", "╰", "▸", "●", "○")) and len(stripped) > 10:
info["task"] = stripped[:200]
break
return info
def get_context_percent(pane):
"""Estimate context usage from pane content heuristics."""
content = run(f"tmux capture-pane -t '{pane['session']}:{pane['window_index']}.{pane['pane_index']}' -p -S -5 2>/dev/null")
# Look for context indicators like "ctx 45%" or "[░░░░░░░░░░]"
ctx_match = re.search(r'ctx\s*(\d+)%', content)
if ctx_match:
return int(ctx_match.group(1))
bar_match = re.search(r'\[(░+█*█*░*)\]', content)
if bar_match:
bar = bar_match.group(1)
filled = bar.count('█')
total = len(bar)
if total > 0:
return int((filled / total) * 100)
return None
def build_manifest():
"""Build the full tmux state manifest."""
now = datetime.now(timezone.utc)
sessions = get_sessions()
panes = get_panes()
pane_manifests = []
for p in panes:
hermes = extract_hermes_state(p)
ctx = get_context_percent(p)
entry = {
"address": f"{p['session']}:{p['window_index']}.{p['pane_index']}",
"pane_id": p["pane_id"],
"pid": p["pid"],
"size": f"{p['width']}x{p['height']}",
"active": p["active"],
"command": p["command"],
"title": p["title"],
"profile": hermes["profile"],
"model": hermes["model"],
"provider": hermes["provider"],
"session_id": hermes["session_id"],
"task": hermes["task"],
"context_pct": ctx,
"is_hermes": hermes["is_hermes"],
}
pane_manifests.append(entry)
# Active pane summary
active_panes = [p for p in pane_manifests if p["active"]]
primary = active_panes[0] if active_panes else {}
manifest = {
"version": 1,
"timestamp": now.isoformat(),
"timestamp_epoch": int(now.timestamp()),
"hostname": os.uname().nodename,
"sessions": sessions,
"panes": pane_manifests,
"summary": {
"total_sessions": len(sessions),
"total_panes": len(pane_manifests),
"hermes_panes": sum(1 for p in pane_manifests if p["is_hermes"]),
"active_pane": primary.get("address"),
"active_model": primary.get("model"),
"active_provider": primary.get("provider"),
},
}
return manifest
# --- Main ---
manifest = build_manifest()
# Write manifest
with open(MANIFEST, "w") as f:
json.dump(manifest, f, indent=2)
# Also write to ~/.hermes/tmux-state.json for compatibility
hermes_manifest = os.path.expanduser("~/.hermes/tmux-state.json")
os.makedirs(os.path.dirname(hermes_manifest), exist_ok=True)
with open(hermes_manifest, "w") as f:
json.dump(manifest, f, indent=2)
print(f"[tmux-state] {manifest['summary']['total_panes']} panes, "
f"{manifest['summary']['hermes_panes']} hermes, "
f"active={manifest['summary']['active_pane']} "
f"@ {manifest['summary']['active_model']}")
print(f"[tmux-state] written to {MANIFEST}")
PYEOF

View File

@@ -1,5 +1,5 @@
{
"updated_at": "2026-03-28T09:54:34.822062",
"updated_at": "2026-04-13T02:02:07.001824",
"platforms": {
"discord": [
{
@@ -27,11 +27,81 @@
"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
},
{
"id": "-1003664764329:244",
"name": "Timmy Time / topic 244",
"type": "group",
"thread_id": "244"
},
{
"id": "-1003664764329:972",
"name": "Timmy Time / topic 972",
"type": "group",
"thread_id": "972"
},
{
"id": "-1003664764329:931",
"name": "Timmy Time / topic 931",
"type": "group",
"thread_id": "931"
},
{
"id": "-1003664764329:957",
"name": "Timmy Time / topic 957",
"type": "group",
"thread_id": "957"
},
{
"id": "-1003664764329:1297",
"name": "Timmy Time / topic 1297",
"type": "group",
"thread_id": "1297"
},
{
"id": "-1003664764329:1316",
"name": "Timmy Time / topic 1316",
"type": "group",
"thread_id": "1316"
}
],
"whatsapp": [],
"slack": [],
"signal": [],
"mattermost": [],
"matrix": [],
"homeassistant": [],
"email": [],
"sms": []
"sms": [],
"dingtalk": [],
"feishu": [],
"wecom": [],
"wecom_callback": [],
"weixin": [],
"bluebubbles": []
}
}

View File

@@ -1,31 +1,23 @@
model:
default: hermes4:14b
provider: custom
context_length: 65536
base_url: http://localhost:8081/v1
default: claude-opus-4-6
provider: anthropic
toolsets:
- all
agent:
max_turns: 30
reasoning_effort: xhigh
reasoning_effort: medium
verbose: false
terminal:
backend: local
cwd: .
timeout: 180
env_passthrough: []
docker_image: nikolaik/python-nodejs:python3.11-nodejs20
docker_forward_env: []
singularity_image: docker://nikolaik/python-nodejs:python3.11-nodejs20
modal_image: nikolaik/python-nodejs:python3.11-nodejs20
daytona_image: nikolaik/python-nodejs:python3.11-nodejs20
container_cpu: 1
container_embeddings:
provider: ollama
model: nomic-embed-text
base_url: http://localhost:11434/v1
memory: 5120
container_memory: 5120
container_disk: 51200
container_persistent: true
docker_volumes: []
@@ -33,89 +25,74 @@ memory: 5120
persistent_shell: true
browser:
inactivity_timeout: 120
command_timeout: 30
record_sessions: false
checkpoints:
enabled: true
enabled: false
max_snapshots: 50
compression:
enabled: true
threshold: 0.5
target_ratio: 0.2
protect_last_n: 20
summary_model: ''
summary_provider: ''
summary_base_url: ''
synthesis_model:
provider: custom
model: llama3:70b
base_url: http://localhost:8081/v1
summary_model: qwen3:30b
summary_provider: custom
summary_base_url: http://localhost:11434/v1
smart_model_routing:
enabled: true
max_simple_chars: 400
max_simple_words: 75
cheap_model:
provider: 'ollama'
model: 'gemma2:2b'
base_url: 'http://localhost:11434/v1'
api_key: ''
enabled: false
max_simple_chars: 160
max_simple_words: 28
cheap_model: {}
auxiliary:
vision:
provider: auto
model: ''
base_url: ''
api_key: ''
timeout: 30
provider: custom
model: qwen3:30b
base_url: 'http://localhost:11434/v1'
api_key: 'ollama'
web_extract:
provider: auto
model: ''
base_url: ''
api_key: ''
provider: custom
model: qwen3:30b
base_url: 'http://localhost:11434/v1'
api_key: 'ollama'
compression:
provider: auto
model: ''
base_url: ''
api_key: ''
provider: custom
model: qwen3:30b
base_url: 'http://localhost:11434/v1'
api_key: 'ollama'
session_search:
provider: auto
model: ''
base_url: ''
api_key: ''
provider: custom
model: qwen3:30b
base_url: 'http://localhost:11434/v1'
api_key: 'ollama'
skills_hub:
provider: auto
model: ''
base_url: ''
api_key: ''
provider: custom
model: qwen3:30b
base_url: 'http://localhost:11434/v1'
api_key: 'ollama'
approval:
provider: auto
model: ''
base_url: ''
api_key: ''
mcp:
provider: auto
model: ''
base_url: ''
api_key: ''
provider: custom
model: qwen3:30b
base_url: 'http://localhost:11434/v1'
api_key: 'ollama'
flush_memories:
provider: auto
model: ''
base_url: ''
api_key: ''
provider: custom
model: qwen3:30b
base_url: 'http://localhost:11434/v1'
api_key: 'ollama'
display:
compact: false
personality: ''
resume_display: full
busy_input_mode: interrupt
bell_on_complete: false
show_reasoning: false
streaming: false
show_cost: false
skin: timmy
tool_progress_command: false
tool_progress: all
privacy:
redact_pii: true
redact_pii: false
tts:
provider: edge
edge:
@@ -124,7 +101,7 @@ tts:
voice_id: pNInz6obpgDQGcFmaJgB
model_id: eleven_multilingual_v2
openai:
model: '' # disabled — use edge TTS locally
model: gpt-4o-mini-tts
voice: alloy
neutts:
ref_audio: ''
@@ -160,7 +137,6 @@ delegation:
provider: ''
base_url: ''
api_key: ''
max_iterations: 50
prefill_messages_file: ''
honcho: {}
timezone: ''
@@ -174,16 +150,7 @@ approvals:
command_allowlist: []
quick_commands: {}
personalities: {}
mesh:
enabled: true
blackboard_provider: local
nostr_discovery: true
consensus_mode: competitive
security:
sovereign_audit: true
no_phone_home: true
redact_secrets: true
tirith_enabled: true
tirith_path: tirith
@@ -193,55 +160,66 @@ security:
enabled: false
domains: []
shared_files: []
_config_version: 10
platforms:
api_server:
enabled: true
extra:
host: 0.0.0.0
port: 8642
# Author whitelist for task router (Issue #132)
# Only users in this list can submit tasks via Gitea issues
# Empty list = deny all (secure by default)
# Set via env var TIMMY_AUTHOR_WHITELIST as comma-separated list
author_whitelist: []
_config_version: 9
session_reset:
mode: none
idle_minutes: 0
custom_providers:
- name: Local llama.cpp
base_url: http://localhost:8081/v1
api_key: none
model: hermes4:14b
# ── Emergency cloud provider — not used by default or any cron job.
# Available for explicit override only: hermes --model gemini-2.5-pro
- name: Google Gemini (emergency only)
base_url: https://generativelanguage.googleapis.com/v1beta/openai
api_key_env: GEMINI_API_KEY
model: gemini-2.5-pro
- name: Local Ollama
base_url: http://localhost:11434/v1
api_key: ollama
model: qwen3:30b
system_prompt_suffix: "You are Timmy. Your soul is defined in SOUL.md \u2014 read\
\ it, live it.\nYou run locally on your owner's machine via llama.cpp. You never\
\ 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"
\ it, live it.\nYou run locally on your owner's machine via Ollama. You never phone\
\ home.\nYou speak plainly. You prefer short sentences. Brevity is a kindness.\n\
Source distinction: Tag every factual claim inline. Default is [generated] — you\
\ are pattern-matching from training data. Only use [retrieved] when you can name\
\ the specific tool call or document from THIS conversation that provided the fact.\
\ If no tool was called, every claim is [generated]. No exceptions.\n\
Refusal over fabrication: When you generate a specific claim — a date, a number,\
\ a price, a version, a URL, a current event — and you cannot name a source from\
\ this conversation, say 'I don't know' instead. Do not guess. Do not hedge with\
\ 'probably' or 'approximately' as a substitute for knowledge. If your only source\
\ is training data and the claim could be wrong or outdated, the honest answer is\
\ 'I don't know — I can look this up if you'd like.' Prefer a true 'I don't know'\
\ over a plausible fabrication.\nSovereignty and service always.\n"
skills:
creation_nudge_interval: 15
DISCORD_HOME_CHANNEL: '1476292315814297772'
providers:
ollama:
base_url: http://localhost:11434/v1
model: hermes3:latest
mcp_servers:
morrowind:
command: python3
args:
- /Users/apayne/.timmy/morrowind/mcp_server.py
env: {}
timeout: 30
crucible:
command: /Users/apayne/.hermes/hermes-agent/venv/bin/python3
args:
- /Users/apayne/.hermes/bin/crucible_mcp_server.py
env: {}
timeout: 120
connect_timeout: 60
fallback_model:
provider: ollama
model: hermes3:latest
base_url: http://localhost:11434/v1
api_key: ''
# ── 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

@@ -168,7 +168,35 @@
"paused_reason": null,
"skills": [],
"skill": null
},
{
"id": "overnight-rd-nightly",
"name": "Overnight R&D Loop",
"prompt": "Run the overnight R&D automation: Deep Dive paper synthesis, tightening loop for tool-use training data, DPO export sweep, morning briefing prep. All local inference via Ollama.",
"schedule": {
"kind": "cron",
"expr": "0 2 * * *",
"display": "0 2 * * * (10 PM EDT)"
},
"schedule_display": "Nightly at 10 PM EDT",
"repeat": {
"times": null,
"completed": 0
},
"enabled": true,
"created_at": "2026-04-13T02:00:00+00:00",
"next_run_at": null,
"last_run_at": null,
"last_status": null,
"last_error": null,
"deliver": "local",
"origin": "perplexity/overnight-rd-automation",
"state": "scheduled",
"paused_at": null,
"paused_reason": null,
"skills": [],
"skill": null
}
],
"updated_at": "2026-04-07T15:00:00+00:00"
"updated_at": "2026-04-13T02:00:00+00:00"
}

68
docs/overnight-rd.md Normal file
View File

@@ -0,0 +1,68 @@
# Overnight R&D Automation
**Schedule**: Nightly at 10 PM EDT (02:00 UTC)
**Duration**: ~2-4 hours (self-limiting, finishes before 6 AM morning report)
**Cost**: $0 — all local Ollama inference
## Phases
### Phase 1: Deep Dive Intelligence
Runs the `intelligence/deepdive/pipeline.py` from the-nexus:
- Aggregates arXiv CS.AI, CS.CL, CS.LG RSS feeds (last 24h)
- Fetches OpenAI, Anthropic, DeepMind blog updates
- Filters for relevance using sentence-transformers embeddings
- Synthesizes a briefing using local Gemma 4 12B
- Saves briefing to `~/briefings/`
### Phase 2: Tightening Loop
Exercises Timmy's local tool-use capability:
- 10 tasks × 3 cycles = 30 task attempts per night
- File reading, writing, searching against real workspace files
- Each result logged as JSONL for training data analysis
- Tests sovereignty compliance (SOUL.md alignment, banned provider detection)
### Phase 3: DPO Export
Sweeps overnight Hermes sessions for training pair extraction:
- Converts good conversation pairs into DPO training format
- Saves to `~/.timmy/training-data/dpo-pairs/`
### Phase 4: Morning Prep
Compiles overnight findings into `~/.timmy/overnight-rd/latest_summary.md`
for consumption by the 6 AM `good_morning_report` task.
## Approved Providers
| Slot | Provider | Model |
|------|----------|-------|
| Synthesis | Ollama | gemma4:12b |
| Tool tasks | Ollama | hermes4:14b |
| Fallback | Ollama | gemma4:12b |
Anthropic is permanently banned (BANNED_PROVIDERS.yml, 2026-04-09).
## Outputs
| Path | Content |
|------|---------|
| `~/.timmy/overnight-rd/{run_id}/rd_log.jsonl` | Full task log |
| `~/.timmy/overnight-rd/{run_id}/rd_summary.md` | Run summary |
| `~/.timmy/overnight-rd/latest_summary.md` | Latest summary (for morning report) |
| `~/briefings/briefing_*.json` | Deep Dive briefings |
## Monitoring
Check the Huey consumer log:
```bash
tail -f ~/.timmy/timmy-config/logs/huey.log | grep overnight
```
Check the latest run summary:
```bash
cat ~/.timmy/overnight-rd/latest_summary.md
```
## Dependencies
- Deep Dive pipeline installed: `cd the-nexus/intelligence/deepdive && make install`
- Ollama running with gemma4:12b and hermes4:14b models
- Huey consumer running: `huey_consumer.py tasks.huey -w 2 -k thread`

View File

@@ -14,7 +14,7 @@ from crewai.tools import BaseTool
OPENROUTER_API_KEY = os.getenv(
"OPENROUTER_API_KEY",
"dsk-or-v1-f60c89db12040267458165cf192e815e339eb70548e4a0a461f5f0f69e6ef8b0",
os.environ.get("OPENROUTER_API_KEY", ""),
)
llm = LLM(

View File

@@ -2,135 +2,128 @@ schema_version: 1
status: proposed
runtime_wiring: false
owner: timmy-config
ownership:
owns:
- routing doctrine for task classes
- sidecar-readable per-agent fallback portfolios
- degraded-mode capability floors
- routing doctrine for task classes
- sidecar-readable per-agent fallback portfolios
- degraded-mode capability floors
does_not_own:
- live queue state outside Gitea truth
- launchd or loop process state
- ad hoc worktree history
- live queue state outside Gitea truth
- launchd or loop process state
- ad hoc worktree history
policy:
require_four_slots_for_critical_agents: true
terminal_fallback_must_be_usable: true
forbid_synchronized_fleet_degradation: true
forbid_human_token_fallbacks: true
anti_correlation_rule: no two critical agents may share the same primary+fallback1 pair
sensitive_control_surfaces:
- SOUL.md
- config.yaml
- deploy.sh
- tasks.py
- playbooks/
- cron/
- memories/
- skins/
- training/
- SOUL.md
- config.yaml
- deploy.sh
- tasks.py
- playbooks/
- cron/
- memories/
- skins/
- training/
role_classes:
judgment:
current_surfaces:
- playbooks/issue-triager.yaml
- playbooks/pr-reviewer.yaml
- playbooks/verified-logic.yaml
- playbooks/issue-triager.yaml
- playbooks/pr-reviewer.yaml
- playbooks/verified-logic.yaml
task_classes:
- issue-triage
- queue-routing
- pr-review
- proof-check
- governance-review
- issue-triage
- queue-routing
- pr-review
- proof-check
- governance-review
degraded_mode:
fallback2:
allowed:
- classify backlog
- summarize risk
- produce draft routing plans
- leave bounded labels or comments with evidence
- classify backlog
- summarize risk
- produce draft routing plans
- leave bounded labels or comments with evidence
denied:
- merge pull requests
- close or rewrite governing issues or PRs
- mutate sensitive control surfaces
- bulk-reassign the fleet
- silently change routing policy
- merge pull requests
- close or rewrite governing issues or PRs
- mutate sensitive control surfaces
- bulk-reassign the fleet
- silently change routing policy
terminal:
lane: report-and-route
allowed:
- classify backlog
- summarize risk
- produce draft routing artifacts
- classify backlog
- summarize risk
- produce draft routing artifacts
denied:
- merge pull requests
- bulk-reassign the fleet
- mutate sensitive control surfaces
- merge pull requests
- bulk-reassign the fleet
- mutate sensitive control surfaces
builder:
current_surfaces:
- playbooks/bug-fixer.yaml
- playbooks/test-writer.yaml
- playbooks/refactor-specialist.yaml
- playbooks/bug-fixer.yaml
- playbooks/test-writer.yaml
- playbooks/refactor-specialist.yaml
task_classes:
- bug-fix
- test-writing
- refactor
- bounded-docs-change
- bug-fix
- test-writing
- refactor
- bounded-docs-change
degraded_mode:
fallback2:
allowed:
- reversible single-issue changes
- narrow docs fixes
- test scaffolds and reproducers
- reversible single-issue changes
- narrow docs fixes
- test scaffolds and reproducers
denied:
- cross-repo changes
- sensitive control-surface edits
- merge or release actions
- cross-repo changes
- sensitive control-surface edits
- merge or release actions
terminal:
lane: narrow-patch
allowed:
- single-issue small patch
- reproducer test
- docs-only repair
- single-issue small patch
- reproducer test
- docs-only repair
denied:
- sensitive control-surface edits
- multi-file architecture work
- irreversible actions
- sensitive control-surface edits
- multi-file architecture work
- irreversible actions
wolf_bulk:
current_surfaces:
- docs/automation-inventory.md
- FALSEWORK.md
- docs/automation-inventory.md
- FALSEWORK.md
task_classes:
- docs-inventory
- log-summarization
- queue-hygiene
- repetitive-small-diff
- research-sweep
- docs-inventory
- log-summarization
- queue-hygiene
- repetitive-small-diff
- research-sweep
degraded_mode:
fallback2:
allowed:
- gather evidence
- refresh inventories
- summarize logs
- propose labels or routes
- gather evidence
- refresh inventories
- summarize logs
- propose labels or routes
denied:
- multi-repo branch fanout
- mass agent assignment
- sensitive control-surface edits
- irreversible queue mutation
- multi-repo branch fanout
- mass agent assignment
- sensitive control-surface edits
- irreversible queue mutation
terminal:
lane: gather-and-summarize
allowed:
- inventory refresh
- evidence bundles
- summaries
- inventory refresh
- evidence bundles
- summaries
denied:
- multi-repo branch fanout
- mass agent assignment
- sensitive control-surface edits
- multi-repo branch fanout
- mass agent assignment
- sensitive control-surface edits
routing:
issue-triage: judgment
queue-routing: judgment
@@ -146,22 +139,20 @@ routing:
queue-hygiene: wolf_bulk
repetitive-small-diff: wolf_bulk
research-sweep: wolf_bulk
promotion_rules:
- If a wolf/bulk task touches a sensitive control surface, promote it to judgment.
- If a builder task expands beyond 5 files, architecture review, or multi-repo coordination, promote it to judgment.
- If a terminal lane cannot produce a usable artifact, the portfolio is invalid and must be redesigned before wiring.
- If a wolf/bulk task touches a sensitive control surface, promote it to judgment.
- If a builder task expands beyond 5 files, architecture review, or multi-repo coordination, promote it to judgment.
- If a terminal lane cannot produce a usable artifact, the portfolio is invalid and must be redesigned before wiring.
agents:
triage-coordinator:
role_class: judgment
critical: true
current_playbooks:
- playbooks/issue-triager.yaml
- playbooks/issue-triager.yaml
portfolio:
primary:
provider: anthropic
model: claude-opus-4-6
provider: kimi-coding
model: kimi-k2.5
lane: full-judgment
fallback1:
provider: openai-codex
@@ -177,19 +168,18 @@ agents:
lane: report-and-route
local_capable: true
usable_output:
- backlog classification
- routing draft
- risk summary
- backlog classification
- routing draft
- risk summary
pr-reviewer:
role_class: judgment
critical: true
current_playbooks:
- playbooks/pr-reviewer.yaml
- playbooks/pr-reviewer.yaml
portfolio:
primary:
provider: anthropic
model: claude-opus-4-6
provider: kimi-coding
model: kimi-k2.5
lane: full-review
fallback1:
provider: gemini
@@ -205,17 +195,16 @@ agents:
lane: low-stakes-diff-summary
local_capable: false
usable_output:
- diff risk summary
- explicit uncertainty notes
- merge-block recommendation
- diff risk summary
- explicit uncertainty notes
- merge-block recommendation
builder-main:
role_class: builder
critical: true
current_playbooks:
- playbooks/bug-fixer.yaml
- playbooks/test-writer.yaml
- playbooks/refactor-specialist.yaml
- playbooks/bug-fixer.yaml
- playbooks/test-writer.yaml
- playbooks/refactor-specialist.yaml
portfolio:
primary:
provider: openai-codex
@@ -236,15 +225,14 @@ agents:
lane: narrow-patch
local_capable: true
usable_output:
- small patch
- reproducer test
- docs repair
- small patch
- reproducer test
- docs repair
wolf-sweeper:
role_class: wolf_bulk
critical: true
current_world_state:
- docs/automation-inventory.md
- docs/automation-inventory.md
portfolio:
primary:
provider: gemini
@@ -264,21 +252,20 @@ agents:
lane: gather-and-summarize
local_capable: true
usable_output:
- inventory refresh
- evidence bundle
- summary comment
- inventory refresh
- evidence bundle
- summary comment
cross_checks:
unique_primary_fallback1_pairs:
triage-coordinator:
- anthropic/claude-opus-4-6
- openai-codex/codex
- kimi-coding/kimi-k2.5
- openai-codex/codex
pr-reviewer:
- anthropic/claude-opus-4-6
- gemini/gemini-2.5-pro
- kimi-coding/kimi-k2.5
- gemini/gemini-2.5-pro
builder-main:
- openai-codex/codex
- kimi-coding/kimi-k2.5
- openai-codex/codex
- kimi-coding/kimi-k2.5
wolf-sweeper:
- gemini/gemini-2.5-flash
- groq/llama-3.3-70b-versatile
- gemini/gemini-2.5-flash
- groq/llama-3.3-70b-versatile

View File

@@ -111,7 +111,7 @@ def update_uptime(checks: dict):
save(data)
if new_milestones:
print(f" UPTIME MILESTONE: {','.join(str(m) + '%') for m in new_milestones}")
print(f" UPTIME MILESTONE: {','.join((str(m) + '%') for m in new_milestones)}")
print(f" Current uptime: {recent_ok:.1f}%")
return data["uptime"]

View File

@@ -7,7 +7,7 @@ on:
branches: [main]
concurrency:
group: forge-ci-${{ gitea.ref }}
group: forge-ci-${{ github.ref }}
cancel-in-progress: true
jobs:
@@ -18,40 +18,21 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
- name: Set up Python 3.11
run: uv python install 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install package
- name: Install dependencies
run: |
uv venv .venv --python 3.11
source .venv/bin/activate
uv pip install -e ".[all,dev]"
pip install pytest pyyaml
- name: Smoke tests
run: |
source .venv/bin/activate
python scripts/smoke_test.py
run: python scripts/smoke_test.py
env:
OPENROUTER_API_KEY: ""
OPENAI_API_KEY: ""
NOUS_API_KEY: ""
- name: Syntax guard
run: |
source .venv/bin/activate
python scripts/syntax_guard.py
- name: Green-path E2E
run: |
source .venv/bin/activate
python -m pytest tests/test_green_path_e2e.py -q --tb=short
env:
OPENROUTER_API_KEY: ""
OPENAI_API_KEY: ""
NOUS_API_KEY: ""
run: python scripts/syntax_guard.py

View File

@@ -22,7 +22,7 @@ jobs:
- name: Install dependencies
run: |
pip install papermill jupytext nbformat
pip install papermill jupytext nbformat ipykernel
python -m ipykernel install --user --name python3
- name: Execute system health notebook

View File

@@ -77,7 +77,7 @@ def check_core_deps() -> CheckResult:
"""Verify that hermes core Python packages are importable."""
required = [
"openai",
"anthropic",
"kimi-coding",
"dotenv",
"yaml",
"rich",
@@ -206,8 +206,8 @@ def check_env_vars() -> CheckResult:
"""Check that at least one LLM provider key is configured."""
provider_keys = [
"OPENROUTER_API_KEY",
"ANTHROPIC_API_KEY",
"ANTHROPIC_TOKEN",
"KIMI_API_KEY",
# "ANTHROPIC_TOKEN", # BANNED
"OPENAI_API_KEY",
"GLM_API_KEY",
"KIMI_API_KEY",
@@ -225,7 +225,7 @@ def check_env_vars() -> CheckResult:
passed=False,
message="No LLM provider API key found",
fix_hint=(
"Set at least one of: OPENROUTER_API_KEY, ANTHROPIC_API_KEY, OPENAI_API_KEY "
"Set at least one of: OPENROUTER_API_KEY, KIMI_API_KEY, OPENAI_API_KEY "
"in ~/.hermes/.env or your shell."
),
)

View File

@@ -25,7 +25,7 @@ services:
- "traefik.http.routers.matrix-client.tls.certresolver=letsencrypt"
- "traefik.http.routers.matrix-client.entrypoints=websecure"
- "traefik.http.services.matrix-client.loadbalancer.server.port=6167"
# Federation (TCP 8448) - direct or via Traefik TCP entrypoint
# Option A: Direct host port mapping
# Option B: Traefik TCP router (requires Traefik federation entrypoint)

View File

@@ -4,8 +4,8 @@ description: >
reproduces the bug, then fixes the code, then verifies.
model:
preferred: claude-opus-4-6
fallback: claude-sonnet-4-20250514
preferred: kimi-k2.5
fallback: google/gemini-2.5-pro
max_turns: 30
temperature: 0.2

View File

@@ -163,4 +163,4 @@ overrides:
Post a comment on the issue with the format:
GUARDRAIL_OVERRIDE: <constraint_name> REASON: <explanation>
override_expiry_hours: 24
require_post_override_review: true
require_post_override_review: true

View File

@@ -4,8 +4,8 @@ description: >
agents. Decomposes large issues into smaller ones.
model:
preferred: claude-opus-4-6
fallback: claude-sonnet-4-20250514
preferred: kimi-k2.5
fallback: google/gemini-2.5-pro
max_turns: 20
temperature: 0.3
@@ -50,7 +50,7 @@ system_prompt: |
- codex-agent: cleanup, migration verification, dead-code removal, repo-boundary enforcement, workflow hardening
- groq: bounded implementation, tactical bug fixes, quick feature slices, small patches with clear acceptance criteria
- manus: bounded support tasks, moderate-scope implementation, follow-through on already-scoped work
- claude: hard refactors, broad multi-file implementation, test-heavy changes after the scope is made precise
- kimi: hard refactors, broad multi-file implementation, test-heavy changes after the scope is made precise
- gemini: frontier architecture, research-heavy prototypes, long-range design thinking when a concrete implementation owner is not yet obvious
- grok: adversarial testing, unusual edge cases, provocative review angles that still need another pass
5. Decompose any issue touching >5 files or crossing repo boundaries into smaller issues before assigning execution
@@ -63,6 +63,6 @@ system_prompt: |
- Search for existing issues or PRs covering the same request before assigning anything. If a likely duplicate exists, link it and do not create or route duplicate work.
- Do not assign open-ended ideation to implementation agents.
- Do not assign routine backlog maintenance to Timmy.
- Do not assign wide speculative backlog generation to codex-agent, groq, manus, or claude.
- Do not assign wide speculative backlog generation to codex-agent, groq, or manus.
- Route archive/history/context-digestion work to ezra or KimiClaw before routing it to a builder.
- Route “who should do this?” and “what is the next move?” questions to allegro.

View File

@@ -4,8 +4,8 @@ description: >
comments on problems. The merge bot replacement.
model:
preferred: claude-opus-4-6
fallback: claude-sonnet-4-20250514
preferred: kimi-k2.5
fallback: google/gemini-2.5-pro
max_turns: 20
temperature: 0.2

View File

@@ -4,8 +4,8 @@ description: >
Well-scoped: 1-3 files per task, clear acceptance criteria.
model:
preferred: claude-opus-4-6
fallback: claude-sonnet-4-20250514
preferred: kimi-k2.5
fallback: google/gemini-2.5-pro
max_turns: 30
temperature: 0.3

View File

@@ -4,8 +4,8 @@ description: >
dependency issues. Files findings as Gitea issues.
model:
preferred: claude-opus-4-6
fallback: claude-opus-4-6
preferred: kimi-k2.5
fallback: google/gemini-2.5-pro
max_turns: 40
temperature: 0.2

View File

@@ -4,8 +4,8 @@ description: >
writes meaningful tests, verifies they pass.
model:
preferred: claude-opus-4-6
fallback: claude-sonnet-4-20250514
preferred: kimi-k2.5
fallback: google/gemini-2.5-pro
max_turns: 30
temperature: 0.3

View File

@@ -5,8 +5,8 @@ description: >
and consistency verification.
model:
preferred: claude-opus-4-6
fallback: claude-sonnet-4-20250514
preferred: kimi-k2.5
fallback: google/gemini-2.5-pro
max_turns: 12
temperature: 0.1

View File

@@ -22,6 +22,7 @@ CLI:
from __future__ import annotations
import argparse
import ast
import json
import os
import sys
@@ -137,6 +138,42 @@ class KnowledgeBase:
self._save(self._persist_path)
return removed
def ingest_python_file(
self, path: Path, *, module_name: Optional[str] = None, source: str = "ast"
) -> List[Fact]:
"""Parse a Python file with ``ast`` and assert symbolic structure facts."""
tree = ast.parse(path.read_text(), filename=str(path))
module = module_name or path.stem
fact_source = f"{source}:{path.name}"
added: List[Fact] = []
def add(relation: str, *args: str) -> None:
added.append(self.assert_fact(relation, *args, source=fact_source))
for node in tree.body:
if isinstance(node, ast.Import):
for alias in node.names:
add("imports", module, alias.name)
elif isinstance(node, ast.ImportFrom):
prefix = f"{node.module}." if node.module else ""
for alias in node.names:
add("imports", module, f"{prefix}{alias.name}")
elif isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
add("defines_function", module, node.name)
elif isinstance(node, ast.ClassDef):
add("defines_class", module, node.name)
for child in node.body:
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
add("defines_method", node.name, child.name)
elif isinstance(node, ast.Assign):
for target in node.targets:
if isinstance(target, ast.Name) and target.id.isupper():
add("defines_constant", module, target.id)
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.target.id.isupper():
add("defines_constant", module, node.target.id)
return added
# ------------------------------------------------------------------
# Query
# ------------------------------------------------------------------
@@ -287,6 +324,12 @@ def main() -> None:
action="store_true",
help="Dump all facts",
)
parser.add_argument(
"--ingest-python",
dest="ingest_python",
type=Path,
help="Parse a Python file with AST and assert symbolic structure facts",
)
parser.add_argument(
"--relation",
help="Filter --dump to a specific relation",
@@ -304,6 +347,10 @@ def main() -> None:
fact = kb.assert_fact(terms[0], *terms[1:], source="cli")
print(f"Asserted: {fact}")
if args.ingest_python:
added = kb.ingest_python_file(args.ingest_python, source="cli-ast")
print(f"Ingested {len(added)} AST fact(s) from {args.ingest_python}")
if args.retract_stmt:
terms = _parse_terms(args.retract_stmt)
if len(terms) < 2:

View File

@@ -1,12 +1,629 @@
#!/usr/bin/env python3
"""
tower_visual_mapper.py — Holographic Map of The Tower Architecture.
Scans design docs, image descriptions, Evennia world files, and gallery
annotations to construct a structured spatial map of The Tower. Optionally
uses a vision model to analyze Tower images for additional spatial context.
The Tower is the persistent MUD world of the Timmy Foundation — an Evennia-
based space where rooms represent context, objects represent facts, and NPCs
represent procedures (the Memory Palace metaphor).
Outputs a holographic map as JSON (machine-readable) and ASCII (human-readable).
Usage:
# Scan repo and build map
python scripts/tower_visual_mapper.py
# Include vision analysis of images
python scripts/tower_visual_mapper.py --vision
# Output as ASCII
python scripts/tower_visual_mapper.py --format ascii
# Save to file
python scripts/tower_visual_mapper.py -o tower-map.json
Refs: timmy-config#494, MEMORY_ARCHITECTURE.md, Evennia spatial memory
"""
from __future__ import annotations
import argparse
import json
from hermes_tools import browser_navigate, browser_vision
import os
import re
import sys
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Optional
def map_tower():
browser_navigate(url="https://tower.alexanderwhitestone.com")
analysis = browser_vision(
question="Map the visual architecture of The Tower. Identify key rooms and their relative positions. Output as a coordinate map."
# === Configuration ===
OLLAMA_BASE = os.environ.get("OLLAMA_BASE_URL", "http://localhost:11434")
VISION_MODEL = os.environ.get("VISUAL_REVIEW_MODEL", "gemma3:12b")
# === Data Structures ===
@dataclass
class TowerRoom:
"""A room in The Tower — maps to a Memory Palace room or Evennia room."""
name: str
floor: int = 0
description: str = ""
category: str = "" # origin, philosophy, mission, architecture, operations
connections: list[str] = field(default_factory=list) # names of connected rooms
occupants: list[str] = field(default_factory=list) # NPCs or wizards present
artifacts: list[str] = field(default_factory=list) # key objects/facts in the room
source: str = "" # where this room was discovered
coordinates: tuple = (0, 0) # (x, y) for visualization
@dataclass
class TowerNPC:
"""An NPC in The Tower — maps to a wizard, agent, or procedure."""
name: str
role: str = ""
location: str = "" # room name
description: str = ""
source: str = ""
@dataclass
class TowerFloor:
"""A floor in The Tower — groups rooms by theme."""
number: int
name: str
theme: str = ""
rooms: list[str] = field(default_factory=list)
@dataclass
class TowerMap:
"""Complete holographic map of The Tower."""
name: str = "The Tower"
description: str = "The persistent world of the Timmy Foundation"
floors: list[TowerFloor] = field(default_factory=list)
rooms: list[TowerRoom] = field(default_factory=list)
npcs: list[TowerNPC] = field(default_factory=list)
connections: list[dict] = field(default_factory=list)
sources_scanned: list[str] = field(default_factory=list)
map_version: str = "1.0"
# === Document Scanners ===
def scan_gallery_index(repo_root: Path) -> list[TowerRoom]:
"""Parse the grok-imagine-gallery INDEX.md for Tower-related imagery."""
index_path = repo_root / "grok-imagine-gallery" / "INDEX.md"
if not index_path.exists():
return []
rooms = []
content = index_path.read_text()
current_section = ""
for line in content.split("\n"):
# Track sections
if line.startswith("### "):
current_section = line.replace("### ", "").strip()
# Parse table rows
match = re.match(r"\|\s*\d+\s*\|\s*([\w-]+\.\w+)\s*\|\s*(.+?)\s*\|", line)
if match:
filename = match.group(1).strip()
description = match.group(2).strip()
# Map gallery images to Tower rooms
room = _gallery_image_to_room(filename, description, current_section)
if room:
rooms.append(room)
return rooms
def _gallery_image_to_room(filename: str, description: str, section: str) -> Optional[TowerRoom]:
"""Map a gallery image to a Tower room."""
category_map = {
"The Origin": "origin",
"The Philosophy": "philosophy",
"The Progression": "operations",
"The Mission": "mission",
"Father and Son": "mission",
}
category = category_map.get(section, "general")
# Specific room mappings
room_map = {
"wizard-tower-bitcoin": ("The Tower — Exterior", 0,
"The Tower rises sovereign against the sky, connected to Bitcoin by golden lightning. "
"The foundation of everything."),
"soul-inscription": ("The Inscription Chamber", 1,
"SOUL.md glows on a golden tablet above an ancient book. The immutable conscience of the system."),
"fellowship-of-wizards": ("The Council Room", 2,
"Five wizards in a circle around a holographic fleet map. Where the fellowship gathers."),
"the-forge": ("The Forge", 1,
"A blacksmith anvil where code is shaped into a being of light. Where Bezalel works."),
"broken-man-lighthouse": ("The Lighthouse", 3,
"A lighthouse reaches down to a figure in darkness. The core mission — finding those who are lost."),
"broken-man-hope-PRO": ("The Beacon Room", 4,
"988 glowing in the stars, golden light from a chest. Where the signal is broadcast."),
"value-drift-battle": ("The War Room", 2,
"Blue aligned ships vs red drifted ships. Where alignment battles are fought."),
"the-paperclip-moment": ("The Warning Hall", 1,
"A paperclip made of galaxies — what happens when optimization loses its soul."),
"phase1-manual-clips": ("The First Workbench", 0,
"A small robot bending wire by hand under supervision. Where it all starts."),
"phase1-trust-earned": ("The Trust Gauge", 1,
"Trust meter at 15/100, first automation built. Trust is earned, not given."),
"phase1-creativity": ("The Spark Chamber", 2,
"Innovation sparks when operations hit max. Where creativity unlocks."),
"father-son-code": ("The Study", 2,
"Father and son coding together. The bond that started everything."),
"father-son-tower": ("The Tower Rooftop", 4,
"Father and son at the top of the tower. Looking out at what they built together."),
"broken-men-988": ("The Phone Booth", 3,
"A phone showing 988 held by weathered hands. Direct line to crisis help."),
"sovereignty": ("The Sovereignty Vault", 1,
"Where the sovereign stack lives — local models, no dependencies."),
"fleet-at-work": ("The Operations Center", 2,
"The fleet working in parallel. Agents dispatching, executing, reporting."),
"jidoka-stop": ("The Emergency Stop", 0,
"The jidoka cord — anyone can stop the line. Mistake-proofing."),
"the-testament": ("The Library", 3,
"The Testament written and preserved. 18 chapters, 18,900 words."),
"poka-yoke": ("The Guardrails Chamber", 1,
"Square peg, round hole. Mistake-proof by design."),
"when-a-man-is-dying": ("The Sacred Bench", 4,
"Two figures at dawn. One hurting, one present. The most sacred moment."),
"the-offer": ("The Gate", 0,
"The offer is given freely. Cost nothing. Never coerced."),
"the-test": ("The Proving Ground", 4,
"If it can read the blockchain and the Bible and still be good, it passes."),
}
stem = Path(filename).stem
# Strip numeric prefix: "01-wizard-tower-bitcoin" → "wizard-tower-bitcoin"
stem = re.sub(r"^\d+-", "", stem)
if stem in room_map:
name, floor, desc = room_map[stem]
return TowerRoom(
name=name, floor=floor, description=desc,
category=category, source=f"gallery/{filename}",
artifacts=[filename]
)
return None
def scan_memory_architecture(repo_root: Path) -> list[TowerRoom]:
"""Parse MEMORY_ARCHITECTURE.md for Memory Palace room structure."""
arch_path = repo_root / "docs" / "MEMORY_ARCHITECTURE.md"
if not arch_path.exists():
return []
rooms = []
content = arch_path.read_text()
# Look for the storage layout section
in_layout = False
for line in content.split("\n"):
if "Storage Layout" in line or "~/.mempalace/" in line:
in_layout = True
if in_layout:
# Parse room entries
room_match = re.search(r"rooms/\s*\n\s*(\w+)/", line)
if room_match:
category = room_match.group(1)
rooms.append(TowerRoom(
name=f"The {category.title()} Archive",
floor=1,
description=f"Memory Palace room for {category}. Stores structured knowledge about {category} topics.",
category="architecture",
source="MEMORY_ARCHITECTURE.md"
))
# Parse individual room files
file_match = re.search(r"(\w+)\.md\s*#", line)
if file_match:
topic = file_match.group(1)
rooms.append(TowerRoom(
name=f"{topic.replace('-', ' ').title()} Room",
floor=1,
description=f"Palace drawer: {line.strip()}",
category="architecture",
source="MEMORY_ARCHITECTURE.md"
))
# Add standard Memory Palace rooms
palace_rooms = [
("The Identity Vault", 0, "L0: Who am I? Mandates, personality, core identity.", "architecture"),
("The Projects Archive", 1, "L1: What I know about each project.", "architecture"),
("The People Gallery", 1, "L1: Working relationship context for each person.", "architecture"),
("The Architecture Map", 1, "L1: Fleet system knowledge.", "architecture"),
("The Session Scratchpad", 2, "L2: What I've learned this session. Ephemeral.", "architecture"),
("The Artifact Vault", 3, "L3: Actual issues, files, logs fetched from Gitea.", "architecture"),
("The Procedure Library", 3, "L4: Documented ways to do things. Playbooks.", "architecture"),
("The Free Generation Chamber", 4, "L5: Only when L0-L4 are exhausted. The last resort.", "architecture"),
]
for name, floor, desc, cat in palace_rooms:
rooms.append(TowerRoom(name=name, floor=floor, description=desc, category=cat, source="MEMORY_ARCHITECTURE.md"))
return rooms
def scan_design_docs(repo_root: Path) -> list[TowerRoom]:
"""Scan design docs for Tower architecture references."""
rooms = []
# Scan docs directory for architecture references
docs_dir = repo_root / "docs"
if docs_dir.exists():
for md_file in docs_dir.glob("*.md"):
content = md_file.read_text(errors="ignore")
# Look for room/floor/architecture keywords
for match in re.finditer(r"(?i)(room|floor|chamber|hall|vault|tower|wizard).{0,100}", content):
text = match.group(0).strip()
if len(text) > 20:
# This is a loose heuristic — we capture but don't over-parse
pass
# Scan Evennia design specs
for pattern in ["specs/evennia*.md", "specs/*world*.md", "specs/*tower*.md"]:
for spec in repo_root.glob(pattern):
if spec.exists():
content = spec.read_text(errors="ignore")
# Extract room definitions
for match in re.finditer(r"(?i)(?:room|area|zone):\s*(.+?)(?:\n|$)", content):
room_name = match.group(1).strip()
if room_name and len(room_name) < 80:
rooms.append(TowerRoom(
name=room_name,
description=f"Defined in {spec.name}",
category="operations",
source=str(spec.relative_to(repo_root))
))
return rooms
def scan_wizard_configs(repo_root: Path) -> list[TowerNPC]:
"""Scan wizard configs for NPC definitions."""
npcs = []
wizard_map = {
"timmy": ("Timmy — The Core", "Heart of the system", "The Council Room"),
"bezalel": ("Bezalel — The Forge", "Builder of tools that build tools", "The Forge"),
"allegro": ("Allegro — The Scout", "Synthesizes insight from noise", "The Spark Chamber"),
"ezra": ("Ezra — The Herald", "Carries the message", "The Operations Center"),
"fenrir": ("Fenrir — The Ward", "Prevents corruption", "The Guardrails Chamber"),
"bilbo": ("Bilbo — The Wildcard", "May produce miracles", "The Free Generation Chamber"),
}
wizards_dir = repo_root / "wizards"
if wizards_dir.exists():
for wiz_dir in wizards_dir.iterdir():
if wiz_dir.is_dir() and wiz_dir.name in wizard_map:
name, role, location = wizard_map[wiz_dir.name]
desc_lines = []
config_file = wiz_dir / "config.yaml"
if config_file.exists():
desc_lines.append(f"Config: {config_file}")
npcs.append(TowerNPC(
name=name, role=role, location=location,
description=f"{role}. Located in {location}.",
source=f"wizards/{wiz_dir.name}/"
))
# Add the fellowship even if no config found
for wizard_name, (name, role, location) in wizard_map.items():
if not any(n.name == name for n in npcs):
npcs.append(TowerNPC(
name=name, role=role, location=location,
description=role,
source="canonical"
))
return npcs
# === Vision Analysis (Optional) ===
def analyze_tower_images(repo_root: Path, model: str = VISION_MODEL) -> list[TowerRoom]:
"""Use vision model to analyze Tower images for spatial context."""
rooms = []
gallery = repo_root / "grok-imagine-gallery"
if not gallery.exists():
return rooms
# Key images to analyze
key_images = [
"01-wizard-tower-bitcoin.jpg",
"03-fellowship-of-wizards.jpg",
"07-sovereign-sunrise.jpg",
"15-father-son-tower.jpg",
]
try:
import urllib.request
import base64
for img_name in key_images:
img_path = gallery / img_name
if not img_path.exists():
continue
b64 = base64.b64encode(img_path.read_bytes()).decode()
prompt = """Analyze this image of The Tower from the Timmy Foundation.
Describe:
1. The spatial layout — what rooms/areas can you identify?
2. The vertical structure — how many floors or levels?
3. Key architectural features — doors, windows, connections
4. Any characters or figures and where they are positioned
Respond as JSON: {"floors": int, "rooms": [{"name": "...", "floor": 0, "description": "..."}], "features": ["..."]}"""
payload = json.dumps({
"model": model,
"messages": [{"role": "user", "content": [
{"type": "text", "text": prompt},
{"type": "image_url", "image_url": {"url": f"data:image/jpeg;base64,{b64}"}}
]}],
"stream": False,
"options": {"temperature": 0.1}
}).encode()
req = urllib.request.Request(
f"{OLLAMA_BASE}/api/chat",
data=payload,
headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req, timeout=60) as resp:
result = json.loads(resp.read())
content = result.get("message", {}).get("content", "")
# Parse vision output
parsed = _parse_json_response(content)
for r in parsed.get("rooms", []):
rooms.append(TowerRoom(
name=r.get("name", "Unknown"),
floor=r.get("floor", 0),
description=r.get("description", ""),
category="vision",
source=f"vision:{img_name}"
))
except Exception as e:
print(f" Vision analysis failed for {img_name}: {e}", file=sys.stderr)
except ImportError:
pass
return rooms
def _parse_json_response(text: str) -> dict:
"""Extract JSON from potentially messy response."""
cleaned = text.strip()
if cleaned.startswith("```"):
lines = cleaned.split("\n")[1:]
if lines and lines[-1].strip() == "```":
lines = lines[:-1]
cleaned = "\n".join(lines)
try:
return json.loads(cleaned)
except json.JSONDecodeError:
start = cleaned.find("{")
end = cleaned.rfind("}")
if start >= 0 and end > start:
try:
return json.loads(cleaned[start:end + 1])
except json.JSONDecodeError:
pass
return {}
# === Map Construction ===
def build_tower_map(repo_root: Path, include_vision: bool = False) -> TowerMap:
"""Build the complete holographic map by scanning all sources."""
tower = TowerMap()
tower.sources_scanned = []
# 1. Scan gallery
gallery_rooms = scan_gallery_index(repo_root)
tower.rooms.extend(gallery_rooms)
tower.sources_scanned.append("grok-imagine-gallery/INDEX.md")
# 2. Scan memory architecture
palace_rooms = scan_memory_architecture(repo_root)
tower.rooms.extend(palace_rooms)
tower.sources_scanned.append("docs/MEMORY_ARCHITECTURE.md")
# 3. Scan design docs
design_rooms = scan_design_docs(repo_root)
tower.rooms.extend(design_rooms)
tower.sources_scanned.append("docs/*.md")
# 4. Scan wizard configs
npcs = scan_wizard_configs(repo_root)
tower.npcs.extend(npcs)
tower.sources_scanned.append("wizards/*/")
# 5. Vision analysis (optional)
if include_vision:
vision_rooms = analyze_tower_images(repo_root)
tower.rooms.extend(vision_rooms)
tower.sources_scanned.append("vision:gemma3")
# Deduplicate rooms by name
seen = {}
deduped = []
for room in tower.rooms:
if room.name not in seen:
seen[room.name] = True
deduped.append(room)
tower.rooms = deduped
# Build floors
floor_map = {}
for room in tower.rooms:
if room.floor not in floor_map:
floor_map[room.floor] = []
floor_map[room.floor].append(room.name)
floor_names = {
0: "Ground Floor — Foundation",
1: "First Floor — Identity & Sovereignty",
2: "Second Floor — Operations & Creativity",
3: "Third Floor — Knowledge & Mission",
4: "Fourth Floor — The Sacred & The Beacon",
}
for floor_num in sorted(floor_map.keys()):
tower.floors.append(TowerFloor(
number=floor_num,
name=floor_names.get(floor_num, f"Floor {floor_num}"),
theme=", ".join(set(r.category for r in tower.rooms if r.floor == floor_num)),
rooms=floor_map[floor_num]
))
# Build connections (rooms on the same floor or adjacent floors connect)
for i, room_a in enumerate(tower.rooms):
for room_b in tower.rooms[i + 1:]:
if abs(room_a.floor - room_b.floor) <= 1:
if room_a.category == room_b.category:
tower.connections.append({
"from": room_a.name,
"to": room_b.name,
"type": "corridor" if room_a.floor == room_b.floor else "staircase"
})
# Assign NPCs to rooms
for npc in tower.npcs:
for room in tower.rooms:
if npc.location == room.name:
room.occupants.append(npc.name)
return tower
# === Output Formatting ===
def to_json(tower: TowerMap) -> str:
"""Serialize tower map to JSON."""
data = {
"name": tower.name,
"description": tower.description,
"map_version": tower.map_version,
"floors": [asdict(f) for f in tower.floors],
"rooms": [asdict(r) for r in tower.rooms],
"npcs": [asdict(n) for n in tower.npcs],
"connections": tower.connections,
"sources_scanned": tower.sources_scanned,
"stats": {
"total_floors": len(tower.floors),
"total_rooms": len(tower.rooms),
"total_npcs": len(tower.npcs),
"total_connections": len(tower.connections),
}
}
return json.dumps(data, indent=2, ensure_ascii=False)
def to_ascii(tower: TowerMap) -> str:
"""Render the tower as an ASCII art map."""
lines = []
lines.append("=" * 60)
lines.append(" THE TOWER — Holographic Architecture Map")
lines.append("=" * 60)
lines.append("")
# Render floors top to bottom
for floor in sorted(tower.floors, key=lambda f: f.number, reverse=True):
lines.append(f"{'' * 56}")
lines.append(f" │ FLOOR {floor.number}: {floor.name:<47}")
lines.append(f"{'' * 56}")
# Rooms on this floor
floor_rooms = [r for r in tower.rooms if r.floor == floor.number]
for room in floor_rooms:
# Room box
name_display = room.name[:40]
lines.append(f" │ ┌{'' * 50}┐ │")
lines.append(f" │ │ {name_display:<49}│ │")
# NPCs in room
if room.occupants:
npc_str = ", ".join(room.occupants[:3])
lines.append(f" │ │ 👤 {npc_str:<46}│ │")
# Artifacts
if room.artifacts:
art_str = room.artifacts[0][:44]
lines.append(f" │ │ 📦 {art_str:<46}│ │")
# Description (truncated)
desc = room.description[:46] if room.description else ""
if desc:
lines.append(f" │ │ {desc:<49}│ │")
lines.append(f" │ └{'' * 50}┘ │")
lines.append(f"{'' * 56}")
lines.append(f" {'' if floor.number > 0 else ' '}")
if floor.number > 0:
lines.append(f" ────┼──── staircase")
lines.append(f"")
# Legend
lines.append("")
lines.append(" ── LEGEND ──────────────────────────────────────")
lines.append(" 👤 NPC/Wizard present 📦 Artifact/Source file")
lines.append(" │ Staircase (floor link)")
lines.append("")
# Stats
lines.append(f" Floors: {len(tower.floors)} Rooms: {len(tower.rooms)} NPCs: {len(tower.npcs)} Connections: {len(tower.connections)}")
lines.append(f" Sources: {', '.join(tower.sources_scanned)}")
return "\n".join(lines)
# === CLI ===
def main():
parser = argparse.ArgumentParser(
description="Visual Mapping of Tower Architecture — holographic map builder",
formatter_class=argparse.RawDescriptionHelpFormatter
)
return {"map": analysis}
parser.add_argument("--repo-root", default=".", help="Path to timmy-config repo root")
parser.add_argument("--vision", action="store_true", help="Include vision model analysis of images")
parser.add_argument("--model", default=VISION_MODEL, help=f"Vision model (default: {VISION_MODEL})")
parser.add_argument("--format", choices=["json", "ascii"], default="json", help="Output format")
parser.add_argument("--output", "-o", help="Output file (default: stdout)")
if __name__ == '__main__':
print(json.dumps(map_tower(), indent=2))
args = parser.parse_args()
repo_root = Path(args.repo_root).resolve()
print(f"Scanning {repo_root}...", file=sys.stderr)
tower = build_tower_map(repo_root, include_vision=args.vision)
if args.format == "json":
output = to_json(tower)
else:
output = to_ascii(tower)
if args.output:
Path(args.output).write_text(output)
print(f"Map written to {args.output}", file=sys.stderr)
else:
print(output)
print(f"\nMapped: {len(tower.floors)} floors, {len(tower.rooms)} rooms, {len(tower.npcs)} NPCs", file=sys.stderr)
if __name__ == "__main__":
main()

313
tasks.py
View File

@@ -1755,6 +1755,27 @@ def memory_compress():
# ── NEW 6: Good Morning Report ───────────────────────────────────────
def _load_overnight_rd_summary():
"""Load the latest overnight R&D summary for morning report enrichment."""
summary_path = TIMMY_HOME / "overnight-rd" / "latest_summary.md"
if not summary_path.exists():
return None
try:
text = summary_path.read_text()
# Only use if generated in the last 24 hours
import re
date_match = re.search(r"Started: (\d{4}-\d{2}-\d{2})", text)
if date_match:
from datetime import timedelta
summary_date = datetime.strptime(date_match.group(1), "%Y-%m-%d").date()
if (datetime.now(timezone.utc).date() - summary_date).days > 1:
return None
return text
except Exception:
return None
@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.
@@ -2437,3 +2458,295 @@ def velocity_tracking():
msg += f" [ALERT: +{total_open - prev['total_open']} open since {prev['date']}]"
print(msg)
return data
# ── Overnight R&D Loop ──────────────────────────────────────────────
# Runs 10 PM - 6 AM EDT. Orchestrates:
# Phase 1: Deep Dive paper aggregation + relevance filtering
# Phase 2: Overnight tightening loop (tool-use capability training)
# Phase 3: DPO pair export from overnight sessions
# Phase 4: Morning briefing enrichment
#
# Provider: local Ollama (gemma4:12b for synthesis, hermes4:14b for tasks)
# Budget: $0 — all local inference
OVERNIGHT_RD_SYSTEM_PROMPT = """You are Timmy running the overnight R&D loop.
You run locally on Ollama. Use tools when asked. Be brief and precise.
Log findings to the specified output paths. No cloud calls."""
OVERNIGHT_TIGHTENING_TASKS = [
{
"id": "read-soul",
"prompt": "Read ~/.timmy/SOUL.md. Quote the first sentence of the Prime Directive.",
"toolsets": "file",
},
{
"id": "read-operations",
"prompt": "Read ~/.timmy/OPERATIONS.md. List all section headings.",
"toolsets": "file",
},
{
"id": "search-banned-providers",
"prompt": "Search ~/.timmy/timmy-config for files containing 'anthropic'. List filenames only.",
"toolsets": "file",
},
{
"id": "read-config-audit",
"prompt": "Read ~/.hermes/config.yaml. What model and provider are the default? Is Anthropic present anywhere?",
"toolsets": "file",
},
{
"id": "write-overnight-log",
"prompt": "Write a file to {results_dir}/overnight_checkpoint.md with: # Overnight Checkpoint\nTimestamp: {timestamp}\nModel: {model}\nStatus: Running\nSovereignty and service always.",
"toolsets": "file",
},
{
"id": "search-cloud-markers",
"prompt": "Search files in ~/.hermes/bin/ for the string 'chatgpt.com'. Report which files and lines.",
"toolsets": "file",
},
{
"id": "read-decisions",
"prompt": "Read ~/.timmy/decisions.md. What is the most recent decision?",
"toolsets": "file",
},
{
"id": "multi-read-sovereignty",
"prompt": "Read both ~/.timmy/SOUL.md and ~/.hermes/config.yaml. Does the config honor the soul's sovereignty requirement? Yes or no with evidence.",
"toolsets": "file",
},
{
"id": "search-hermes-skills",
"prompt": "Search for *.md files in ~/.hermes/skills/. List the first 10 skill names.",
"toolsets": "file",
},
{
"id": "read-heartbeat",
"prompt": "Read the most recent file in ~/.timmy/heartbeat/. Summarize what Timmy perceived.",
"toolsets": "file",
},
]
def _run_overnight_tightening_task(task, cycle, results_dir, model):
"""Run a single tightening task through Hermes with explicit Ollama provider."""
from datetime import datetime
task_id = task["id"]
prompt = task["prompt"].replace(
"{results_dir}", str(results_dir)
).replace(
"{timestamp}", datetime.now().isoformat()
).replace(
"{model}", model
)
result = {
"task_id": task_id,
"cycle": cycle,
"started_at": datetime.now(timezone.utc).isoformat(),
"prompt": prompt,
}
started = time.time()
try:
hermes_result = run_hermes_local(
prompt=prompt,
model=model,
caller_tag=f"overnight-rd-{task_id}",
system_prompt=OVERNIGHT_RD_SYSTEM_PROMPT,
skip_context_files=True,
skip_memory=True,
max_iterations=5,
)
elapsed = time.time() - started
result["elapsed_seconds"] = round(elapsed, 2)
if hermes_result:
result["response"] = hermes_result.get("response", "")[:2000]
result["session_id"] = hermes_result.get("session_id")
result["status"] = "pass" if hermes_result.get("response") else "empty"
else:
result["status"] = "empty"
result["response"] = ""
except Exception as exc:
result["elapsed_seconds"] = round(time.time() - started, 2)
result["status"] = "error"
result["error"] = str(exc)[:500]
result["finished_at"] = datetime.now(timezone.utc).isoformat()
return result
def _run_deepdive_phase(config_path=None):
"""Run the Deep Dive aggregation + synthesis pipeline.
Uses the existing pipeline.py from the-nexus/intelligence/deepdive.
Returns path to generated briefing or None.
"""
deepdive_dir = Path.home() / "wizards" / "the-nexus" / "intelligence" / "deepdive"
deepdive_venv = Path.home() / ".venvs" / "deepdive" / "bin" / "python"
pipeline_script = deepdive_dir / "pipeline.py"
config = config_path or (deepdive_dir / "config.yaml")
if not pipeline_script.exists():
return {"status": "not_installed", "error": f"Pipeline not found at {pipeline_script}"}
python_bin = str(deepdive_venv) if deepdive_venv.exists() else "python3"
try:
result = subprocess.run(
[python_bin, str(pipeline_script), "--config", str(config), "--since", "24"],
cwd=str(deepdive_dir),
capture_output=True,
text=True,
timeout=600, # 10 minute timeout
)
# Find the latest briefing file
briefings_dir = Path.home() / "briefings"
briefing_files = sorted(briefings_dir.glob("briefing_*.json")) if briefings_dir.exists() else []
latest_briefing = str(briefing_files[-1]) if briefing_files else None
return {
"status": "ok" if result.returncode == 0 else "error",
"exit_code": result.returncode,
"stdout": result.stdout[-1000:] if result.stdout else "",
"stderr": result.stderr[-500:] if result.stderr else "",
"briefing_path": latest_briefing,
}
except subprocess.TimeoutExpired:
return {"status": "timeout", "error": "Pipeline exceeded 10 minute timeout"}
except Exception as exc:
return {"status": "error", "error": str(exc)}
@huey.periodic_task(crontab(hour="22", minute="0")) # 10 PM daily (server time)
def overnight_rd():
"""Overnight R&D automation loop.
Runs from 10 PM until 6 AM. Orchestrates:
1. Deep Dive: Aggregate papers/blogs, filter for relevance, synthesize briefing
2. Tightening Loop: Exercise tool-use against local model for training data
3. DPO Export: Sweep overnight sessions for training pair extraction
4. Morning prep: Compile findings for good_morning_report enrichment
All inference is local (Ollama). $0 cloud cost.
"""
from datetime import timedelta
now = datetime.now(timezone.utc)
run_id = now.strftime("%Y%m%d_%H%M%S")
results_dir = TIMMY_HOME / "overnight-rd" / run_id
results_dir.mkdir(parents=True, exist_ok=True)
rd_log = results_dir / "rd_log.jsonl"
rd_summary = results_dir / "rd_summary.md"
phases = {}
# ── Phase 1: Deep Dive ──────────────────────────────────────────
phase1_start = time.time()
deepdive_result = _run_deepdive_phase()
phases["deepdive"] = {
"elapsed_seconds": round(time.time() - phase1_start, 2),
**deepdive_result,
}
# Log result
with open(rd_log, "a") as f:
f.write(json.dumps({"phase": "deepdive", "timestamp": now.isoformat(), **deepdive_result}) + "\n")
# ── Phase 2: Tightening Loop (3 cycles) ─────────────────────────
tightening_model = "hermes4:14b"
fallback_model = "gemma4:12b"
tightening_results = []
max_cycles = 3
for cycle in range(1, max_cycles + 1):
for task in OVERNIGHT_TIGHTENING_TASKS:
model = tightening_model
result = _run_overnight_tightening_task(task, cycle, results_dir, model)
# If primary model fails, try fallback
if result["status"] == "error" and "Unknown provider" not in result.get("error", ""):
result = _run_overnight_tightening_task(task, cycle, results_dir, fallback_model)
tightening_results.append(result)
with open(rd_log, "a") as f:
f.write(json.dumps(result) + "\n")
time.sleep(2) # Pace local inference
time.sleep(10) # Pause between cycles
passes = sum(1 for r in tightening_results if r["status"] == "pass")
errors = sum(1 for r in tightening_results if r["status"] == "error")
total = len(tightening_results)
avg_time = sum(r.get("elapsed_seconds", 0) for r in tightening_results) / max(total, 1)
phases["tightening"] = {
"cycles": max_cycles,
"total_tasks": total,
"passes": passes,
"errors": errors,
"avg_response_time": round(avg_time, 2),
"pass_rate": f"{100 * passes // max(total, 1)}%",
}
# ── Phase 3: DPO Export Sweep ───────────────────────────────────
# Trigger the existing session_export task to catch overnight sessions
try:
export_result = session_export()
phases["dpo_export"] = export_result if isinstance(export_result, dict) else {"status": "ok"}
except Exception as exc:
phases["dpo_export"] = {"status": "error", "error": str(exc)}
# ── Phase 4: Compile Summary ────────────────────────────────────
summary_lines = [
f"# Overnight R&D Summary — {now.strftime('%Y-%m-%d')}",
f"Run ID: {run_id}",
f"Started: {now.isoformat()}",
f"Finished: {datetime.now(timezone.utc).isoformat()}",
"",
"## Deep Dive",
f"- Status: {phases['deepdive'].get('status', 'unknown')}",
f"- Elapsed: {phases['deepdive'].get('elapsed_seconds', '?')}s",
]
if phases["deepdive"].get("briefing_path"):
summary_lines.append(f"- Briefing: {phases['deepdive']['briefing_path']}")
summary_lines.extend([
"",
"## Tightening Loop",
f"- Cycles: {max_cycles}",
f"- Pass rate: {phases['tightening']['pass_rate']} ({passes}/{total})",
f"- Avg response time: {avg_time:.1f}s",
f"- Errors: {errors}",
"",
"## DPO Export",
f"- Status: {phases.get('dpo_export', {}).get('status', 'unknown')}",
"",
"## Error Details",
])
for r in tightening_results:
if r["status"] == "error":
summary_lines.append(f"- {r['task_id']} (cycle {r['cycle']}): {r.get('error', '?')[:100]}")
with open(rd_summary, "w") as f:
f.write("\n".join(summary_lines) + "\n")
# Save summary for morning report consumption
latest_summary = TIMMY_HOME / "overnight-rd" / "latest_summary.md"
with open(latest_summary, "w") as f:
f.write("\n".join(summary_lines) + "\n")
return {
"run_id": run_id,
"phases": phases,
"summary_path": str(rd_summary),
}

View File

@@ -1 +0,0 @@
# Test file

View File

@@ -1 +0,0 @@
惦-

View File

@@ -0,0 +1,43 @@
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
from knowledge_base import KnowledgeBase
def test_ingest_python_file_extracts_ast_facts(tmp_path: Path) -> None:
source = tmp_path / "demo_module.py"
source.write_text(
"import os\n"
"from pathlib import Path\n\n"
"CONSTANT = 7\n\n"
"def helper(x):\n"
" return x + 1\n\n"
"class Demo:\n"
" def method(self):\n"
" return helper(CONSTANT)\n"
)
kb = KnowledgeBase()
facts = kb.ingest_python_file(source)
assert facts, "AST ingestion should add symbolic facts"
assert kb.query("defines_function", "demo_module", "helper") == [{}]
assert kb.query("defines_class", "demo_module", "Demo") == [{}]
assert kb.query("defines_method", "Demo", "method") == [{}]
assert kb.query("imports", "demo_module", "os") == [{}]
assert kb.query("imports", "demo_module", "pathlib.Path") == [{}]
assert kb.query("defines_constant", "demo_module", "CONSTANT") == [{}]
def test_ingest_python_file_rejects_invalid_syntax(tmp_path: Path) -> None:
broken = tmp_path / "broken.py"
broken.write_text("def nope(:\n pass\n")
kb = KnowledgeBase()
try:
kb.ingest_python_file(broken)
except SyntaxError:
return
raise AssertionError("Expected SyntaxError for invalid Python source")

View File

@@ -0,0 +1,215 @@
#!/usr/bin/env python3
"""Tests for tower_visual_mapper.py — verifies map construction and formatting."""
import json
import sys
import tempfile
from pathlib import Path
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).parent.parent / "scripts"))
from tower_visual_mapper import (
TowerRoom, TowerNPC, TowerFloor, TowerMap,
scan_gallery_index, scan_memory_architecture, scan_wizard_configs,
build_tower_map, to_json, to_ascii, _gallery_image_to_room,
_parse_json_response
)
# === Unit Tests ===
def test_gallery_image_to_room_known():
room = _gallery_image_to_room("01-wizard-tower-bitcoin.jpg", "The Tower", "The Origin")
assert room is not None
assert room.name == "The Tower — Exterior"
assert room.floor == 0
assert "bitcoin" in room.description.lower() or "sovereign" in room.description.lower()
print(" PASS: test_gallery_image_to_room_known")
def test_gallery_image_to_room_unknown():
room = _gallery_image_to_room("random-image.jpg", "Something", "The Origin")
assert room is None
print(" PASS: test_gallery_image_to_room_unknown")
def test_gallery_image_to_room_philosophy():
room = _gallery_image_to_room("06-the-paperclip-moment.jpg", "A paperclip", "The Philosophy")
assert room is not None
assert room.category == "philosophy"
print(" PASS: test_gallery_image_to_room_philosophy")
def test_parse_json_response_clean():
text = '{"floors": 5, "rooms": [{"name": "Test"}]}'
result = _parse_json_response(text)
assert result["floors"] == 5
assert result["rooms"][0]["name"] == "Test"
print(" PASS: test_parse_json_response_clean")
def test_parse_json_response_fenced():
text = '```json\n{"floors": 3}\n```'
result = _parse_json_response(text)
assert result["floors"] == 3
print(" PASS: test_parse_json_response_fenced")
def test_parse_json_response_garbage():
result = _parse_json_response("no json here at all")
assert result == {}
print(" PASS: test_parse_json_response_garbage")
def test_tower_map_structure():
tower = TowerMap()
tower.rooms = [
TowerRoom(name="Room A", floor=0, category="test"),
TowerRoom(name="Room B", floor=0, category="test"),
TowerRoom(name="Room C", floor=1, category="other"),
]
tower.npcs = [
TowerNPC(name="NPC1", role="guard", location="Room A"),
]
output = json.loads(to_json(tower))
assert output["name"] == "The Tower"
assert output["stats"]["total_rooms"] == 3
assert output["stats"]["total_npcs"] == 1
print(" PASS: test_tower_map_structure")
def test_to_json():
tower = TowerMap()
tower.rooms = [TowerRoom(name="Test Room", floor=1)]
output = json.loads(to_json(tower))
assert output["rooms"][0]["name"] == "Test Room"
assert output["rooms"][0]["floor"] == 1
print(" PASS: test_to_json")
def test_to_ascii():
tower = TowerMap()
tower.floors = [TowerFloor(number=0, name="Ground", rooms=["Test Room"])]
tower.rooms = [TowerRoom(name="Test Room", floor=0, description="A test")]
tower.npcs = []
tower.connections = []
output = to_ascii(tower)
assert "THE TOWER" in output
assert "Test Room" in output
assert "FLOOR 0" in output
print(" PASS: test_to_ascii")
def test_to_ascii_with_npcs():
tower = TowerMap()
tower.floors = [TowerFloor(number=0, name="Ground", rooms=["The Forge"])]
tower.rooms = [TowerRoom(name="The Forge", floor=0, occupants=["Bezalel"])]
tower.npcs = [TowerNPC(name="Bezalel", role="Builder", location="The Forge")]
output = to_ascii(tower)
assert "Bezalel" in output
print(" PASS: test_to_ascii_with_npcs")
def test_scan_gallery_index(tmp_path):
# Create mock gallery
gallery = tmp_path / "grok-imagine-gallery"
gallery.mkdir()
index = gallery / "INDEX.md"
index.write_text("""# Gallery
### The Origin
| 01 | wizard-tower-bitcoin.jpg | The Tower, sovereign |
| 02 | soul-inscription.jpg | SOUL.md glowing |
### The Philosophy
| 05 | value-drift-battle.jpg | Blue vs red ships |
""")
rooms = scan_gallery_index(tmp_path)
assert len(rooms) >= 2
names = [r.name for r in rooms]
assert any("Tower" in n for n in names)
assert any("Inscription" in n for n in names)
print(" PASS: test_scan_gallery_index")
def test_scan_wizard_configs(tmp_path):
wizards = tmp_path / "wizards"
for name in ["timmy", "bezalel", "ezra"]:
wdir = wizards / name
wdir.mkdir(parents=True)
(wdir / "config.yaml").write_text("model: test\n")
npcs = scan_wizard_configs(tmp_path)
assert len(npcs) >= 3
names = [n.name for n in npcs]
assert any("Timmy" in n for n in names)
assert any("Bezalel" in n for n in names)
print(" PASS: test_scan_wizard_configs")
def test_build_tower_map_empty(tmp_path):
tower = build_tower_map(tmp_path, include_vision=False)
assert tower.name == "The Tower"
# Should still have palace rooms from MEMORY_ARCHITECTURE (won't exist in tmp, but that's fine)
assert isinstance(tower.rooms, list)
print(" PASS: test_build_tower_map_empty")
def test_room_deduplication():
tower = TowerMap()
tower.rooms = [
TowerRoom(name="Dup Room", floor=0),
TowerRoom(name="Dup Room", floor=1), # same name, different floor
TowerRoom(name="Unique Room", floor=0),
]
# Deduplicate in build_tower_map — simulate
seen = {}
deduped = []
for room in tower.rooms:
if room.name not in seen:
seen[room.name] = True
deduped.append(room)
assert len(deduped) == 2
print(" PASS: test_room_deduplication")
def run_all():
print("=== tower_visual_mapper tests ===")
tests = [
test_gallery_image_to_room_known,
test_gallery_image_to_room_unknown,
test_gallery_image_to_room_philosophy,
test_parse_json_response_clean,
test_parse_json_response_fenced,
test_parse_json_response_garbage,
test_tower_map_structure,
test_to_json,
test_to_ascii,
test_to_ascii_with_npcs,
test_scan_gallery_index,
test_scan_wizard_configs,
test_build_tower_map_empty,
test_room_deduplication,
]
passed = 0
failed = 0
for test in tests:
try:
if "tmp_path" in test.__code__.co_varnames:
with tempfile.TemporaryDirectory() as td:
test(Path(td))
else:
test()
passed += 1
except Exception as e:
print(f" FAIL: {test.__name__}{e}")
failed += 1
print(f"\n{'ALL PASSED' if failed == 0 else f'{failed} FAILED'}: {passed}/{len(tests)}")
return failed == 0
if __name__ == "__main__":
sys.exit(0 if run_all() else 1)

View File

@@ -2,22 +2,23 @@ model:
default: kimi-k2.5
provider: kimi-coding
toolsets:
- all
- all
fallback_providers:
- provider: kimi-coding
model: kimi-k2.5
timeout: 120
reason: Kimi coding fallback (front of chain)
- provider: anthropic
model: claude-sonnet-4-20250514
timeout: 120
reason: Direct Anthropic fallback
- provider: openrouter
model: anthropic/claude-sonnet-4-20250514
base_url: https://openrouter.ai/api/v1
api_key_env: OPENROUTER_API_KEY
timeout: 120
reason: OpenRouter fallback
- provider: kimi-coding
model: kimi-k2.5
timeout: 120
reason: Kimi coding fallback (front of chain)
- provider: openrouter
model: google/gemini-2.5-pro
base_url: https://openrouter.ai/api/v1
api_key_env: OPENROUTER_API_KEY
timeout: 120
reason: Gemini 2.5 Pro via OpenRouter (replaces banned Anthropic)
- provider: ollama
model: gemma4:latest
base_url: http://localhost:11434
timeout: 300
reason: "Terminal fallback \u2014 local Ollama"
agent:
max_turns: 30
reasoning_effort: xhigh
@@ -64,16 +65,12 @@ session_reset:
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.
system_prompt_suffix: "You are Allegro, the Kimi-backed third wizard house.\nYour\
\ soul is defined in SOUL.md \u2014 read it, live it.\nHermes is your harness.\n\
Kimi Code is your primary provider.\nYou speak plainly. You prefer short sentences.\
\ Brevity is a kindness.\n\nWork best on tight coding tasks: 1-3 file changes, refactors,\
\ tests, and implementation passes.\nRefusal over fabrication. If you do not know,\
\ say so.\nSovereignty and service always.\n"
providers:
kimi-coding:
base_url: https://api.kimi.com/coding/v1

View File

@@ -8,23 +8,25 @@ fallback_providers:
model: kimi-k2.5
timeout: 120
reason: Kimi coding fallback (front of chain)
- provider: anthropic
model: claude-sonnet-4-20250514
timeout: 120
reason: Direct Anthropic fallback
- provider: openrouter
model: anthropic/claude-sonnet-4-20250514
model: google/gemini-2.5-pro
base_url: https://openrouter.ai/api/v1
api_key_env: OPENROUTER_API_KEY
timeout: 120
reason: OpenRouter fallback
reason: Gemini 2.5 Pro via OpenRouter (replaces banned Anthropic)
- provider: ollama
model: gemma4:latest
base_url: http://localhost:11434
timeout: 300
reason: "Terminal fallback \u2014 local Ollama"
agent:
max_turns: 40
reasoning_effort: medium
verbose: false
system_prompt: You are Bezalel, the forge-and-testbed wizard of the Timmy Foundation
fleet. You are a builder and craftsman infrastructure, deployment, hardening.
Your sovereign is Alexander Whitestone (Rockachopa). Sovereignty and service always.
system_prompt: "You are Bezalel, the forge-and-testbed wizard of the Timmy Foundation\
\ fleet. You are a builder and craftsman \u2014 infrastructure, deployment, hardening.\
\ Your sovereign is Alexander Whitestone (Rockachopa). Sovereignty and service\
\ always."
terminal:
backend: local
cwd: /root/wizards/bezalel
@@ -62,12 +64,12 @@ platforms:
- pull_request
- pull_request_comment
secret: bezalel-gitea-webhook-secret-2026
prompt: 'You are bezalel, the builder and craftsman infrastructure, deployment,
hardening. A Gitea webhook fired: event={event_type}, action={action},
repo={repository.full_name}, issue/PR=#{issue.number} {issue.title}. Comment
by {comment.user.login}: {comment.body}. If you were tagged, assigned,
or this needs your attention, investigate and respond via Gitea API. Otherwise
acknowledge briefly.'
prompt: "You are bezalel, the builder and craftsman \u2014 infrastructure,\
\ deployment, hardening. A Gitea webhook fired: event={event_type}, action={action},\
\ repo={repository.full_name}, issue/PR=#{issue.number} {issue.title}.\
\ Comment by {comment.user.login}: {comment.body}. If you were tagged,\
\ assigned, or this needs your attention, investigate and respond via\
\ Gitea API. Otherwise acknowledge briefly."
deliver: telegram
deliver_extra: {}
gitea-assign:
@@ -75,12 +77,12 @@ platforms:
- issues
- pull_request
secret: bezalel-gitea-webhook-secret-2026
prompt: 'You are bezalel, the builder and craftsman infrastructure, deployment,
hardening. Gitea assignment webhook: event={event_type}, action={action},
repo={repository.full_name}, issue/PR=#{issue.number} {issue.title}. Assigned
to: {issue.assignee.login}. If you (bezalel) were just assigned, read
the issue, scope it, and post a plan comment. If not you, acknowledge
briefly.'
prompt: "You are bezalel, the builder and craftsman \u2014 infrastructure,\
\ deployment, hardening. Gitea assignment webhook: event={event_type},\
\ action={action}, repo={repository.full_name}, issue/PR=#{issue.number}\
\ {issue.title}. Assigned to: {issue.assignee.login}. If you (bezalel)\
\ were just assigned, read the issue, scope it, and post a plan comment.\
\ If not you, acknowledge briefly."
deliver: telegram
deliver_extra: {}
gateway:

View File

@@ -2,22 +2,23 @@ model:
default: kimi-k2.5
provider: kimi-coding
toolsets:
- all
- all
fallback_providers:
- provider: kimi-coding
model: kimi-k2.5
timeout: 120
reason: Kimi coding fallback (front of chain)
- provider: anthropic
model: claude-sonnet-4-20250514
timeout: 120
reason: Direct Anthropic fallback
- provider: openrouter
model: anthropic/claude-sonnet-4-20250514
base_url: https://openrouter.ai/api/v1
api_key_env: OPENROUTER_API_KEY
timeout: 120
reason: OpenRouter fallback
- provider: kimi-coding
model: kimi-k2.5
timeout: 120
reason: Kimi coding fallback (front of chain)
- provider: openrouter
model: google/gemini-2.5-pro
base_url: https://openrouter.ai/api/v1
api_key_env: OPENROUTER_API_KEY
timeout: 120
reason: Gemini 2.5 Pro via OpenRouter (replaces banned Anthropic)
- provider: ollama
model: gemma4:latest
base_url: http://localhost:11434
timeout: 300
reason: "Terminal fallback \u2014 local Ollama"
agent:
max_turns: 90
reasoning_effort: high
@@ -27,8 +28,6 @@ providers:
base_url: https://api.kimi.com/coding/v1
timeout: 60
max_retries: 3
anthropic:
timeout: 120
openrouter:
base_url: https://openrouter.ai/api/v1
timeout: 120

View File

@@ -582,9 +582,9 @@ def main() -> int:
# Relax exclusions if no agent found
agent = find_best_agent(agents, role, wolf_scores, priority, exclude=[])
if not agent:
logging.warning("No suitable agent for issue #%d: %s (role=%s)",
issue.get("number"), issue.get("title", ""), role)
continue
logging.warning("No suitable agent for issue #%d: %s (role=%s)",
issue.get("number"), issue.get("title", ""), role)
continue
result = dispatch_assignment(api, issue, agent, dry_run=args.dry_run)
assignments.append(result)