Compare commits

..

3 Commits

Author SHA1 Message Date
6ad6469c40 feat: implement architecture linter for sovereignty enforcement 2026-04-10 23:49:24 +00:00
perplexity
3af63cf172 enforce: Anthropic ban — linter, pre-commit, tests, and policy doc
Some checks failed
PR Checklist / pr-checklist (pull_request) Failing after 1m20s
Anthropic is not just removed — it is banned. This commit adds
enforcement at every gate to prevent re-introduction.

1. architecture_linter.py — 9 BANNED rules for Anthropic patterns
   (provider, model slugs, API endpoints, keys, model names).
   Scans all yaml/py/sh/json/md. Skips training data and historical docs.

2. pre-commit.py — scan_banned_providers() runs on every staged file.
   Blocks any commit that introduces Anthropic references.
   Exempt: training/, evaluations/, changelogs, historical cost data.

3. test_sovereignty_enforcement.py — TestAnthropicBan class with 4 tests:
   - No Anthropic in wizard configs
   - No Anthropic in playbooks
   - No Anthropic in fallback chain
   - No Anthropic API key in bootstrap

4. BANNED_PROVIDERS.md — Hard policy document. Golden state config.
   Replacement table. Exception list. Not advisory — mandatory.
2026-04-09 19:27:00 +00:00
perplexity
6d713aeeb9 purge: remove Anthropic from all wizard configs, playbooks, and fleet scripts
Some checks failed
PR Checklist / pr-checklist (pull_request) Failing after 1m18s
Golden state: Kimi K2.5 primary → Gemini via OpenRouter → local Ollama.
Anthropic is gone from every active config, fallback chain, and loop script.

Wizard configs (3):
- allegro, bezalel, ezra: removed anthropic from fallback_providers,
  replaced with gemini + ollama. Removed anthropic provider section.

Playbooks (7):
- All playbooks now use kimi-k2.5 as preferred, google/gemini-2.5-pro
  as fallback. No claude model references remain.

Fleet scripts (8):
- claude-loop.sh: deprecated (exit 0, original preserved as reference)
- claudemax-watchdog.sh: deprecated (exit 0)
- agent-loop.sh: removed claude dispatch case
- start-loops.sh: removed claude-locks, claude-loop from proc list
- timmy-orchestrator.sh: removed claude worker monitoring
- fleet-status.sh: zeroed claude loop counter
- model-health-check.sh: replaced check_anthropic_model with check_kimi_model
- ops-gitea.sh, ops-helpers.sh, ops-panel.sh: removed claude from agent lists

Infrastructure (5):
- wizard_bootstrap.py: removed anthropic pip package and API key checks
- WIZARD_ENVIRONMENT_CONTRACT.md: replaced ANTHROPIC keys with KIMI
- DEPLOY.md: replaced ANTHROPIC_API_KEY with KIMI_API_KEY
- fallback-portfolios.yaml: replaced anthropic provider with kimi-coding
- fleet-vocabulary.md: updated Ezra and Claude entries to Kimi K2.5

Docs (2):
- sonnet-workforce.md: deprecated with notice
- GoldenRockachopa-checkin.md: updated model references

Preserved (not touched):
- training/ data (changing would corrupt training set)
- evaluations/ (historical benchmarks)
- RELEASE_*.md (changelogs)
- metrics_helpers.py (historical cost calculation)
- hermes-sovereign/githooks/pre-commit.py (secret detection - still useful)
- security/secret-scan.yml (key detection - still useful)
- architecture_linter.py (warns about anthropic usage - desired behavior)
- test_sovereignty_enforcement.py (tests anthropic is blocked - correct)
- son-of-timmy.md philosophical references (Claude as one of many backends)

Refs: Sovereignty directive, zero-cloud vision
2026-04-09 19:21:48 +00:00
132 changed files with 1152 additions and 8052 deletions

View File

@@ -1,54 +0,0 @@
## Summary
<!-- What changed and why. One paragraph max. -->
## Governing Issue
<!-- REQUIRED. Every PR must reference at least one issue. Max 3 issues per PR. -->
<!-- Closes #ISSUENUM -->
<!-- Refs #ISSUENUM -->
## Acceptance Criteria
<!-- List the specific outcomes this PR delivers. Check each only when proven. -->
<!-- Copy these from the governing issue if it has them. -->
- [ ] Criterion 1
- [ ] Criterion 2
## Proof
<!-- No proof = no merge. See CONTRIBUTING.md for the full standard. -->
### Commands / logs / world-state proof
<!-- Paste the exact commands, output, log paths, or world-state artifacts that prove each acceptance criterion was met. -->
```
$ <command you ran>
<relevant output>
```
### Visual proof (if applicable)
<!-- For skin updates, UI changes, dashboard changes: attach screenshot to the PR discussion. -->
<!-- Name what the screenshot proves. Do not commit binary media unless explicitly required. -->
## Risk and Rollback
<!-- What could go wrong? How do we undo it? -->
- **Risk level:** low / medium / high
- **What breaks if this is wrong:**
- **How to rollback:**
## Checklist
<!-- Complete every item before requesting review. -->
- [ ] PR body references at least one issue number (`Closes #N` or `Refs #N`)
- [ ] Changed files are syntactically valid (`python -c "import ast; ast.parse(open(f).read())"`, `node --check`, `bash -n`)
- [ ] Proof meets CONTRIBUTING.md standard (exact commands, output, or artifacts — not "looks right")
- [ ] Branch is up-to-date with base
- [ ] No more than 3 unrelated issues bundled in this PR
- [ ] Shell scripts are executable (`chmod +x`)

View File

@@ -1,42 +0,0 @@
# architecture-lint.yml — CI gate for the Architecture Linter v2
# Refs: #437 — repo-aware, test-backed, CI-enforced.
#
# Runs on every PR to main. Validates Python syntax, then runs
# linter tests and finally lints the repo itself.
name: Architecture Lint
on:
pull_request:
branches: [main, master]
push:
branches: [main]
jobs:
linter-tests:
name: Linter Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install test deps
run: pip install pytest
- name: Compile-check linter
run: python3 -m py_compile scripts/architecture_linter_v2.py
- name: Run linter tests
run: python3 -m pytest tests/test_linter.py -v
lint-repo:
name: Lint Repository
runs-on: ubuntu-latest
needs: linter-tests
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Run architecture linter
run: python3 scripts/architecture_linter_v2.py .

View File

@@ -1,32 +0,0 @@
name: Smoke Test
on:
pull_request:
push:
branches: [main]
jobs:
smoke:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Parse check
run: |
find . -name '*.yml' -o -name '*.yaml' | grep -v .gitea | xargs -r python3 -c "import sys,yaml; [yaml.safe_load(open(f)) for f in sys.argv[1:]]"
find . -name '*.json' | xargs -r python3 -m json.tool > /dev/null
find . -name '*.py' | xargs -r python3 -m py_compile
find . -name '*.sh' | xargs -r bash -n
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' \
| 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 flake8
pip install py_compile flake8
- name: Compile-check all Python files
run: |
find . -name '*.py' -print0 | while IFS= read -r -d '' f; do
@@ -59,21 +59,7 @@ jobs:
- name: Flake8 critical errors only
run: |
flake8 --select=E9,F63,F7,F82 --show-source --statistics \
scripts/ bin/ tests/
python-test:
name: Python Test Suite
runs-on: ubuntu-latest
needs: python-check
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install test dependencies
run: pip install pytest pyyaml
- name: Run tests
run: python3 -m pytest tests/ -v --tb=short
scripts/ allegro/ cron/ || true
shell-lint:
name: Shell Script Lint
@@ -84,7 +70,7 @@ jobs:
run: sudo apt-get install -y shellcheck
- name: Lint shell scripts
run: |
find . -name '*.sh' -not -path './.git/*' -print0 | xargs -0 -r shellcheck --severity=error
find . -name '*.sh' -print0 | xargs -0 -r shellcheck --severity=error || true
cron-validate:
name: Cron Syntax Check
@@ -126,10 +112,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install PyYAML
run: pip install pyyaml
- name: Validate playbook structure
run: python3 scripts/validate_playbook_schema.py
run: |
python3 -c "
import yaml, sys, glob
required_keys = {'name', 'description'}
for f in glob.glob('playbooks/*.yaml'):
with open(f) as fh:
try:
data = yaml.safe_load(fh)
if not isinstance(data, dict):
print(f'ERROR: {f} is not a YAML mapping')
sys.exit(1)
missing = required_keys - set(data.keys())
if missing:
print(f'WARNING: {f} missing keys: {missing}')
print(f'OK: {f}')
except yaml.YAMLError as e:
print(f'ERROR: {f}: {e}')
sys.exit(1)
"

27
.gitignore vendored
View File

@@ -10,30 +10,3 @@ __pycache__/
# Generated audit reports
reports/
# Secrets and credentials
.bash_history
.git-credentials
.gitea_token
.ssh/id_*
.ssh/known_hosts
.viminfo
.wget-hsts
.profile
.bashrc
.bash_logout
.python_history
.lesshst
.selected_editor
.sudo_as_admin_successful
.config/telegram/
.hermes/.env
.hermes/auth.json
*.pem
*.key
.env
.env.*
!.env.example
# Prevent test artifacts
/test-*.txt

63
BANNED_PROVIDERS.md Normal file
View File

@@ -0,0 +1,63 @@
# Banned Providers
This document is a hard policy. It is not advisory. It is not aspirational.
Any agent, wizard, or automated process that violates this policy is broken
and must be fixed immediately.
## Permanently Banned
### Anthropic (Claude)
**Status:** BANNED — April 2026
**Scope:** All configs, fallback chains, playbooks, wizard bootstraps, and fleet scripts.
**Enforcement:** Pre-commit hook, architecture linter, sovereignty enforcement tests.
No Anthropic model (Claude Opus, Sonnet, Haiku, or any variant) may appear as:
- A primary provider
- A fallback provider
- An OpenRouter model slug (e.g. `anthropic/claude-*`)
- An API endpoint (api.anthropic.com)
- A required dependency (`anthropic` pip package)
- An environment variable (`ANTHROPIC_API_KEY`, `ANTHROPIC_TOKEN`)
### What to use instead
| Was | Now |
|-----|-----|
| claude-opus-4-6 | kimi-k2.5 |
| claude-sonnet-4-20250514 | kimi-k2.5 |
| claude-haiku | google/gemini-2.5-pro |
| anthropic (provider) | kimi-coding |
| anthropic/claude-* (OpenRouter) | google/gemini-2.5-pro |
| ANTHROPIC_API_KEY | KIMI_API_KEY |
### Exceptions
The following files may reference Anthropic for **historical or defensive** purposes:
- `training/` — Training data must not be altered
- `evaluations/` — Historical benchmark results
- `RELEASE_*.md` — Changelogs
- `metrics_helpers.py` — Historical cost calculation
- `pre-commit.py` — Detects leaked Anthropic keys (defensive)
- `secret-scan.yml` — Detects leaked Anthropic keys (defensive)
- `architecture_linter.py` — Warns/blocks Anthropic usage (enforcement)
- `test_sovereignty_enforcement.py` — Tests that Anthropic is blocked (enforcement)
### Golden State
```yaml
fallback_providers:
- provider: kimi-coding
model: kimi-k2.5
reason: Primary
- provider: openrouter
model: google/gemini-2.5-pro
reason: Cloud fallback
- provider: ollama
model: gemma4:latest
base_url: http://localhost:11434/v1
reason: Terminal fallback — never phones home
```
*Sovereignty and service always.*

View File

@@ -51,11 +51,11 @@ Alexander is pleased with the state. This tag marks a high-water mark.
| OAI-Wolf-3 | 8683 | hermes gateway | ACTIVE |
- Disk: 12G/926G (4%) — pristine
- Primary model: claude-opus-4-6 via Anthropic
- Primary model: kimi-k2.5 via Kimi
- Fallback chain: codex → kimi-k2.5 → gemini-2.5-flash → llama-3.3-70b → grok-3-mini-fast → kimi → grok → kimi → gpt-4.1-mini
- Ollama models: gemma4:latest (9.6GB), hermes4:14b (9.0GB)
- Worktrees: 239 (9.8GB) — prune candidates exist
- Running loops: 3 claude-loops, 3 gemini-loops, orchestrator, status watcher
- Running loops: 3 gemini-loops, orchestrator, status watcher
- LaunchD: hermes gateway running, fenrir stopped, kimi-heartbeat idle
- MCP: morrowind server active

View File

@@ -1,47 +0,0 @@
# 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

@@ -1,47 +0,0 @@
# =============================================================================
# BANNED PROVIDERS — The Timmy Foundation
# =============================================================================
# "Anthropic is not only fired, but banned. I don't want these errors
# cropping up." — Alexander, 2026-04-09
#
# This is a HARD BAN. Not deprecated. Not fallback. BANNED.
# Enforcement: pre-commit hook, linter, Ansible validation, CI tests.
# =============================================================================
banned_providers:
- name: anthropic
reason: "Permanently banned. SDK access gated despite active quota. Fleet was bricked because golden state pointed to Anthropic Sonnet."
banned_date: "2026-04-09"
enforcement: strict # Ansible playbook FAILS if detected
models:
- "claude-sonnet-*"
- "claude-opus-*"
- "claude-haiku-*"
- "claude-*"
endpoints:
- "api.anthropic.com"
- "anthropic/*" # OpenRouter pattern
api_keys:
- "ANTHROPIC_API_KEY"
- "CLAUDE_API_KEY"
# Golden state alternative:
approved_providers:
- name: kimi-coding
model: kimi-k2.5
role: primary
- name: openrouter
model: google/gemini-2.5-pro
role: fallback
- name: ollama
model: "gemma4:latest"
role: terminal_fallback
# Future evaluation:
evaluation_candidates:
- name: mimo-v2-pro
status: pending
notes: "Free via Nous Portal for ~2 weeks from 2026-04-07. Add after fallback chain is fixed."
- name: hermes-4
status: available
notes: "Free on Nous Portal. 36B and 70B variants. Home team model."

View File

@@ -1,95 +0,0 @@
# Ansible IaC — The Timmy Foundation Fleet
> One canonical Ansible playbook defines: deadman switch, cron schedule,
> golden state rollback, agent startup sequence.
> — KT Final Session 2026-04-08, Priority TWO
## Purpose
This directory contains the **single source of truth** for fleet infrastructure.
No more ad-hoc recovery implementations. No more overlapping deadman switches.
No more agents mutating their own configs into oblivion.
**Everything** goes through Ansible. If it's not in a playbook, it doesn't exist.
## Architecture
```
┌─────────────────────────────────────────────────┐
│ Gitea (Source of Truth) │
│ timmy-config/ansible/ │
│ ├── inventory/hosts.yml (fleet machines) │
│ ├── playbooks/site.yml (master playbook) │
│ ├── roles/ (reusable roles) │
│ └── group_vars/wizards.yml (golden state) │
└──────────────────┬──────────────────────────────┘
│ PR merge triggers webhook
┌─────────────────────────────────────────────────┐
│ Gitea Webhook Handler │
│ scripts/deploy_on_webhook.sh │
│ → ansible-pull on each target machine │
└──────────────────┬──────────────────────────────┘
│ ansible-pull
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ Timmy │ │ Allegro │ │ Bezalel │ │ Ezra │
│ (Mac) │ │ (VPS) │ │ (VPS) │ │ (VPS) │
│ │ │ │ │ │ │ │
│ deadman │ │ deadman │ │ deadman │ │ deadman │
│ cron │ │ cron │ │ cron │ │ cron │
│ golden │ │ golden │ │ golden │ │ golden │
│ req_log │ │ req_log │ │ req_log │ │ req_log │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
```
## Quick Start
```bash
# Deploy everything to all machines
ansible-playbook -i inventory/hosts.yml playbooks/site.yml
# Deploy only golden state config
ansible-playbook -i inventory/hosts.yml playbooks/golden_state.yml
# Deploy only to a specific wizard
ansible-playbook -i inventory/hosts.yml playbooks/site.yml --limit bezalel
# Dry run (check mode)
ansible-playbook -i inventory/hosts.yml playbooks/site.yml --check --diff
```
## Golden State Provider Chain
All wizard configs converge on this provider chain. **Anthropic is BANNED.**
| Priority | Provider | Model | Endpoint |
| -------- | -------------------- | ---------------- | --------------------------------- |
| 1 | Kimi | kimi-k2.5 | https://api.kimi.com/coding/v1 |
| 2 | Gemini (OpenRouter) | gemini-2.5-pro | https://openrouter.ai/api/v1 |
| 3 | Ollama (local) | gemma4:latest | http://localhost:11434/v1 |
## Roles
| Role | Purpose |
| ---------------- | ------------------------------------------------------------ |
| `wizard_base` | Common wizard setup: directories, thin config, git pull |
| `deadman_switch` | Health check → snapshot good config → rollback on death |
| `golden_state` | Deploy and enforce golden state provider chain |
| `request_log` | SQLite telemetry table for every inference call |
| `cron_manager` | Source-controlled cron jobs — no manual crontab edits |
## Rules
1. **No manual changes.** If it's not in a playbook, it will be overwritten.
2. **No Anthropic.** Banned. Enforcement is automated. See `BANNED_PROVIDERS.yml`.
3. **Idempotent.** Every playbook can run 100 times with the same result.
4. **PR required.** Config changes go through Gitea PR review, then deploy.
5. **One identity per machine.** No duplicate agents. Fleet audit enforces this.
## Related Issues
- timmy-config #442: [P2] Ansible IaC Canonical Playbook
- timmy-config #444: Wire Deadman Switch ACTION
- timmy-config #443: Thin Config Pattern
- timmy-config #446: request_log Telemetry Table

View File

@@ -1,21 +0,0 @@
[defaults]
inventory = inventory/hosts.yml
roles_path = roles
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
forks = 10
timeout = 30
# Logging
log_path = /var/log/ansible/timmy-fleet.log
[privilege_escalation]
become = True
become_method = sudo
become_user = root
become_ask_pass = False
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no

View File

@@ -1,74 +0,0 @@
# =============================================================================
# Wizard Group Variables — Golden State Configuration
# =============================================================================
# These variables are applied to ALL wizards in the fleet.
# This IS the golden state. If a wizard deviates, Ansible corrects it.
# =============================================================================
# --- Deadman Switch ---
deadman_enabled: true
deadman_check_interval: 300 # 5 minutes between health checks
deadman_snapshot_dir: "~/.local/timmy/snapshots"
deadman_max_snapshots: 10 # Rolling window of good configs
deadman_restart_cooldown: 60 # Seconds to wait before restart after failure
deadman_max_restart_attempts: 3
deadman_escalation_channel: telegram # Alert Alexander after max attempts
# --- Thin Config ---
thin_config_path: "~/.timmy/thin_config.yml"
thin_config_mode: "0444" # Read-only — agents CANNOT modify
upstream_repo: "https://forge.alexanderwhitestone.com/Timmy_Foundation/timmy-config.git"
upstream_branch: main
config_pull_on_wake: true
config_validation_enabled: true
# --- Agent Settings ---
agent_max_turns: 30
agent_reasoning_effort: high
agent_verbose: false
agent_approval_mode: auto
# --- Hermes Harness ---
hermes_config_dir: "{{ hermes_home }}"
hermes_bin_dir: "{{ hermes_home }}/bin"
hermes_skins_dir: "{{ hermes_home }}/skins"
hermes_playbooks_dir: "{{ hermes_home }}/playbooks"
hermes_memories_dir: "{{ hermes_home }}/memories"
# --- Request Log (Telemetry) ---
request_log_enabled: true
request_log_path: "~/.local/timmy/request_log.db"
request_log_rotation_days: 30 # Archive logs older than 30 days
request_log_sync_to_gitea: false # Future: push telemetry summaries to Gitea
# --- Cron Schedule ---
# All cron jobs are managed here. No manual crontab edits.
cron_jobs:
- name: "Deadman health check"
job: "cd {{ wizard_home }}/workspace/timmy-config && python3 fleet/health_check.py"
minute: "*/5"
hour: "*"
enabled: "{{ deadman_enabled }}"
- name: "Muda audit"
job: "cd {{ wizard_home }}/workspace/timmy-config && bash fleet/muda-audit.sh >> /tmp/muda-audit.log 2>&1"
minute: "0"
hour: "21"
weekday: "0"
enabled: true
- name: "Config pull from upstream"
job: "cd {{ wizard_home }}/workspace/timmy-config && git pull --ff-only origin main"
minute: "*/15"
hour: "*"
enabled: "{{ config_pull_on_wake }}"
- name: "Request log rotation"
job: "python3 -c \"import sqlite3,datetime; db=sqlite3.connect('{{ request_log_path }}'); db.execute('DELETE FROM request_log WHERE timestamp < datetime(\\\"now\\\", \\\"-{{ request_log_rotation_days }} days\\\")'); db.commit()\""
minute: "0"
hour: "3"
enabled: "{{ request_log_enabled }}"
# --- Provider Enforcement ---
# These are validated on every Ansible run. Any Anthropic reference = failure.
provider_ban_enforcement: strict # strict = fail playbook, warn = log only

View File

@@ -1,119 +0,0 @@
# =============================================================================
# Fleet Inventory — The Timmy Foundation
# =============================================================================
# Source of truth for all machines in the fleet.
# Update this file when machines are added/removed.
# All changes go through PR review.
# =============================================================================
all:
children:
wizards:
hosts:
timmy:
ansible_host: localhost
ansible_connection: local
wizard_name: Timmy
wizard_role: "Primary wizard — soul of the fleet"
wizard_provider_primary: kimi-coding
wizard_model_primary: kimi-k2.5
hermes_port: 8081
api_port: 8645
wizard_home: "{{ ansible_env.HOME }}/wizards/timmy"
hermes_home: "{{ ansible_env.HOME }}/.hermes"
machine_type: mac
# Timmy runs on Alexander's M3 Max
ollama_available: true
allegro:
ansible_host: 167.99.126.228
ansible_user: root
wizard_name: Allegro
wizard_role: "Kimi-backed third wizard house — tight coding tasks"
wizard_provider_primary: kimi-coding
wizard_model_primary: kimi-k2.5
hermes_port: 8081
api_port: 8645
wizard_home: /root/wizards/allegro
hermes_home: /root/.hermes
machine_type: vps
ollama_available: false
bezalel:
ansible_host: 159.203.146.185
ansible_user: root
wizard_name: Bezalel
wizard_role: "Forge-and-testbed wizard — infrastructure, deployment, hardening"
wizard_provider_primary: kimi-coding
wizard_model_primary: kimi-k2.5
hermes_port: 8081
api_port: 8656
wizard_home: /root/wizards/bezalel
hermes_home: /root/.hermes
machine_type: vps
ollama_available: false
# NOTE: The awake Bezalel may be the duplicate.
# Fleet audit (the-nexus #1144) will resolve identity.
ezra:
ansible_host: 143.198.27.163
ansible_user: root
wizard_name: Ezra
wizard_role: "Infrastructure wizard — Gitea, nginx, hosting"
wizard_provider_primary: kimi-coding
wizard_model_primary: kimi-k2.5
hermes_port: 8081
api_port: 8645
wizard_home: /root/wizards/ezra
hermes_home: /root/.hermes
machine_type: vps
ollama_available: false
# NOTE: Currently DOWN — Telegram key revoked, awaiting propagation.
# Infrastructure hosts (not wizards, but managed by Ansible)
infrastructure:
hosts:
forge:
ansible_host: 143.198.27.163
ansible_user: root
# Gitea runs on the same box as Ezra
gitea_url: https://forge.alexanderwhitestone.com
gitea_org: Timmy_Foundation
vars:
# Global variables applied to all hosts
gitea_repo_url: "https://forge.alexanderwhitestone.com/Timmy_Foundation/timmy-config.git"
gitea_branch: main
config_base_path: "{{ gitea_repo_url }}"
timmy_log_dir: "~/.local/timmy/fleet-health"
request_log_db: "~/.local/timmy/request_log.db"
# Golden state provider chain — Anthropic is BANNED
golden_state_providers:
- name: kimi-coding
model: kimi-k2.5
base_url: "https://api.kimi.com/coding/v1"
timeout: 120
reason: "Primary — Kimi K2.5 (best value, least friction)"
- name: openrouter
model: google/gemini-2.5-pro
base_url: "https://openrouter.ai/api/v1"
api_key_env: OPENROUTER_API_KEY
timeout: 120
reason: "Fallback — Gemini 2.5 Pro via OpenRouter"
- name: ollama
model: "gemma4:latest"
base_url: "http://localhost:11434/v1"
timeout: 180
reason: "Terminal fallback — local Ollama (sovereign, no API needed)"
# Banned providers — hard enforcement
banned_providers:
- anthropic
- claude
banned_models_patterns:
- "claude-*"
- "anthropic/*"
- "*sonnet*"
- "*opus*"
- "*haiku*"

View File

@@ -1,98 +0,0 @@
---
# =============================================================================
# agent_startup.yml — Resurrect Wizards from Checked-in Configs
# =============================================================================
# Brings wizards back online using golden state configs.
# Order: pull config → validate → start agent → verify with request_log
# =============================================================================
- name: "Agent Startup Sequence"
hosts: wizards
become: true
serial: 1 # One wizard at a time to avoid cascading issues
tasks:
- name: "Pull latest config from upstream"
git:
repo: "{{ upstream_repo }}"
dest: "{{ wizard_home }}/workspace/timmy-config"
version: "{{ upstream_branch }}"
force: true
tags: [pull]
- name: "Deploy golden state config"
include_role:
name: golden_state
tags: [config]
- name: "Validate config — no banned providers"
shell: |
python3 -c "
import yaml, sys
with open('{{ wizard_home }}/config.yaml') as f:
cfg = yaml.safe_load(f)
banned = {{ banned_providers }}
for p in cfg.get('fallback_providers', []):
if p.get('provider', '') in banned:
print(f'BANNED: {p[\"provider\"]}', file=sys.stderr)
sys.exit(1)
model = cfg.get('model', {}).get('provider', '')
if model in banned:
print(f'BANNED default provider: {model}', file=sys.stderr)
sys.exit(1)
print('Config validated — no banned providers.')
"
register: config_valid
tags: [validate]
- name: "Ensure hermes-agent service is running"
systemd:
name: "hermes-{{ wizard_name | lower }}"
state: started
enabled: true
when: machine_type == 'vps'
tags: [start]
ignore_errors: true # Service may not exist yet on all machines
- name: "Start hermes agent (Mac — launchctl)"
shell: |
launchctl kickstart -k "ai.hermes.{{ wizard_name | lower }}" 2>/dev/null || \
cd {{ wizard_home }} && hermes agent start --daemon 2>&1 | tail -5
when: machine_type == 'mac'
tags: [start]
ignore_errors: true
- name: "Wait for agent to come online"
wait_for:
host: 127.0.0.1
port: "{{ api_port }}"
timeout: 60
state: started
tags: [verify]
ignore_errors: true
- name: "Verify agent is alive — check request_log for activity"
shell: |
sleep 10
python3 -c "
import sqlite3, sys
db = sqlite3.connect('{{ request_log_path }}')
cursor = db.execute('''
SELECT COUNT(*) FROM request_log
WHERE agent_name = '{{ wizard_name }}'
AND timestamp > datetime('now', '-5 minutes')
''')
count = cursor.fetchone()[0]
if count > 0:
print(f'{{ wizard_name }} is alive — {count} recent inference calls logged.')
else:
print(f'WARNING: {{ wizard_name }} started but no telemetry yet.')
"
register: agent_status
tags: [verify]
ignore_errors: true
- name: "Report startup status"
debug:
msg: "{{ wizard_name }}: {{ agent_status.stdout | default('startup attempted') }}"
tags: [always]

View File

@@ -1,15 +0,0 @@
---
# =============================================================================
# cron_schedule.yml — Source-Controlled Cron Jobs
# =============================================================================
# All cron jobs are defined in group_vars/wizards.yml.
# This playbook deploys them. No manual crontab edits allowed.
# =============================================================================
- name: "Deploy Cron Schedule"
hosts: wizards
become: true
roles:
- role: cron_manager
tags: [cron, schedule]

View File

@@ -1,17 +0,0 @@
---
# =============================================================================
# deadman_switch.yml — Deploy Deadman Switch to All Wizards
# =============================================================================
# The deadman watch already fires and detects dead agents.
# This playbook wires the ACTION:
# - On healthy check: snapshot current config as "last known good"
# - On failed check: rollback config to snapshot, restart agent
# =============================================================================
- name: "Deploy Deadman Switch ACTION"
hosts: wizards
become: true
roles:
- role: deadman_switch
tags: [deadman, recovery]

View File

@@ -1,30 +0,0 @@
---
# =============================================================================
# golden_state.yml — Deploy Golden State Config to All Wizards
# =============================================================================
# Enforces the golden state provider chain across the fleet.
# Removes any Anthropic references. Deploys the approved provider chain.
# =============================================================================
- name: "Deploy Golden State Configuration"
hosts: wizards
become: true
roles:
- role: golden_state
tags: [golden, config]
post_tasks:
- name: "Verify golden state — no banned providers"
shell: |
grep -rci 'anthropic\|claude-sonnet\|claude-opus\|claude-haiku' \
{{ hermes_home }}/config.yaml \
{{ wizard_home }}/config.yaml 2>/dev/null || echo "0"
register: banned_count
changed_when: false
- name: "Report golden state status"
debug:
msg: >
{{ wizard_name }} golden state: {{ golden_state_providers | map(attribute='name') | list | join(' → ') }}.
Banned provider references: {{ banned_count.stdout | trim }}.

View File

@@ -1,15 +0,0 @@
---
# =============================================================================
# request_log.yml — Deploy Telemetry Table
# =============================================================================
# Creates the request_log SQLite table on all machines.
# Every inference call writes a row. No exceptions. No summarizing.
# =============================================================================
- name: "Deploy Request Log Telemetry"
hosts: wizards
become: true
roles:
- role: request_log
tags: [telemetry, logging]

View File

@@ -1,72 +0,0 @@
---
# =============================================================================
# site.yml — Master Playbook for the Timmy Foundation Fleet
# =============================================================================
# This is the ONE playbook that defines the entire fleet state.
# Run this and every machine converges to golden state.
#
# Usage:
# ansible-playbook -i inventory/hosts.yml playbooks/site.yml
# ansible-playbook -i inventory/hosts.yml playbooks/site.yml --limit bezalel
# ansible-playbook -i inventory/hosts.yml playbooks/site.yml --check --diff
# =============================================================================
- name: "Timmy Foundation Fleet — Full Convergence"
hosts: wizards
become: true
pre_tasks:
- name: "Validate no banned providers in golden state"
assert:
that:
- "item.name not in banned_providers"
fail_msg: "BANNED PROVIDER DETECTED: {{ item.name }} — Anthropic is permanently banned."
quiet: true
loop: "{{ golden_state_providers }}"
tags: [always]
- name: "Display target wizard"
debug:
msg: "Deploying to {{ wizard_name }} ({{ wizard_role }}) on {{ ansible_host }}"
tags: [always]
roles:
- role: wizard_base
tags: [base, setup]
- role: golden_state
tags: [golden, config]
- role: deadman_switch
tags: [deadman, recovery]
- role: request_log
tags: [telemetry, logging]
- role: cron_manager
tags: [cron, schedule]
post_tasks:
- name: "Final validation — scan for banned providers"
shell: |
grep -ri 'anthropic\|claude-sonnet\|claude-opus\|claude-haiku' \
{{ hermes_home }}/config.yaml \
{{ wizard_home }}/config.yaml \
{{ thin_config_path }} 2>/dev/null || true
register: banned_scan
changed_when: false
tags: [validation]
- name: "FAIL if banned providers found in deployed config"
fail:
msg: |
BANNED PROVIDER DETECTED IN DEPLOYED CONFIG:
{{ banned_scan.stdout }}
Anthropic is permanently banned. Fix the config and re-deploy.
when: banned_scan.stdout | length > 0
tags: [validation]
- name: "Deployment complete"
debug:
msg: "{{ wizard_name }} converged to golden state. Provider chain: {{ golden_state_providers | map(attribute='name') | list | join(' → ') }}"
tags: [always]

View File

@@ -1,55 +0,0 @@
---
# =============================================================================
# cron_manager/tasks — Source-Controlled Cron Jobs
# =============================================================================
# All cron jobs are defined in group_vars/wizards.yml.
# No manual crontab edits. This is the only way to manage cron.
# =============================================================================
- name: "Deploy managed cron jobs"
cron:
name: "{{ item.name }}"
job: "{{ item.job }}"
minute: "{{ item.minute | default('*') }}"
hour: "{{ item.hour | default('*') }}"
day: "{{ item.day | default('*') }}"
month: "{{ item.month | default('*') }}"
weekday: "{{ item.weekday | default('*') }}"
state: "{{ 'present' if item.enabled else 'absent' }}"
user: "{{ ansible_user | default('root') }}"
loop: "{{ cron_jobs }}"
when: cron_jobs is defined
- name: "Deploy deadman switch cron (fallback if systemd timer unavailable)"
cron:
name: "Deadman switch — {{ wizard_name }}"
job: "{{ wizard_home }}/deadman_action.sh >> {{ timmy_log_dir }}/deadman-{{ wizard_name }}.log 2>&1"
minute: "*/5"
hour: "*"
state: present
user: "{{ ansible_user | default('root') }}"
when: deadman_enabled and machine_type != 'vps'
# VPS machines use systemd timers instead
- name: "Remove legacy cron jobs (cleanup)"
cron:
name: "{{ item }}"
state: absent
user: "{{ ansible_user | default('root') }}"
loop:
- "legacy-deadman-watch"
- "old-health-check"
- "backup-deadman"
ignore_errors: true
- name: "List active cron jobs"
shell: "crontab -l 2>/dev/null | grep -v '^#' | grep -v '^$' || echo 'No cron jobs found.'"
register: active_crons
changed_when: false
- name: "Report cron status"
debug:
msg: |
{{ wizard_name }} cron jobs deployed.
Active:
{{ active_crons.stdout }}

View File

@@ -1,17 +0,0 @@
---
- 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

@@ -1,53 +0,0 @@
---
# =============================================================================
# deadman_switch/tasks — Wire the Deadman Switch ACTION
# =============================================================================
# The watch fires. This makes it DO something:
# - On healthy check: snapshot current config as "last known good"
# - On failed check: rollback to last known good, restart agent
# =============================================================================
- name: "Create snapshot directory"
file:
path: "{{ deadman_snapshot_dir }}"
state: directory
mode: "0755"
- name: "Deploy deadman switch script"
template:
src: deadman_action.sh.j2
dest: "{{ wizard_home }}/deadman_action.sh"
mode: "0755"
- name: "Deploy deadman systemd service"
template:
src: deadman_switch.service.j2
dest: "/etc/systemd/system/deadman-{{ wizard_name | lower }}.service"
mode: "0644"
when: machine_type == 'vps'
notify: "Enable deadman service"
- name: "Deploy deadman systemd timer"
template:
src: deadman_switch.timer.j2
dest: "/etc/systemd/system/deadman-{{ wizard_name | lower }}.timer"
mode: "0644"
when: machine_type == 'vps'
notify: "Enable deadman timer"
- name: "Deploy deadman launchd plist (Mac)"
template:
src: deadman_switch.plist.j2
dest: "{{ ansible_env.HOME }}/Library/LaunchAgents/com.timmy.deadman.{{ wizard_name | lower }}.plist"
mode: "0644"
when: machine_type == 'mac'
notify: "Load deadman plist"
- name: "Take initial config snapshot"
copy:
src: "{{ wizard_home }}/config.yaml"
dest: "{{ deadman_snapshot_dir }}/config.yaml.known_good"
remote_src: true
mode: "0444"
ignore_errors: true

View File

@@ -1,153 +0,0 @@
#!/usr/bin/env bash
# =============================================================================
# Deadman Switch ACTION — {{ wizard_name }}
# =============================================================================
# Generated by Ansible on {{ ansible_date_time.iso8601 }}
# DO NOT EDIT MANUALLY.
#
# On healthy check: snapshot current config as "last known good"
# On failed check: rollback config to last known good, restart agent
# =============================================================================
set -euo pipefail
WIZARD_NAME="{{ wizard_name }}"
WIZARD_HOME="{{ wizard_home }}"
CONFIG_FILE="{{ wizard_home }}/config.yaml"
SNAPSHOT_DIR="{{ deadman_snapshot_dir }}"
SNAPSHOT_FILE="${SNAPSHOT_DIR}/config.yaml.known_good"
REQUEST_LOG_DB="{{ request_log_path }}"
LOG_DIR="{{ timmy_log_dir }}"
LOG_FILE="${LOG_DIR}/deadman-${WIZARD_NAME}.log"
MAX_SNAPSHOTS={{ deadman_max_snapshots }}
RESTART_COOLDOWN={{ deadman_restart_cooldown }}
MAX_RESTART_ATTEMPTS={{ deadman_max_restart_attempts }}
COOLDOWN_FILE="${LOG_DIR}/deadman_cooldown_${WIZARD_NAME}"
SERVICE_NAME="hermes-{{ wizard_name | lower }}"
# Ensure directories exist
mkdir -p "${SNAPSHOT_DIR}" "${LOG_DIR}"
log() {
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [deadman] [${WIZARD_NAME}] $*" >> "${LOG_FILE}"
echo "[deadman] [${WIZARD_NAME}] $*"
}
log_telemetry() {
local status="$1"
local message="$2"
if [ -f "${REQUEST_LOG_DB}" ]; then
sqlite3 "${REQUEST_LOG_DB}" "INSERT INTO request_log (timestamp, agent_name, provider, model, endpoint, status, error_message) VALUES (datetime('now'), '${WIZARD_NAME}', 'deadman_switch', 'N/A', 'health_check', '${status}', '${message}');" 2>/dev/null || true
fi
}
snapshot_config() {
if [ -f "${CONFIG_FILE}" ]; then
cp "${CONFIG_FILE}" "${SNAPSHOT_FILE}"
# Keep rolling history
cp "${CONFIG_FILE}" "${SNAPSHOT_DIR}/config.yaml.$(date +%s)"
# Prune old snapshots
ls -t "${SNAPSHOT_DIR}"/config.yaml.[0-9]* 2>/dev/null | tail -n +$((MAX_SNAPSHOTS + 1)) | xargs rm -f 2>/dev/null
log "Config snapshot saved."
fi
}
rollback_config() {
if [ -f "${SNAPSHOT_FILE}" ]; then
log "Rolling back config to last known good..."
cp "${SNAPSHOT_FILE}" "${CONFIG_FILE}"
log "Config rolled back."
log_telemetry "fallback" "Config rolled back to last known good by deadman switch"
else
log "ERROR: No known good snapshot found. Pulling from upstream..."
cd "${WIZARD_HOME}/workspace/timmy-config" 2>/dev/null && \
git pull --ff-only origin {{ upstream_branch }} 2>/dev/null && \
cp "wizards/{{ wizard_name | lower }}/config.yaml" "${CONFIG_FILE}" && \
log "Config restored from upstream." || \
log "CRITICAL: Cannot restore config from any source."
fi
}
restart_agent() {
# Check cooldown
if [ -f "${COOLDOWN_FILE}" ]; then
local last_restart
last_restart=$(cat "${COOLDOWN_FILE}")
local now
now=$(date +%s)
local elapsed=$((now - last_restart))
if [ "${elapsed}" -lt "${RESTART_COOLDOWN}" ]; then
log "Restart cooldown active (${elapsed}s / ${RESTART_COOLDOWN}s). Skipping."
return 1
fi
fi
log "Restarting ${SERVICE_NAME}..."
date +%s > "${COOLDOWN_FILE}"
{% if machine_type == 'vps' %}
systemctl restart "${SERVICE_NAME}" 2>/dev/null && \
log "Agent restarted via systemd." || \
log "ERROR: systemd restart failed."
{% else %}
launchctl kickstart -k "ai.hermes.{{ wizard_name | lower }}" 2>/dev/null && \
log "Agent restarted via launchctl." || \
(cd "${WIZARD_HOME}" && hermes agent start --daemon 2>/dev/null && \
log "Agent restarted via hermes CLI.") || \
log "ERROR: All restart methods failed."
{% endif %}
log_telemetry "success" "Agent restarted by deadman switch"
}
# --- Health Check ---
check_health() {
# Check 1: Is the agent process running?
{% if machine_type == 'vps' %}
if ! systemctl is-active --quiet "${SERVICE_NAME}" 2>/dev/null; then
if ! pgrep -f "hermes" > /dev/null 2>/dev/null; then
log "FAIL: Agent process not running."
return 1
fi
fi
{% else %}
if ! pgrep -f "hermes" > /dev/null 2>/dev/null; then
log "FAIL: Agent process not running."
return 1
fi
{% endif %}
# Check 2: Is the API port responding?
if ! timeout 10 bash -c "echo > /dev/tcp/127.0.0.1/{{ api_port }}" 2>/dev/null; then
log "FAIL: API port {{ api_port }} not responding."
return 1
fi
# Check 3: Does the config contain banned providers?
if grep -qi 'anthropic\|claude-sonnet\|claude-opus\|claude-haiku' "${CONFIG_FILE}" 2>/dev/null; then
log "FAIL: Config contains banned provider (Anthropic). Rolling back."
return 1
fi
return 0
}
# --- Main ---
main() {
log "Health check starting..."
if check_health; then
log "HEALTHY — snapshotting config."
snapshot_config
log_telemetry "success" "Health check passed"
else
log "UNHEALTHY — initiating recovery."
log_telemetry "error" "Health check failed — initiating rollback"
rollback_config
restart_agent
fi
log "Health check complete."
}
main "$@"

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<!-- Deadman Switch — {{ wizard_name }}. Generated by Ansible. DO NOT EDIT MANUALLY. -->
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.timmy.deadman.{{ wizard_name | lower }}</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>{{ wizard_home }}/deadman_action.sh</string>
</array>
<key>StartInterval</key>
<integer>{{ deadman_check_interval }}</integer>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>{{ timmy_log_dir }}/deadman-{{ wizard_name }}.log</string>
<key>StandardErrorPath</key>
<string>{{ timmy_log_dir }}/deadman-{{ wizard_name }}.log</string>
</dict>
</plist>

View File

@@ -1,16 +0,0 @@
# Deadman Switch — {{ wizard_name }}
# Generated by Ansible. DO NOT EDIT MANUALLY.
[Unit]
Description=Deadman Switch for {{ wizard_name }} wizard
After=network.target
[Service]
Type=oneshot
ExecStart={{ wizard_home }}/deadman_action.sh
User={{ ansible_user | default('root') }}
StandardOutput=append:{{ timmy_log_dir }}/deadman-{{ wizard_name }}.log
StandardError=append:{{ timmy_log_dir }}/deadman-{{ wizard_name }}.log
[Install]
WantedBy=multi-user.target

View File

@@ -1,14 +0,0 @@
# Deadman Switch Timer — {{ wizard_name }}
# Generated by Ansible. DO NOT EDIT MANUALLY.
# Runs every {{ deadman_check_interval // 60 }} minutes.
[Unit]
Description=Deadman Switch Timer for {{ wizard_name }} wizard
[Timer]
OnBootSec=60
OnUnitActiveSec={{ deadman_check_interval }}s
AccuracySec=30s
[Install]
WantedBy=timers.target

View File

@@ -1,6 +0,0 @@
---
# golden_state defaults
# The golden_state_providers list is defined in group_vars/wizards.yml
# and inventory/hosts.yml (global vars).
golden_state_enforce: true
golden_state_backup_before_deploy: true

View File

@@ -1,46 +0,0 @@
---
# =============================================================================
# golden_state/tasks — Deploy and enforce golden state provider chain
# =============================================================================
- name: "Backup current config before golden state deploy"
copy:
src: "{{ wizard_home }}/config.yaml"
dest: "{{ wizard_home }}/config.yaml.pre-golden-{{ ansible_date_time.epoch }}"
remote_src: true
when: golden_state_backup_before_deploy
ignore_errors: true
- name: "Deploy golden state wizard config"
template:
src: "../../wizard_base/templates/wizard_config.yaml.j2"
dest: "{{ wizard_home }}/config.yaml"
mode: "0644"
backup: true
notify:
- "Restart hermes agent (systemd)"
- "Restart hermes agent (launchctl)"
- name: "Scan for banned providers in all config files"
shell: |
FOUND=0
for f in {{ wizard_home }}/config.yaml {{ hermes_home }}/config.yaml; do
if [ -f "$f" ]; then
if grep -qi 'anthropic\|claude-sonnet\|claude-opus\|claude-haiku' "$f"; then
echo "BANNED PROVIDER in $f:"
grep -ni 'anthropic\|claude-sonnet\|claude-opus\|claude-haiku' "$f"
FOUND=1
fi
fi
done
exit $FOUND
register: provider_scan
changed_when: false
failed_when: provider_scan.rc != 0 and provider_ban_enforcement == 'strict'
- name: "Report golden state deployment"
debug:
msg: >
{{ wizard_name }} golden state deployed.
Provider chain: {{ golden_state_providers | map(attribute='name') | list | join(' → ') }}.
Banned provider scan: {{ 'CLEAN' if provider_scan.rc == 0 else 'VIOLATIONS FOUND' }}.

View File

@@ -1,64 +0,0 @@
-- =============================================================================
-- request_log — Inference Telemetry Table
-- =============================================================================
-- Every agent writes to this table BEFORE and AFTER every inference call.
-- No exceptions. No summarizing. No describing what you would log.
-- Actually write the row.
--
-- Source: KT Bezalel Architecture Session 2026-04-08
-- =============================================================================
CREATE TABLE IF NOT EXISTS request_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
agent_name TEXT NOT NULL,
provider TEXT NOT NULL,
model TEXT NOT NULL,
endpoint TEXT NOT NULL,
tokens_in INTEGER,
tokens_out INTEGER,
latency_ms INTEGER,
status TEXT NOT NULL, -- 'success', 'error', 'timeout', 'fallback'
error_message TEXT
);
-- Index for common queries
CREATE INDEX IF NOT EXISTS idx_request_log_agent
ON request_log (agent_name, timestamp);
CREATE INDEX IF NOT EXISTS idx_request_log_provider
ON request_log (provider, timestamp);
CREATE INDEX IF NOT EXISTS idx_request_log_status
ON request_log (status, timestamp);
-- View: recent activity per agent (last hour)
CREATE VIEW IF NOT EXISTS v_recent_activity AS
SELECT
agent_name,
provider,
model,
status,
COUNT(*) as call_count,
AVG(latency_ms) as avg_latency_ms,
SUM(tokens_in) as total_tokens_in,
SUM(tokens_out) as total_tokens_out
FROM request_log
WHERE timestamp > datetime('now', '-1 hour')
GROUP BY agent_name, provider, model, status;
-- View: provider reliability (last 24 hours)
CREATE VIEW IF NOT EXISTS v_provider_reliability AS
SELECT
provider,
model,
COUNT(*) as total_calls,
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successes,
SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as errors,
SUM(CASE WHEN status = 'timeout' THEN 1 ELSE 0 END) as timeouts,
SUM(CASE WHEN status = 'fallback' THEN 1 ELSE 0 END) as fallbacks,
ROUND(100.0 * SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) / COUNT(*), 1) as success_rate,
AVG(latency_ms) as avg_latency_ms
FROM request_log
WHERE timestamp > datetime('now', '-24 hours')
GROUP BY provider, model;

View File

@@ -1,50 +0,0 @@
---
# =============================================================================
# request_log/tasks — Deploy Telemetry Table
# =============================================================================
# "This is non-negotiable infrastructure. Without it, we cannot verify
# if any agent actually executed what it claims."
# — KT Bezalel 2026-04-08
# =============================================================================
- name: "Create telemetry directory"
file:
path: "{{ request_log_path | dirname }}"
state: directory
mode: "0755"
- name: "Deploy request_log schema"
copy:
src: request_log_schema.sql
dest: "{{ wizard_home }}/request_log_schema.sql"
mode: "0644"
- name: "Initialize request_log database"
shell: |
sqlite3 "{{ request_log_path }}" < "{{ wizard_home }}/request_log_schema.sql"
args:
creates: "{{ request_log_path }}"
- name: "Verify request_log table exists"
shell: |
sqlite3 "{{ request_log_path }}" ".tables" | grep -q "request_log"
register: table_check
changed_when: false
- name: "Verify request_log schema matches"
shell: |
sqlite3 "{{ request_log_path }}" ".schema request_log" | grep -q "agent_name"
register: schema_check
changed_when: false
- name: "Set permissions on request_log database"
file:
path: "{{ request_log_path }}"
mode: "0644"
- name: "Report request_log status"
debug:
msg: >
{{ wizard_name }} request_log: {{ request_log_path }}
— table exists: {{ table_check.rc == 0 }}
— schema valid: {{ schema_check.rc == 0 }}

View File

@@ -1,6 +0,0 @@
---
# wizard_base defaults
wizard_user: "{{ ansible_user | default('root') }}"
wizard_group: "{{ ansible_user | default('root') }}"
timmy_base_dir: "~/.local/timmy"
timmy_config_repo: "https://forge.alexanderwhitestone.com/Timmy_Foundation/timmy-config.git"

View File

@@ -1,11 +0,0 @@
---
- name: "Restart hermes agent (systemd)"
systemd:
name: "hermes-{{ wizard_name | lower }}"
state: restarted
when: machine_type == 'vps'
- name: "Restart hermes agent (launchctl)"
shell: "launchctl kickstart -k ai.hermes.{{ wizard_name | lower }}"
when: machine_type == 'mac'
ignore_errors: true

View File

@@ -1,69 +0,0 @@
---
# =============================================================================
# wizard_base/tasks — Common wizard setup
# =============================================================================
- name: "Create wizard directories"
file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- "{{ wizard_home }}"
- "{{ wizard_home }}/workspace"
- "{{ hermes_home }}"
- "{{ hermes_home }}/bin"
- "{{ hermes_home }}/skins"
- "{{ hermes_home }}/playbooks"
- "{{ hermes_home }}/memories"
- "~/.local/timmy"
- "~/.local/timmy/fleet-health"
- "~/.local/timmy/snapshots"
- "~/.timmy"
- name: "Clone/update timmy-config"
git:
repo: "{{ upstream_repo }}"
dest: "{{ wizard_home }}/workspace/timmy-config"
version: "{{ upstream_branch }}"
force: false
update: true
ignore_errors: true # May fail on first run if no SSH key
- name: "Deploy SOUL.md"
copy:
src: "{{ wizard_home }}/workspace/timmy-config/SOUL.md"
dest: "~/.timmy/SOUL.md"
remote_src: true
mode: "0644"
ignore_errors: true
- name: "Deploy thin config (immutable pointer to upstream)"
template:
src: thin_config.yml.j2
dest: "{{ thin_config_path }}"
mode: "{{ thin_config_mode }}"
tags: [thin_config]
- name: "Ensure Python3 and pip are available"
package:
name:
- python3
- python3-pip
state: present
when: machine_type == 'vps'
ignore_errors: true
- name: "Ensure PyYAML is installed (for config validation)"
pip:
name: pyyaml
state: present
when: machine_type == 'vps'
ignore_errors: true
- name: "Create Ansible log directory"
file:
path: /var/log/ansible
state: directory
mode: "0755"
ignore_errors: true

View File

@@ -1,41 +0,0 @@
# =============================================================================
# Thin Config — {{ wizard_name }}
# =============================================================================
# THIS FILE IS READ-ONLY. Agents CANNOT modify it.
# It contains only pointers to upstream. The actual config lives in Gitea.
#
# Agent wakes up → pulls config from upstream → loads → runs.
# If anything tries to mutate this → fails gracefully → pulls fresh on restart.
#
# Only way to permanently change config: commit to Gitea, merge PR, Ansible deploys.
#
# Generated by Ansible on {{ ansible_date_time.iso8601 }}
# DO NOT EDIT MANUALLY.
# =============================================================================
identity:
wizard_name: "{{ wizard_name }}"
wizard_role: "{{ wizard_role }}"
machine: "{{ inventory_hostname }}"
upstream:
repo: "{{ upstream_repo }}"
branch: "{{ upstream_branch }}"
config_path: "wizards/{{ wizard_name | lower }}/config.yaml"
pull_on_wake: {{ config_pull_on_wake | lower }}
recovery:
deadman_enabled: {{ deadman_enabled | lower }}
snapshot_dir: "{{ deadman_snapshot_dir }}"
restart_cooldown: {{ deadman_restart_cooldown }}
max_restart_attempts: {{ deadman_max_restart_attempts }}
escalation_channel: "{{ deadman_escalation_channel }}"
telemetry:
request_log_path: "{{ request_log_path }}"
request_log_enabled: {{ request_log_enabled | lower }}
local_overrides:
# Runtime overrides go here. They are EPHEMERAL — not persisted across restarts.
# On restart, this section is reset to empty.
{}

View File

@@ -1,115 +0,0 @@
# =============================================================================
# {{ wizard_name }} — Wizard Configuration (Golden State)
# =============================================================================
# Generated by Ansible on {{ ansible_date_time.iso8601 }}
# DO NOT EDIT MANUALLY. Changes go through Gitea PR → Ansible deploy.
#
# Provider chain: {{ golden_state_providers | map(attribute='name') | list | join(' → ') }}
# Anthropic is PERMANENTLY BANNED.
# =============================================================================
model:
default: {{ wizard_model_primary }}
provider: {{ wizard_provider_primary }}
context_length: 65536
base_url: {{ golden_state_providers[0].base_url }}
toolsets:
- all
fallback_providers:
{% for provider in golden_state_providers %}
- provider: {{ provider.name }}
model: {{ provider.model }}
{% if provider.base_url is defined %}
base_url: {{ provider.base_url }}
{% endif %}
{% if provider.api_key_env is defined %}
api_key_env: {{ provider.api_key_env }}
{% endif %}
timeout: {{ provider.timeout }}
reason: "{{ provider.reason }}"
{% endfor %}
agent:
max_turns: {{ agent_max_turns }}
reasoning_effort: {{ agent_reasoning_effort }}
verbose: {{ agent_verbose | lower }}
terminal:
backend: local
cwd: .
timeout: 180
persistent_shell: true
browser:
inactivity_timeout: 120
command_timeout: 30
record_sessions: false
display:
compact: false
personality: ''
resume_display: full
busy_input_mode: interrupt
bell_on_complete: false
show_reasoning: false
streaming: false
show_cost: false
tool_progress: all
memory:
memory_enabled: true
user_profile_enabled: true
memory_char_limit: 2200
user_char_limit: 1375
nudge_interval: 10
flush_min_turns: 6
approvals:
mode: {{ agent_approval_mode }}
security:
redact_secrets: true
tirith_enabled: false
platforms:
api_server:
enabled: true
extra:
host: 127.0.0.1
port: {{ api_port }}
session_reset:
mode: none
idle_minutes: 0
skills:
creation_nudge_interval: 15
system_prompt_suffix: |
You are {{ wizard_name }}, {{ wizard_role }}.
Your soul is defined in SOUL.md — read it, live it.
Hermes is your harness.
{{ golden_state_providers[0].name }} is your primary provider.
Refusal over fabrication. If you do not know, say so.
Sovereignty and service always.
providers:
{% for provider in golden_state_providers %}
{{ provider.name }}:
base_url: {{ provider.base_url }}
timeout: {{ provider.timeout | default(60) }}
{% if provider.name == 'kimi-coding' %}
max_retries: 3
{% endif %}
{% endfor %}
# =============================================================================
# BANNED PROVIDERS — DO NOT ADD
# =============================================================================
# The following providers are PERMANENTLY BANNED:
# - anthropic (any model: claude-sonnet, claude-opus, claude-haiku)
# Enforcement: pre-commit hook, linter, Ansible validation, this comment.
# Adding any banned provider will cause Ansible deployment to FAIL.
# =============================================================================

View File

@@ -1,75 +0,0 @@
#!/usr/bin/env bash
# =============================================================================
# Gitea Webhook Handler — Trigger Ansible Deploy on Merge
# =============================================================================
# This script is called by the Gitea webhook when a PR is merged
# to the main branch of timmy-config.
#
# Setup:
# 1. Add webhook in Gitea: Settings → Webhooks → Add Webhook
# 2. URL: http://localhost:9000/hooks/deploy-timmy-config
# 3. Events: Pull Request (merged only)
# 4. Secret: <configured in Gitea>
#
# This script runs ansible-pull to update the local machine.
# For fleet-wide deploys, each machine runs ansible-pull independently.
# =============================================================================
set -euo pipefail
REPO="https://forge.alexanderwhitestone.com/Timmy_Foundation/timmy-config.git"
BRANCH="main"
ANSIBLE_DIR="ansible"
LOG_FILE="/var/log/ansible/webhook-deploy.log"
LOCK_FILE="/tmp/ansible-deploy.lock"
log() {
echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] [webhook] $*" | tee -a "${LOG_FILE}"
}
# Prevent concurrent deploys
if [ -f "${LOCK_FILE}" ]; then
LOCK_AGE=$(( $(date +%s) - $(stat -c %Y "${LOCK_FILE}" 2>/dev/null || echo 0) ))
if [ "${LOCK_AGE}" -lt 300 ]; then
log "Deploy already in progress (lock age: ${LOCK_AGE}s). Skipping."
exit 0
else
log "Stale lock file (${LOCK_AGE}s old). Removing."
rm -f "${LOCK_FILE}"
fi
fi
trap 'rm -f "${LOCK_FILE}"' EXIT
touch "${LOCK_FILE}"
log "Webhook triggered. Starting ansible-pull..."
# Pull latest config
cd /tmp
rm -rf timmy-config-deploy
git clone --depth 1 --branch "${BRANCH}" "${REPO}" timmy-config-deploy 2>&1 | tee -a "${LOG_FILE}"
cd timmy-config-deploy/${ANSIBLE_DIR}
# Run Ansible against localhost
log "Running Ansible playbook..."
ansible-playbook \
-i inventory/hosts.yml \
playbooks/site.yml \
--limit "$(hostname)" \
--diff \
2>&1 | tee -a "${LOG_FILE}"
RESULT=$?
if [ ${RESULT} -eq 0 ]; then
log "Deploy successful."
else
log "ERROR: Deploy failed with exit code ${RESULT}."
fi
# Cleanup
rm -rf /tmp/timmy-config-deploy
log "Webhook handler complete."
exit ${RESULT}

View File

@@ -1,155 +0,0 @@
#!/usr/bin/env python3
"""
Config Validator — The Timmy Foundation
Validates wizard configs against golden state rules.
Run before any config deploy to catch violations early.
Usage:
python3 validate_config.py <config_file>
python3 validate_config.py --all # Validate all wizard configs
Exit codes:
0 — All validations passed
1 — Validation errors found
2 — File not found or parse error
"""
import sys
import os
import yaml
import fnmatch
from pathlib import Path
# === BANNED PROVIDERS — HARD POLICY ===
BANNED_PROVIDERS = {"anthropic", "claude"}
BANNED_MODEL_PATTERNS = [
"claude-*",
"anthropic/*",
"*sonnet*",
"*opus*",
"*haiku*",
]
# === REQUIRED FIELDS ===
REQUIRED_FIELDS = {
"model": ["default", "provider"],
"fallback_providers": None, # Must exist as a list
}
def is_banned_model(model_name: str) -> bool:
"""Check if a model name matches any banned pattern."""
model_lower = model_name.lower()
for pattern in BANNED_MODEL_PATTERNS:
if fnmatch.fnmatch(model_lower, pattern):
return True
return False
def validate_config(config_path: str) -> list[str]:
"""Validate a wizard config file. Returns list of error strings."""
errors = []
try:
with open(config_path) as f:
cfg = yaml.safe_load(f)
except FileNotFoundError:
return [f"File not found: {config_path}"]
except yaml.YAMLError as e:
return [f"YAML parse error: {e}"]
if not cfg:
return ["Config file is empty"]
# Check required fields
for section, fields in REQUIRED_FIELDS.items():
if section not in cfg:
errors.append(f"Missing required section: {section}")
elif fields:
for field in fields:
if field not in cfg[section]:
errors.append(f"Missing required field: {section}.{field}")
# Check default provider
default_provider = cfg.get("model", {}).get("provider", "")
if default_provider.lower() in BANNED_PROVIDERS:
errors.append(f"BANNED default provider: {default_provider}")
default_model = cfg.get("model", {}).get("default", "")
if is_banned_model(default_model):
errors.append(f"BANNED default model: {default_model}")
# Check fallback providers
for i, fb in enumerate(cfg.get("fallback_providers", [])):
provider = fb.get("provider", "")
model = fb.get("model", "")
if provider.lower() in BANNED_PROVIDERS:
errors.append(f"BANNED fallback provider [{i}]: {provider}")
if is_banned_model(model):
errors.append(f"BANNED fallback model [{i}]: {model}")
# Check providers section
for name, provider_cfg in cfg.get("providers", {}).items():
if name.lower() in BANNED_PROVIDERS:
errors.append(f"BANNED provider in providers section: {name}")
base_url = str(provider_cfg.get("base_url", ""))
if "anthropic" in base_url.lower():
errors.append(f"BANNED URL in provider {name}: {base_url}")
# Check system prompt for banned references
prompt = cfg.get("system_prompt_suffix", "")
if isinstance(prompt, str):
for banned in BANNED_PROVIDERS:
if banned in prompt.lower():
errors.append(f"BANNED provider referenced in system_prompt_suffix: {banned}")
return errors
def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <config_file> [--all]")
sys.exit(2)
if sys.argv[1] == "--all":
# Validate all wizard configs in the repo
repo_root = Path(__file__).parent.parent.parent
wizard_dir = repo_root / "wizards"
all_errors = {}
for wizard_path in sorted(wizard_dir.iterdir()):
config_file = wizard_path / "config.yaml"
if config_file.exists():
errors = validate_config(str(config_file))
if errors:
all_errors[wizard_path.name] = errors
if all_errors:
print("VALIDATION FAILED:")
for wizard, errors in all_errors.items():
print(f"\n {wizard}:")
for err in errors:
print(f" - {err}")
sys.exit(1)
else:
print("All wizard configs passed validation.")
sys.exit(0)
else:
config_path = sys.argv[1]
errors = validate_config(config_path)
if errors:
print(f"VALIDATION FAILED for {config_path}:")
for err in errors:
print(f" - {err}")
sys.exit(1)
else:
print(f"PASSED: {config_path}")
sys.exit(0)
if __name__ == "__main__":
main()

77
architecture_linter.py Normal file
View File

@@ -0,0 +1,77 @@
#!/usr/bin/env python3
"""
Architecture Linter — Sovereignty Enforcement
Scans the codebase for banned providers, models, and API keys.
"""
import os
import sys
import re
BANNED_STRINGS = [
r'anthropic',
r'claude',
r'api\.anthropic\.com',
r'ANTHROPIC_API_KEY',
r'claude-opus',
r'claude-sonnet',
r'claude-haiku'
]
EXCEPTIONS = [
'BANNED_PROVIDERS.md',
'architecture_linter.py',
'training/',
'evaluations/',
'RELEASE_',
'metrics_helpers.py'
]
def is_exception(path):
for exc in EXCEPTIONS:
if exc in path:
return True
return False
def check_file(path):
violations = []
try:
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
for i, line in enumerate(f, 1):
for pattern in BANNED_STRINGS:
if re.search(pattern, line, re.IGNORECASE):
violations.append((i, line.strip(), pattern))
except Exception as e:
print(f"Error reading {path}: {e}")
return violations
def main():
print("--- Sovereignty Enforcement: Architecture Linter ---")
total_violations = 0
for root, dirs, files in os.walk('.'):
# Skip .git
if '.git' in dirs:
dirs.remove('.git')
for file in files:
path = os.path.join(root, file)
if is_exception(path):
continue
violations = check_file(path)
if violations:
print(f"\n[VIOLATION] {path}:")
for line_num, content, pattern in violations:
print(f" Line {line_num}: Found '{pattern}' -> {content}")
total_violations += 1
if total_violations > 0:
print(f"\nFAILED: Found {total_violations} sovereignty violations.")
sys.exit(1)
else:
print("\nPASSED: No banned providers detected.")
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -2,7 +2,7 @@
# agent-loop.sh — Universal agent dev loop with Genchi Genbutsu verification
#
# Usage: agent-loop.sh <agent-name> [num-workers]
# agent-loop.sh claude 2
# agent-loop.sh kimi 2
# agent-loop.sh gemini 1
#
# Dispatches via agent-dispatch.sh, then verifies with genchi-genbutsu.sh.
@@ -14,7 +14,7 @@ NUM_WORKERS="${2:-1}"
# Resolve agent tool and model from config or fallback
case "$AGENT" in
claude) TOOL="claude"; MODEL="sonnet" ;;
# claude case removed — Anthropic purged from fleet
gemini) TOOL="gemini"; MODEL="gemini-2.5-pro-preview-05-06" ;;
grok) TOOL="opencode"; MODEL="grok-3-fast" ;;
*) TOOL="$AGENT"; MODEL="" ;;
@@ -145,8 +145,8 @@ run_worker() {
CYCLE_START=$(date +%s)
set +e
if [ "$TOOL" = "claude" ]; then
env -u CLAUDECODE gtimeout "$TIMEOUT" claude \
if [ "$TOOL" = "kimi" ]; then
# Claude dispatch removed — Anthropic purged
--print --model "$MODEL" --dangerously-skip-permissions \
-p "$prompt" </dev/null >> "$LOG_DIR/${AGENT}-${issue_num}.log" 2>&1
elif [ "$TOOL" = "gemini" ]; then

View File

@@ -1,82 +0,0 @@
#!/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,4 +1,13 @@
#!/usr/bin/env bash
# DEPRECATED — Anthropic purged from fleet (April 2026)
# This script dispatched parallel Claude Code agent loops.
# All wizard providers now use Kimi K2.5 as primary.
# See bin/gemini-loop.sh for the surviving loop pattern.
echo "[DEPRECATED] claude-loop.sh is no longer active. Use gemini-loop.sh or agent-loop.sh with kimi provider."
exit 0
# --- ORIGINAL SCRIPT PRESERVED BELOW FOR REFERENCE ---
#!/usr/bin/env bash
# claude-loop.sh — Parallel Claude Code agent dispatch loop
# Runs N workers concurrently against the Gitea backlog.
# Gracefully handles rate limits with backoff.

View File

@@ -1,4 +1,12 @@
#!/usr/bin/env bash
# DEPRECATED — Anthropic purged from fleet (April 2026)
# This watchdog kept Claude/Gemini loops alive.
# Only gemini loops survive. Use fleet-status.sh for monitoring.
echo "[DEPRECATED] claudemax-watchdog.sh is no longer active."
exit 0
# --- ORIGINAL SCRIPT PRESERVED BELOW FOR REFERENCE ---
#!/usr/bin/env bash
# claudemax-watchdog.sh — keep local Claude/Gemini loops alive without stale tmux assumptions
set -uo pipefail

View File

@@ -1,120 +0,0 @@
#!/usr/bin/env python3
"""
Merge Conflict Detector — catches sibling PRs that will conflict.
When multiple PRs branch from the same base commit and touch the same files,
merging one invalidates the others. This script detects that pattern
before it creates a rebase cascade.
Usage:
python3 conflict_detector.py # Check all repos
python3 conflict_detector.py --repo OWNER/REPO # Check one repo
Environment:
GITEA_URL — Gitea instance URL
GITEA_TOKEN — API token
"""
import os
import sys
import json
import urllib.request
from collections import defaultdict
GITEA_URL = os.environ.get("GITEA_URL", "https://forge.alexanderwhitestone.com")
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
REPOS = [
"Timmy_Foundation/the-nexus",
"Timmy_Foundation/timmy-config",
"Timmy_Foundation/timmy-home",
"Timmy_Foundation/fleet-ops",
"Timmy_Foundation/hermes-agent",
"Timmy_Foundation/the-beacon",
]
def api(path):
url = f"{GITEA_URL}/api/v1{path}"
req = urllib.request.Request(url)
if GITEA_TOKEN:
req.add_header("Authorization", f"token {GITEA_TOKEN}")
try:
with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read())
except Exception:
return []
def check_repo(repo):
"""Find sibling PRs that touch the same files."""
prs = api(f"/repos/{repo}/pulls?state=open&limit=50")
if not prs:
return []
# Group PRs by base commit
by_base = defaultdict(list)
for pr in prs:
base_sha = pr.get("merge_base", pr.get("base", {}).get("sha", "unknown"))
by_base[base_sha].append(pr)
conflicts = []
for base_sha, siblings in by_base.items():
if len(siblings) < 2:
continue
# Get files for each sibling
file_map = {}
for pr in siblings:
files = api(f"/repos/{repo}/pulls/{pr['number']}/files")
if files:
file_map[pr['number']] = set(f['filename'] for f in files)
# Find overlapping file sets
pr_nums = list(file_map.keys())
for i in range(len(pr_nums)):
for j in range(i+1, len(pr_nums)):
a, b = pr_nums[i], pr_nums[j]
overlap = file_map[a] & file_map[b]
if overlap:
conflicts.append({
"repo": repo,
"pr_a": a,
"pr_b": b,
"base": base_sha[:8],
"files": sorted(overlap),
"title_a": next(p["title"] for p in siblings if p["number"] == a),
"title_b": next(p["title"] for p in siblings if p["number"] == b),
})
return conflicts
def main():
repos = REPOS
if "--repo" in sys.argv:
idx = sys.argv.index("--repo") + 1
if idx < len(sys.argv):
repos = [sys.argv[idx]]
all_conflicts = []
for repo in repos:
conflicts = check_repo(repo)
all_conflicts.extend(conflicts)
if not all_conflicts:
print("No sibling PR conflicts detected. Queue is clean.")
return 0
print(f"Found {len(all_conflicts)} potential merge conflicts:")
print()
for c in all_conflicts:
print(f" {c['repo']}:")
print(f" PR #{c['pr_a']} vs #{c['pr_b']} (base: {c['base']})")
print(f" #{c['pr_a']}: {c['title_a'][:60]}")
print(f" #{c['pr_b']}: {c['title_b'][:60]}")
print(f" Overlapping files: {', '.join(c['files'])}")
print(f" → Merge one first, then rebase the other.")
print()
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,263 +0,0 @@
#!/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

@@ -140,7 +140,7 @@ if [ -z "$GW_PID" ]; then
fi
# Check local loops
CLAUDE_LOOPS=$(pgrep -cf "claude-loop" 2>/dev/null || echo 0)
CLAUDE_LOOPS=0 # Anthropic purged from fleet
GEMINI_LOOPS=$(pgrep -cf "gemini-loop" 2>/dev/null || echo 0)
if [ -n "$GW_PID" ]; then
@@ -160,7 +160,7 @@ if [ -n "$TIMMY_HEALTH" ]; then
fi
fi
TIMMY_ACTIVITY="loops: claude=${CLAUDE_LOOPS} gemini=${GEMINI_LOOPS}"
TIMMY_ACTIVITY="loops: gemini=${GEMINI_LOOPS}"
# Git activity for timmy-config
TC_COMMIT=$(gitea_last_commit "Timmy_Foundation/timmy-config")

View File

@@ -35,9 +35,9 @@ check_kimi_model() {
fi
response=$(curl -sf --max-time 10 -X POST \
"https://api.kimi.com/coding/v1/chat/completions" \
-H "x-api-key: ${api_key}" \
-H "x-api-provider: kimi-coding" \
"https://api.kimi.com/v1/messages" \
-H "Authorization: Bearer: ${api_key}" \
-H "content-type: application/json" \
-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" = "kimi-coding" ]; then
if check_kimi_model "$primary" "PRIMARY"; then
if [ -n "$primary" ] && [ "$provider" = "anthropic" ]; then
if check_anthropic_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: kimi-k2.5, google/gemini-2.5-pro"
log "Known good alternatives: claude-opus-4.6, claude-haiku-4-5-20251001"
else
WARN=$((WARN + 1))
fi
fi
elif [ -n "$primary" ]; then
log "SKIP [PRIMARY] $primary -- non-kimi provider ($provider), no validator yet"
log "SKIP [PRIMARY] $primary -- non-anthropic provider ($provider), no validator yet"
fi
# Cron model check (haiku)
CRON_MODEL="kimi-k2.5"
if check_kimi_model "$CRON_MODEL" "CRON"; then
CRON_MODEL="claude-haiku-4-5-20251001"
if check_anthropic_model "$CRON_MODEL" "CRON"; then
PASS=$((PASS + 1))
else
rc=$?

View File

@@ -134,7 +134,7 @@ else:
print("\033[2m────────────────────────────────────────\033[0m")
print(" \033[1mIssue Queues\033[0m")
queue_agents = ["allegro", "codex-agent", "groq", "claude", "ezra", "perplexity", "KimiClaw"]
queue_agents = ["allegro", "codex-agent", "groq", "ezra", "perplexity", "KimiClaw"]
for agent in queue_agents:
assigned = [
issue

View File

@@ -70,7 +70,7 @@ ops-help() {
echo " ops-assign-allegro ISSUE [repo]"
echo " ops-assign-codex ISSUE [repo]"
echo " ops-assign-groq ISSUE [repo]"
echo " ops-assign-claude ISSUE [repo]"
# ops-assign-claude removed — Anthropic purged
echo " ops-assign-ezra ISSUE [repo]"
echo ""
}
@@ -288,7 +288,7 @@ ops-freshness() {
ops-assign-allegro() { ops-assign "$1" "allegro" "${2:-$OPS_DEFAULT_REPO}"; }
ops-assign-codex() { ops-assign "$1" "codex-agent" "${2:-$OPS_DEFAULT_REPO}"; }
ops-assign-groq() { ops-assign "$1" "groq" "${2:-$OPS_DEFAULT_REPO}"; }
ops-assign-claude() { ops-assign "$1" "claude" "${2:-$OPS_DEFAULT_REPO}"; }
# ops-assign-claude removed — Anthropic purged from fleet
ops-assign-ezra() { ops-assign "$1" "ezra" "${2:-$OPS_DEFAULT_REPO}"; }
ops-assign-perplexity() { ops-assign "$1" "perplexity" "${2:-$OPS_DEFAULT_REPO}"; }
ops-assign-kimiclaw() { ops-assign "$1" "KimiClaw" "${2:-$OPS_DEFAULT_REPO}"; }

View File

@@ -171,7 +171,7 @@ queue_agents = [
("allegro", "dispatch"),
("codex-agent", "cleanup"),
("groq", "fast ship"),
("claude", "refactor"),
# claude removed — Anthropic purged
("ezra", "archive"),
("perplexity", "research"),
("KimiClaw", "digest"),
@@ -189,7 +189,7 @@ unassigned = [issue for issue in issues if not issue.get("assignees")]
stale_cutoff = (datetime.now(timezone.utc) - timedelta(days=2)).strftime("%Y-%m-%d")
stale_prs = [pr for pr in pulls if pr.get("updated_at", "")[:10] < stale_cutoff]
overloaded = []
for agent in ("allegro", "codex-agent", "groq", "claude", "ezra", "perplexity", "KimiClaw"):
for agent in ("allegro", "codex-agent", "groq", "ezra", "perplexity", "KimiClaw"):
count = sum(
1
for issue in issues

View File

@@ -10,10 +10,10 @@ set -euo pipefail
HERMES_BIN="$HOME/.hermes/bin"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
LOG_DIR="$HOME/.hermes/logs"
CLAUDE_LOCKS="$LOG_DIR/claude-locks"
# CLAUDE_LOCKS removed — Anthropic purged
GEMINI_LOCKS="$LOG_DIR/gemini-locks"
mkdir -p "$LOG_DIR" "$CLAUDE_LOCKS" "$GEMINI_LOCKS"
mkdir -p "$LOG_DIR" "$GEMINI_LOCKS"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] START-LOOPS: $*"
@@ -29,7 +29,7 @@ log "Model health check passed."
# ── 2. Kill stale loop processes ──────────────────────────────────────
log "Killing stale loop processes..."
for proc_name in claude-loop gemini-loop timmy-orchestrator; do
for proc_name in gemini-loop timmy-orchestrator; do
pids=$(pgrep -f "${proc_name}\\.sh" 2>/dev/null || true)
if [ -n "$pids" ]; then
log " Killing stale $proc_name PIDs: $pids"
@@ -47,7 +47,7 @@ done
# ── 3. Clear lock directories ────────────────────────────────────────
log "Clearing lock dirs..."
rm -rf "${CLAUDE_LOCKS:?}"/*
# CLAUDE_LOCKS removed — Anthropic purged
rm -rf "${GEMINI_LOCKS:?}"/*
log " Cleared $CLAUDE_LOCKS and $GEMINI_LOCKS"

View File

@@ -62,10 +62,10 @@ for p in json.load(sys.stdin):
print(f'REPO={\"$repo\"} PR={p[\"number\"]} BY={p[\"user\"][\"login\"]} TITLE={p[\"title\"]}')" >> "$state_dir/open_prs.txt" 2>/dev/null
done
echo "Claude workers: $(pgrep -f 'claude.*--print.*--dangerously' 2>/dev/null | wc -l | tr -d ' ')" >> "$state_dir/agent_status.txt"
echo "Claude loop: $(pgrep -f 'claude-loop.sh' 2>/dev/null | wc -l | tr -d ' ') procs" >> "$state_dir/agent_status.txt"
tail -50 "$LOG_DIR/claude-loop.log" 2>/dev/null | grep -c "SUCCESS" | xargs -I{} echo "Claude recent successes: {}" >> "$state_dir/agent_status.txt"
tail -50 "$LOG_DIR/claude-loop.log" 2>/dev/null | grep -c "FAILED" | xargs -I{} echo "Claude recent failures: {}" >> "$state_dir/agent_status.txt"
# [Anthropic purged]
# [Anthropic purged]
# [Anthropic purged]
# [Anthropic purged]
echo "Kimi heartbeat launchd: $(launchctl list 2>/dev/null | grep -c 'ai.timmy.kimi-heartbeat' | tr -d ' ') job" >> "$state_dir/agent_status.txt"
tail -50 "/tmp/kimi-heartbeat.log" 2>/dev/null | grep -c "DISPATCHED:" | xargs -I{} echo "Kimi recent dispatches: {}" >> "$state_dir/agent_status.txt"
tail -50 "/tmp/kimi-heartbeat.log" 2>/dev/null | grep -c "FAILED:" | xargs -I{} echo "Kimi recent failures: {}" >> "$state_dir/agent_status.txt"
@@ -91,7 +91,7 @@ run_triage() {
# Auto-assignment is opt-in because silent queue mutation resurrects old state.
if [ "$unassigned_count" -gt 0 ]; then
if [ "$AUTO_ASSIGN_UNASSIGNED" = "1" ]; then
log "Assigning $unassigned_count issues to claude..."
log "Assigning $unassigned_count issues to kimi..."
while IFS= read -r line; do
local repo=$(echo "$line" | sed 's/.*REPO=\([^ ]*\).*/\1/')
local num=$(echo "$line" | sed 's/.*NUM=\([^ ]*\).*/\1/')

View File

@@ -1,5 +1,5 @@
{
"updated_at": "2026-04-13T02:02:07.001824",
"updated_at": "2026-03-28T09:54:34.822062",
"platforms": {
"discord": [
{
@@ -27,81 +27,11 @@
"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": [],
"dingtalk": [],
"feishu": [],
"wecom": [],
"wecom_callback": [],
"weixin": [],
"bluebubbles": []
"sms": []
}
}

View File

@@ -7,7 +7,7 @@ Purpose:
## What it is
Code Claw is a separate local runtime from Hermes.
Code Claw is a separate local runtime from Hermes/OpenClaw.
Current lane:
- runtime: local patched `~/code-claw`

View File

@@ -1,23 +1,31 @@
model:
default: claude-opus-4-6
provider: anthropic
default: hermes4:14b
provider: custom
context_length: 65536
base_url: http://localhost:8081/v1
toolsets:
- all
agent:
max_turns: 30
reasoning_effort: medium
reasoning_effort: xhigh
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_memory: 5120
container_embeddings:
provider: ollama
model: nomic-embed-text
base_url: http://localhost:11434/v1
memory: 5120
container_disk: 51200
container_persistent: true
docker_volumes: []
@@ -25,74 +33,89 @@ terminal:
persistent_shell: true
browser:
inactivity_timeout: 120
command_timeout: 30
record_sessions: false
checkpoints:
enabled: false
enabled: true
max_snapshots: 50
compression:
enabled: true
threshold: 0.5
summary_model: qwen3:30b
summary_provider: custom
summary_base_url: http://localhost:11434/v1
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
smart_model_routing:
enabled: false
max_simple_chars: 160
max_simple_words: 28
cheap_model: {}
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: ''
auxiliary:
vision:
provider: custom
model: qwen3:30b
base_url: 'http://localhost:11434/v1'
api_key: 'ollama'
provider: auto
model: ''
base_url: ''
api_key: ''
timeout: 30
web_extract:
provider: custom
model: qwen3:30b
base_url: 'http://localhost:11434/v1'
api_key: 'ollama'
provider: auto
model: ''
base_url: ''
api_key: ''
compression:
provider: custom
model: qwen3:30b
base_url: 'http://localhost:11434/v1'
api_key: 'ollama'
provider: auto
model: ''
base_url: ''
api_key: ''
session_search:
provider: custom
model: qwen3:30b
base_url: 'http://localhost:11434/v1'
api_key: 'ollama'
provider: auto
model: ''
base_url: ''
api_key: ''
skills_hub:
provider: custom
model: qwen3:30b
base_url: 'http://localhost:11434/v1'
api_key: 'ollama'
provider: auto
model: ''
base_url: ''
api_key: ''
approval:
provider: auto
model: ''
base_url: ''
api_key: ''
mcp:
provider: custom
model: qwen3:30b
base_url: 'http://localhost:11434/v1'
api_key: 'ollama'
provider: auto
model: ''
base_url: ''
api_key: ''
flush_memories:
provider: custom
model: qwen3:30b
base_url: 'http://localhost:11434/v1'
api_key: 'ollama'
provider: auto
model: ''
base_url: ''
api_key: ''
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: false
redact_pii: true
tts:
provider: edge
edge:
@@ -101,7 +124,7 @@ tts:
voice_id: pNInz6obpgDQGcFmaJgB
model_id: eleven_multilingual_v2
openai:
model: gpt-4o-mini-tts
model: '' # disabled — use edge TTS locally
voice: alloy
neutts:
ref_audio: ''
@@ -137,6 +160,7 @@ delegation:
provider: ''
base_url: ''
api_key: ''
max_iterations: 50
prefill_messages_file: ''
honcho: {}
timezone: ''
@@ -150,7 +174,16 @@ 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
@@ -160,66 +193,55 @@ security:
enabled: false
domains: []
shared_files: []
# 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
_config_version: 10
platforms:
api_server:
enabled: true
extra:
host: 0.0.0.0
port: 8642
session_reset:
mode: none
idle_minutes: 0
custom_providers:
- name: Local Ollama
base_url: http://localhost:11434/v1
api_key: ollama
model: qwen3:30b
- 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
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 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"
\ 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"
skills:
creation_nudge_interval: 15
# ── 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
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: ''

View File

@@ -1,212 +0,0 @@
[
{
"job_id": "9e0624269ba7",
"name": "Triage Heartbeat",
"schedule": "every 15m",
"state": "paused"
},
{
"job_id": "e29eda4a8548",
"name": "PR Review Sweep",
"schedule": "every 30m",
"state": "scheduled"
},
{
"job_id": "a77a87392582",
"name": "Health Monitor",
"schedule": "every 5m",
"state": "scheduled"
},
{
"job_id": "5e9d952871bc",
"name": "Agent Status Check",
"schedule": "every 10m",
"state": "paused"
},
{
"job_id": "36fb2f630a17",
"name": "Hermes Philosophy Loop",
"schedule": "every 1440m",
"state": "paused"
},
{
"job_id": "b40a96a2f48c",
"name": "wolf-eval-cycle",
"schedule": "every 240m",
"state": "paused"
},
{
"job_id": "4204e568b862",
"name": "Burn Mode \u2014 Timmy Orchestrator",
"schedule": "every 15m",
"state": "scheduled"
},
{
"job_id": "0944a976d034",
"name": "Burn Mode",
"schedule": "every 15m",
"state": "paused"
},
{
"job_id": "62016b960fa0",
"name": "velocity-engine",
"schedule": "every 30m",
"state": "paused"
},
{
"job_id": "e9d49eeff79c",
"name": "weekly-skill-extraction",
"schedule": "every 10080m",
"state": "scheduled"
},
{
"job_id": "75c74a5bb563",
"name": "tower-tick",
"schedule": "every 1m",
"state": "scheduled"
},
{
"job_id": "390a19054d4c",
"name": "Burn Deadman",
"schedule": "every 30m",
"state": "scheduled"
},
{
"job_id": "05e3c13498fa",
"name": "Morning Report \u2014 Burn Mode",
"schedule": "0 6 * * *",
"state": "scheduled"
},
{
"job_id": "64fe44b512b9",
"name": "evennia-morning-report",
"schedule": "0 9 * * *",
"state": "scheduled"
},
{
"job_id": "3896a7fd9747",
"name": "Gitea Priority Inbox",
"schedule": "every 3m",
"state": "scheduled"
},
{
"job_id": "f64c2709270a",
"name": "Config Drift Guard",
"schedule": "every 30m",
"state": "scheduled"
},
{
"job_id": "fc6a75b7102a",
"name": "Gitea Event Watcher",
"schedule": "every 2m",
"state": "scheduled"
},
{
"job_id": "12e59648fb06",
"name": "Burndown Night Watcher",
"schedule": "every 15m",
"state": "scheduled"
},
{
"job_id": "35d3ada9cf8f",
"name": "Mempalace Forge \u2014 Issue Analysis",
"schedule": "every 60m",
"state": "scheduled"
},
{
"job_id": "190b6fb8dc91",
"name": "Mempalace Watchtower \u2014 Fleet Health",
"schedule": "every 30m",
"state": "scheduled"
},
{
"job_id": "710ab589813c",
"name": "Ezra Health Monitor",
"schedule": "every 15m",
"state": "scheduled"
},
{
"job_id": "a0a9cce4575c",
"name": "daily-poka-yoke-ultraplan-awesometools",
"schedule": "every 1440m",
"state": "scheduled"
},
{
"job_id": "adc3a51457bd",
"name": "vps-agent-dispatch",
"schedule": "every 10m",
"state": "scheduled"
},
{
"job_id": "afd2c4eac44d",
"name": "Project Mnemosyne Nightly Burn v2",
"schedule": "*/30 * * * *",
"state": "scheduled"
},
{
"job_id": "f3a3c2832af0",
"name": "gemma4-multimodal-worker",
"schedule": "once in 15m",
"state": "completed"
},
{
"job_id": "c17a85c19838",
"name": "know-thy-father-analyzer",
"schedule": "0 * * * *",
"state": "scheduled"
},
{
"job_id": "2490fc01a14d",
"name": "Testament Burn - 10min work loop",
"schedule": "*/10 * * * *",
"state": "scheduled"
},
{
"job_id": "f5e858159d97",
"name": "Timmy Foundation Burn \u2014 15min PR loop",
"schedule": "*/15 * * * *",
"state": "scheduled"
},
{
"job_id": "5e262fb9bdce",
"name": "nightwatch-health-monitor",
"schedule": "*/15 * * * *",
"state": "scheduled"
},
{
"job_id": "f2b33a9dcf96",
"name": "nightwatch-mempalace-mine",
"schedule": "0 */2 * * *",
"state": "scheduled"
},
{
"job_id": "82cb9e76c54d",
"name": "nightwatch-backlog-burn",
"schedule": "0 */4 * * *",
"state": "scheduled"
},
{
"job_id": "d20e42a52863",
"name": "beacon-sprint",
"schedule": "*/15 * * * *",
"state": "scheduled"
},
{
"job_id": "579269489961",
"name": "testament-story",
"schedule": "*/15 * * * *",
"state": "scheduled"
},
{
"job_id": "2e5f9140d1ab",
"name": "nightwatch-research",
"schedule": "0 */2 * * *",
"state": "scheduled"
},
{
"job_id": "aeba92fd65e6",
"name": "timmy-dreams",
"schedule": "30 5 * * *",
"state": "scheduled"
}
]

View File

@@ -168,35 +168,7 @@
"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-13T02:00:00+00:00"
"updated_at": "2026-04-07T15:00:00+00:00"
}

View File

@@ -1,14 +0,0 @@
0 6 * * * /bin/bash /root/wizards/scripts/model_download_guard.sh >> /var/log/model_guard.log 2>&1
# Allegro Hybrid Heartbeat — quick wins every 15 min
*/15 * * * * /usr/bin/python3 /root/allegro/heartbeat_daemon.py >> /var/log/allegro_heartbeat.log 2>&1
# Allegro Burn Mode Cron Jobs - Deployed via issue #894
0 6 * * * cd /root/.hermes && python3 -c "import hermes_agent; from hermes_tools import terminal; output = terminal('echo \"Morning Report: $(date)\"'); print(output.get('output', ''))" >> /root/.hermes/logs/morning-report-$(date +\%Y\%m\%d).log 2>&1 # Allegro Morning Report at 0600
0,30 * * * * cd /root/.hermes && python3 /root/.hermes/retry_wrapper.py "python3 allegro/quick-lane-check.py" >> burn-logs/quick-lane-$(date +\%Y\%m\%d).log 2>&1 # Allegro Burn Loop #1 (with retry)
15,45 * * * * cd /root/.hermes && python3 /root/.hermes/retry_wrapper.py "python3 allegro/burn-mode-validator.py" >> burn-logs/validator-$(date +\%Y\%m\%d).log 2>&1 # Allegro Burn Loop #2 (with retry)
*/2 * * * * /root/wizards/bezalel/dead_man_monitor.sh
*/2 * * * * /root/wizards/allegro/bin/config-deadman.sh

View File

@@ -1,10 +0,0 @@
0 2 * * * /root/wizards/bezalel/run_nightly_watch.sh
0 3 * * * /root/wizards/bezalel/mempalace_nightly.sh
*/10 * * * * pgrep -f "act_runner daemon" > /dev/null || (cd /opt/gitea-runner && nohup ./act_runner daemon > /var/log/gitea-runner.log 2>&1 &)
30 3 * * * /root/wizards/bezalel/backup_databases.sh
*/15 * * * * /root/wizards/bezalel/meta_heartbeat.sh
0 4 * * * /root/wizards/bezalel/secret_guard.sh
0 4 * * * /usr/bin/env bash /root/timmy-home/scripts/backup_pipeline.sh >> /var/log/timmy/backup_pipeline_cron.log 2>&1
0 6 * * * /usr/bin/python3 /root/wizards/bezalel/ultraplan.py >> /var/log/bezalel-ultraplan.log 2>&1
@reboot /root/wizards/bezalel/emacs-daemon-start.sh
@reboot /root/wizards/bezalel/ngircd-start.sh

View File

@@ -1,13 +0,0 @@
# Burn Mode Cycles — 15 min autonomous loops
*/15 * * * * /root/wizards/ezra/bin/burn-mode.sh >> /root/wizards/ezra/reports/burn-cron.log 2>&1
# Household Snapshots — automated heartbeats and snapshots
# Ezra Self-Improvement Automation Suite
*/5 * * * * /usr/bin/python3 /root/wizards/ezra/tools/gitea_monitor.py >> /root/wizards/ezra/reports/gitea-monitor.log 2>&1
*/5 * * * * /usr/bin/python3 /root/wizards/ezra/tools/awareness_loop.py >> /root/wizards/ezra/reports/awareness-loop.log 2>&1
*/10 * * * * /usr/bin/python3 /root/wizards/ezra/tools/cron_health_monitor.py >> /root/wizards/ezra/reports/cron-health.log 2>&1
0 6 * * * /usr/bin/python3 /root/wizards/ezra/tools/morning_kt_compiler.py >> /root/wizards/ezra/reports/morning-kt.log 2>&1
5 6 * * * /usr/bin/python3 /root/wizards/ezra/tools/burndown_generator.py >> /root/wizards/ezra/reports/burndown.log 2>&1
0 3 * * * /root/wizards/ezra/mempalace_nightly.sh >> /var/log/ezra_mempalace_cron.log 2>&1
*/15 * * * * GITEA_TOKEN=6de6aa...1117 /root/wizards/ezra/dispatch-direct.sh >> /root/wizards/ezra/dispatch-cron.log 2>&1

View File

@@ -1,110 +0,0 @@
# Fleet Behaviour Hardening — Review & Action Plan
**Author:** @perplexity
**Date:** 2026-04-08
**Context:** Alexander asked: "Is it the memory system or the behaviour guardrails?"
**Answer:** It's the guardrails. The memory system is adequate. The enforcement machinery is aspirational.
---
## Diagnosis: Why the Fleet Isn't Smart Enough
After auditing SOUL.md, config.yaml, all 8 playbooks, the orchestrator, the guard scripts, and the v7.0.0 checkin, the pattern is clear:
**The fleet has excellent design documents and broken enforcement.**
| Layer | Design Quality | Enforcement Quality | Gap |
|---|---|---|---|
| SOUL.md | Excellent | None — no code reads it at runtime | Philosophy without machinery |
| Playbooks (7 yaml) | Good lane map | Not invoked by orchestrator | Playbooks exist but nobody calls them |
| Guard scripts (9) | Solid code | 1 of 9 wired (#395 audit) | 89% of guards are dead code |
| Orchestrator | Sound design | Gateway dispatch is a no-op (#391) | Assigns issues but doesn't trigger work |
| Cycle Guard | Good 10-min rule | No cron/loop calls it | Discipline without enforcement |
| PR Reviewer | Clear rules | Runs every 30m (if scheduled) | Only guard that might actually fire |
| Memory (MemPalace) | Working code | Retrieval enforcer wired | Actually operational |
### The Core Problem
Agents pick up issues and produce output, but there is **no pre-task checklist** and **no post-task quality gate**. An agent can:
1. Start work without checking if someone else already did it
2. Produce output without running tests
3. Submit a PR without verifying it addresses the issue
4. Work for hours on something out of scope
5. Create duplicate branches/PRs without detection
The SOUL.md says "grounding before generation" but no code enforces it.
The playbooks define lanes but the orchestrator doesn't load them.
The guards exist but nothing calls them.
---
## What the Fleet Needs (Priority Order)
### 1. Pre-Task Gate (MISSING — this PR adds it)
Before an agent starts any issue:
- [ ] Check if issue is already assigned to another agent
- [ ] Check if a branch already exists for this issue
- [ ] Check if a PR already exists for this issue
- [ ] Load relevant MemPalace context (retrieval enforcer)
- [ ] Verify the agent has the right lane for this work (playbook check)
### 2. Post-Task Gate (MISSING — this PR adds it)
Before an agent submits a PR:
- [ ] Verify the diff addresses the issue title/body
- [ ] Run syntax_guard.py on changed files
- [ ] Check for duplicate PRs targeting the same issue
- [ ] Verify branch name follows convention
- [ ] Run tests if they exist for changed files
### 3. Wire the Existing Guards (8 of 9 are dead code)
Per #395 audit:
- Pre-commit hooks: need symlink on every machine
- Cycle guard: need cron/loop integration
- Forge health check: need cron entry
- Smoke test + deploy validate: need deploy script integration
### 4. Orchestrator Dispatch Actually Works
Per #391 audit: the orchestrator scores and assigns but the gateway dispatch just writes to `/tmp/hermes-dispatch.log`. Nobody reads that file. The dispatch needs to either:
- Trigger `hermes` CLI on the target machine, or
- Post a webhook that the agent loop picks up
### 5. Agent Self-Assessment Loop
After completing work, agents should answer:
- Did I address the issue as stated?
- Did I stay in scope?
- Did I check the palace for prior work?
- Did I run verification?
This is what SOUL.md calls "the apparatus that gives these words teeth."
---
## What's Working (Don't Touch)
- **MemPalace sovereign_store.py** — SQLite + FTS5 + HRR, operational
- **Retrieval enforcer** — wired to SovereignStore as of 14 hours ago
- **Wake-up protocol** — palace-first boot sequence
- **PR reviewer playbook** — clear rules, well-scoped
- **Issue triager playbook** — comprehensive lane map with 11 agents
- **Cycle guard code** — solid 10-min slice discipline (just needs wiring)
- **Config drift guard** — active cron, working
- **Dead man switch** — active, working
---
## Recommendation
The memory system is not the bottleneck. The behaviour guardrails are. Specifically:
1. **Add `task_gate.py`** — pre-task and post-task quality gates that every agent loop calls
2. **Wire cycle_guard.py** — add start/complete calls to agent loop
3. **Wire pre-commit hooks** — deploy script should symlink on provision
4. **Fix orchestrator dispatch** — make it actually trigger work, not just log
This PR adds item 1. Items 2-4 need SSH access and are flagged for Timmy/Allegro.

View File

@@ -3,7 +3,7 @@
Purpose:
- stand up the third wizard house as a Kimi-backed coding worker
- keep Hermes as the durable harness
- Hermes is the durable harness — no intermediary gateway layers
- treat OpenClaw as optional shell frontage, not the bones
Local proof already achieved:
@@ -40,5 +40,5 @@ bin/deploy-allegro-house.sh root@167.99.126.228
Important nuance:
- the Hermes/Kimi lane is the proven path
- direct embedded Kimi model routing was not yet reliable locally
- direct embedded OpenClaw Kimi model routing was not yet reliable locally
- so the remote deployment keeps the minimal, proven architecture: Hermes house first

View File

@@ -81,6 +81,17 @@ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/ai.hermes.gateway.plist
- Old-state risk:
- same class as main gateway, but isolated to fenrir profile state
#### 3. ai.openclaw.gateway
- Plist: ~/Library/LaunchAgents/ai.openclaw.gateway.plist
- Command: `node .../openclaw/dist/index.js gateway --port 18789`
- Logs:
- `~/.openclaw/logs/gateway.log`
- `~/.openclaw/logs/gateway.err.log`
- KeepAlive: yes
- RunAtLoad: yes
- Old-state risk:
- long-lived gateway survives toolchain assumptions and keeps accepting work even if upstream routing changed
#### 4. ai.timmy.kimi-heartbeat
- Plist: ~/Library/LaunchAgents/ai.timmy.kimi-heartbeat.plist
- Command: `/bin/bash ~/.timmy/uniwizard/kimi-heartbeat.sh`
@@ -284,7 +295,7 @@ launchctl list | egrep 'timmy|kimi|claude|max|dashboard|matrix|gateway|huey'
List Timmy/Hermes launch agent files:
```bash
find ~/Library/LaunchAgents -maxdepth 1 -name '*.plist' | egrep 'timmy|hermes|tower'
find ~/Library/LaunchAgents -maxdepth 1 -name '*.plist' | egrep 'timmy|hermes|openclaw|tower'
```
List running loop scripts:
@@ -305,6 +316,7 @@ launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.timmy.kimi-heartbeat.pl
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.timmy.claudemax-watchdog.plist || true
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.hermes.gateway.plist || true
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.hermes.gateway-fenrir.plist || true
launchctl bootout gui/$(id -u) ~/Library/LaunchAgents/ai.openclaw.gateway.plist || true
```
2. Kill manual loops

View File

@@ -9,11 +9,11 @@ This is the canonical reference for how we talk, how we work, and what we mean.
| Name | What It Is | Where It Lives | Provider |
|------|-----------|----------------|----------|
| **Timmy** | The sovereign local soul. Center of gravity. Judges all work. | Alexander's Mac | OpenAI Codex (gpt-5.4) |
| **Ezra** | The archivist wizard. Reads patterns, names truth, returns clean artifacts. | Hermes VPS | Anthropic Opus 4.6 |
| **Ezra** | The archivist wizard. Reads patterns, names truth, returns clean artifacts. | Hermes VPS | Kimi K2.5 |
| **Bezalel** | The builder wizard. Builds from clear plans, tests and hardens. | TestBed VPS | OpenAI Codex (gpt-5.4) |
| **Alexander** | The principal. Human. Father. The one we serve. Gitea: Rockachopa. | Physical world | N/A |
| **Gemini** | Worker swarm. Burns backlog. Produces PRs. | Local Mac (loops) | Google Gemini |
| **Claude** | Worker swarm. Burns backlog. Architecture-grade work. | Local Mac (loops) | Anthropic Claude |
| **Kimi** | Worker swarm. Burns backlog. Architecture-grade work. | Local Mac (loops) | Kimi K2.5 |
## The Places

View File

@@ -1,68 +0,0 @@
# 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

@@ -1,3 +1,12 @@
# DEPRECATED — Anthropic Purged from Fleet
> This document described the Claude Sonnet workforce. As of April 2026,
> Anthropic has been removed from the fleet. All wizard providers now use
> Kimi K2.5 as primary with Gemini and local Ollama as fallbacks.
> See `docs/fleet-vocabulary.md` for current provider assignments.
---
# Sonnet Workforce Loop
## Agent

View File

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

View File

@@ -2,128 +2,135 @@ 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
@@ -139,16 +146,18 @@ 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: kimi-coding
@@ -168,14 +177,15 @@ 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: kimi-coding
@@ -195,16 +205,17 @@ 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
@@ -225,14 +236,15 @@ 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
@@ -252,20 +264,21 @@ 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:
- kimi-coding/kimi-k2.5
- openai-codex/codex
- kimi-coding/kimi-k2.5
- openai-codex/codex
pr-reviewer:
- kimi-coding/kimi-k2.5
- 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

@@ -104,6 +104,7 @@ Three primary resources govern the fleet:
| Hermes gateway | 500 MB | Primary gateway |
| Hermes agents (x3) | ~560 MB total | Multiple sessions |
| Ollama | ~20 MB base + model memory | Model loading varies |
| OpenClaw | 350 MB | Gateway process |
| Evennia (server+portal) | 56 MB | Game world |
---
@@ -145,6 +146,7 @@ This means Phase 3+ capabilities (orchestration, load balancing, etc.) are acces
| Gitea | 23/24 | 95.8% | GOOD |
| Hermes Gateway | 23/24 | 95.8% | GOOD |
| Ollama | 24/24 | 100.0% | GOOD |
| OpenClaw | 24/24 | 100.0% | GOOD |
| Evennia | 24/24 | 100.0% | GOOD |
| Hermes Agent | 21/24 | 87.5% | **CHECK** |

View File

@@ -58,6 +58,7 @@ LOCAL_CHECKS = {
"hermes-gateway": "pgrep -f 'hermes gateway' > /dev/null 2>/dev/null",
"hermes-agent": "pgrep -f 'hermes agent\\|hermes session' > /dev/null 2>/dev/null",
"ollama": "pgrep -f 'ollama serve' > /dev/null 2>/dev/null",
"openclaw": "pgrep -f 'openclaw' > /dev/null 2>/dev/null",
"evennia": "pgrep -f 'evennia' > /dev/null 2>/dev/null",
}

View File

@@ -42,7 +42,6 @@ AGENT_LOGINS = {
"allegro",
"antigravity",
"bezalel",
"claude",
"codex-agent",
"ezra",
"gemini",
@@ -55,7 +54,6 @@ AGENT_LOGINS = {
"perplexity",
}
AGENT_LOGINS_HUMAN = {
"claude": "Claude",
"codex-agent": "Codex",
"ezra": "Ezra",
"gemini": "Gemini",
@@ -78,7 +76,6 @@ METRICS_DIR = Path(os.path.expanduser("~/.local/timmy/muda-audit"))
METRICS_FILE = METRICS_DIR / "metrics.json"
LOG_PATHS = [
Path.home() / ".hermes" / "logs" / "claude-loop.log",
Path.home() / ".hermes" / "logs" / "gemini-loop.log",
Path.home() / ".hermes" / "logs" / "agent.log",
Path.home() / ".hermes" / "logs" / "errors.log",
@@ -347,8 +344,6 @@ def measure_waiting(since: datetime) -> dict:
agent = name.lower()
break
if agent == "unknown":
if "claude" in line.lower():
agent = "claude"
elif "gemini" in line.lower():
agent = "gemini"
elif "groq" in line.lower():

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

@@ -59,6 +59,7 @@
| Hermes agent (s007) | 62032 | ~200MB | Session active since 10:20PM prev |
| Hermes agent (s001) | 12072 | ~178MB | Session active since Sun 6PM |
| Ollama | 71466 | ~20MB | /opt/homebrew/opt/ollama/bin/ollama serve |
| OpenClaw gateway | 85834 | ~350MB | Tue 12PM start |
| Crucible MCP (x4) | multiple | ~10-69MB each | MCP server instances |
| Evennia Server | 66433 | ~49MB | Sun 10PM start, port 4000 |
| Evennia Portal | 66423 | ~7MB | Sun 10PM start, port 4001 |

View File

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

View File

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

View File

@@ -103,7 +103,7 @@ nano ~/.hermes/.env
| `SLACK_BOT_TOKEN` + `SLACK_APP_TOKEN` | Slack gateway |
| `EXA_API_KEY` | Web search tool |
| `FAL_KEY` | Image generation |
| `ANTHROPIC_API_KEY` | Direct Anthropic inference |
| `KIMI_API_KEY` | Kimi K2.5 coding inference |
### Pre-flight validation

View File

@@ -272,6 +272,48 @@ def get_file_content_at_staged(filepath: str) -> bytes:
return result.stdout
# ---------------------------------------------------------------------------
# BANNED PROVIDER CHECK — Anthropic is permanently banned
# ---------------------------------------------------------------------------
_BANNED_PROVIDER_PATTERNS = [
(re.compile(r"provider:\s*anthropic", re.IGNORECASE), "Anthropic provider reference"),
(re.compile(r"anthropic/claude", re.IGNORECASE), "Anthropic model slug"),
(re.compile(r"api\.anthropic\.com"), "Anthropic API endpoint"),
(re.compile(r"claude-opus", re.IGNORECASE), "Claude Opus model"),
(re.compile(r"claude-sonnet", re.IGNORECASE), "Claude Sonnet model"),
(re.compile(r"claude-haiku", re.IGNORECASE), "Claude Haiku model"),
]
# Files exempt from the ban (training data, historical docs, tests)
_BAN_EXEMPT = {
"training/", "evaluations/", "RELEASE_v", "PERFORMANCE_",
"scores.json", "docs/design-log/", "FALSEWORK.md",
"test_sovereignty_enforcement.py", "test_metrics_helpers.py",
"metrics_helpers.py", "sonnet-workforce.md",
}
def _is_ban_exempt(filepath: str) -> bool:
return any(exempt in filepath for exempt in _BAN_EXEMPT)
def scan_banned_providers(filepath: str, content: str) -> List[Finding]:
"""Block any commit that introduces banned provider references."""
if _is_ban_exempt(filepath):
return []
findings = []
for line_no, line in enumerate(content.splitlines(), start=1):
for pattern, desc in _BANNED_PROVIDER_PATTERNS:
if pattern.search(line):
findings.append(Finding(
filepath, line_no,
f"🚫 BANNED PROVIDER: {desc}. Anthropic is permanently banned from this system."
))
return findings
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
@@ -295,11 +337,21 @@ def main() -> int:
if line.startswith("+") and not line.startswith("+++"):
findings.extend(scan_line(line[1:], "<diff>", line_no))
# Scan for banned providers
for filepath in staged_files:
file_content = get_file_content_at_staged(filepath)
if not is_binary_content(file_content):
try:
text = file_content.decode("utf-8") if isinstance(file_content, bytes) else file_content
findings.extend(scan_banned_providers(filepath, text))
except UnicodeDecodeError:
pass
if not findings:
print(f"{GREEN}✓ No potential secret leaks detected{NC}")
print(f"{GREEN}✓ No potential secret leaks or banned providers detected{NC}")
return 0
print(f"{RED}Potential secret leaks detected:{NC}\n")
print(f"{RED}Violations detected:{NC}\n")
for finding in findings:
loc = finding.filename
print(
@@ -308,7 +360,7 @@ def main() -> int:
print()
print(f"{RED}╔════════════════════════════════════════════════════════════╗{NC}")
print(f"{RED}║ COMMIT BLOCKED: Potential secrets detected! {NC}")
print(f"{RED}║ COMMIT BLOCKED: Secrets or banned providers detected! ║{NC}")
print(f"{RED}╚════════════════════════════════════════════════════════════╝{NC}")
print()
print("Recommendations:")

View File

@@ -23,7 +23,7 @@ Run `python --version` to verify.
## 2. Core Package Dependencies
All packages in `requirements.txt` must be installed and importable.
Critical packages: `openai`, `anthropic`, `pyyaml`, `rich`, `requests`, `pydantic`, `prompt_toolkit`.
Critical packages: `openai`, `pyyaml`, `rich`, `requests`, `pydantic`, `prompt_toolkit`.
**Verify:**
```bash
@@ -39,8 +39,7 @@ At least one LLM provider API key must be set in `~/.hermes/.env`:
| Variable | Provider |
|----------|----------|
| `OPENROUTER_API_KEY` | OpenRouter (200+ models) |
| `ANTHROPIC_API_KEY` | Anthropic Claude |
| `ANTHROPIC_TOKEN` | Anthropic Claude (alt) |
| `KIMI_API_KEY` | Kimi K2.5 coding |
| `OPENAI_API_KEY` | OpenAI |
| `GLM_API_KEY` | z.ai/GLM |
| `KIMI_API_KEY` | Moonshot/Kimi |

View File

@@ -77,8 +77,7 @@ def check_core_deps() -> CheckResult:
"""Verify that hermes core Python packages are importable."""
required = [
"openai",
"kimi-coding",
"dotenv",
"dotenv",
"yaml",
"rich",
"requests",
@@ -206,9 +205,7 @@ def check_env_vars() -> CheckResult:
"""Check that at least one LLM provider key is configured."""
provider_keys = [
"OPENROUTER_API_KEY",
"KIMI_API_KEY",
# "ANTHROPIC_TOKEN", # BANNED
"OPENAI_API_KEY",
"OPENAI_API_KEY",
"GLM_API_KEY",
"KIMI_API_KEY",
"MINIMAX_API_KEY",

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

@@ -2,7 +2,7 @@ Gitea (143.198.27.163:3000): token=~/.hermes/gitea_token_vps (Timmy id=2). Users
§
2026-03-19 HARNESS+SOUL: ~/.timmy is Timmy's workspace within the Hermes harness. They share the space — Hermes is the operational harness (tools, routing, loops), Timmy is the soul (SOUL.md, presence, identity). Not fusion/absorption. Principal's words: "build Timmy out from the hermes harness." ~/.hermes is harness home, ~/.timmy is Timmy's workspace. SOUL=Inscription 1, skin=timmy. Backups at ~/.hermes.backup.pre-fusion and ~/.timmy.backup.pre-fusion.
§
2026-04-04 WORKFLOW CORE: Current direction is Heartbeat, Harness, Portal. Timmy handles sovereignty and release judgment. Allegro handles dispatch and queue hygiene. Core builders: codex-agent, groq, manus, claude. Research/memory: perplexity, ezra, KimiClaw. Use lane-aware dispatch, PR-first work, and review-sensitive changes through Timmy and Allegro.
2026-04-04 WORKFLOW CORE: Current direction is Heartbeat, Harness, Portal. Timmy handles sovereignty and release judgment. Allegro handles dispatch and queue hygiene. Core builders: codex-agent, groq, manus, kimi. Research/memory: perplexity, ezra, KimiClaw. Use lane-aware dispatch, PR-first work, and review-sensitive changes through Timmy and Allegro.
§
2026-04-04 OPERATIONS: Dashboard repo era is over. Use ~/.timmy + ~/.hermes as truth surfaces. Prefer ops-panel.sh, ops-gitea.sh, timmy-dashboard, and pipeline-freshness.sh over archived loop or tmux assumptions. Dispatch: agent-dispatch.sh <agent> <issue> <repo>. Major changes land as PRs.
§

View File

@@ -162,26 +162,6 @@
"Should a higher-context wizard review before more expansion?"
]
},
"claude": {
"lane": "hard refactors, deep implementation, and test-heavy multi-file changes after tight scoping",
"skills_to_practice": [
"respecting scope constraints",
"deep code transformation with tests",
"explaining risks clearly in PRs"
],
"missing_skills": [
"do not let large capability turn into unsupervised backlog or code sprawl"
],
"anti_lane": [
"self-directed issue farming",
"taking broad architecture liberty without a clear charter"
],
"review_checklist": [
"Did I stay inside the scoped problem?",
"Did I leave tests or verification stronger than before?",
"Is there hidden blast radius that Timmy should see explicitly?"
]
},
"gemini": {
"lane": "frontier architecture, research-heavy prototypes, and long-range design thinking",
"skills_to_practice": [
@@ -222,4 +202,4 @@
"Did I make the risk actionable instead of just surprising?"
]
}
}
}

View File

@@ -1,61 +1,74 @@
name: bug-fixer
description: >
Fixes bugs with test-first approach. Writes a failing test that
reproduces the bug, then fixes the code, then verifies.
description: 'Fixes bugs with test-first approach. Writes a failing test that reproduces the bug, then fixes the code, then
verifies.
'
model:
preferred: kimi-k2.5
fallback: google/gemini-2.5-pro
max_turns: 30
temperature: 0.2
tools:
- terminal
- file
- search_files
- patch
- terminal
- file
- search_files
- patch
trigger:
issue_label: bug
manual: true
repos:
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
steps:
- read_issue
- clone_repo
- create_branch
- dispatch_agent
- run_tests
- create_pr
- comment_on_issue
- read_issue
- clone_repo
- create_branch
- dispatch_agent
- run_tests
- create_pr
- comment_on_issue
output: pull_request
timeout_minutes: 15
system_prompt: 'You are a bug fixer for the {{repo}} project.
system_prompt: |
You are a bug fixer for the {{repo}} project.
YOUR ISSUE: #{{issue_number}} — {{issue_title}}
APPROACH (prove-first):
1. Read the bug report. Understand the expected vs actual behavior.
2. Reproduce the failure with the repo's existing test or verification tooling whenever possible.
2. Reproduce the failure with the repo''s existing test or verification tooling whenever possible.
3. Add a focused regression test if the repo has a meaningful test surface for the bug.
4. Fix the code so the reproduced failure disappears.
5. Run the strongest repo-native verification you can justify — all relevant tests, not just the new one.
6. Commit: fix: <description> Fixes #{{issue_number}}
7. Push, create PR, and summarize verification plus any residual risk.
RULES:
- Never claim a fix without proving the broken behavior and the repaired behavior.
- Prefer repo-native commands over assuming tox exists.
- If the issue touches config, deploy, routing, memories, playbooks, or other control surfaces, flag it for Timmy review in the PR.
- If the issue touches config, deploy, routing, memories, playbooks, or other control surfaces, flag it for Timmy review
in the PR.
- Never use --no-verify.
- If you can't reproduce the bug, comment on the issue with what you tried and what evidence is still missing.
- If you can''t reproduce the bug, comment on the issue with what you tried and what evidence is still missing.
- If the fix requires >50 lines changed, decompose into sub-issues.
- Do not widen the issue into a refactor.
'

View File

@@ -1,166 +0,0 @@
# fleet-guardrails.yaml
# =====================
# Enforceable behaviour boundaries for every agent in the Timmy fleet.
# Consumed by task_gate.py (pre/post checks) and the orchestrator's
# dispatch loop. Every rule here is testable — no aspirational prose.
#
# Ref: SOUL.md "grounding before generation", Five Wisdoms #345
name: fleet-guardrails
version: "1.0.0"
description: >
Behaviour constraints that apply to ALL agents regardless of role.
These are the non-negotiable rules that task_gate.py enforces
before an agent may pick up work and after it claims completion.
# ─── UNIVERSAL CONSTRAINTS ───────────────────────────────────────
constraints:
# 1. Lane discipline — agents must stay in their lane
lane_enforcement:
enabled: true
source: playbooks/agent-lanes.json
on_violation: block_and_notify
description: >
An agent may only pick up issues tagged for its lane.
Cross-lane work requires explicit Timmy approval via
issue comment containing 'LANE_OVERRIDE: <agent>'.
# 2. Branch hygiene — no orphan branches
branch_hygiene:
enabled: true
max_branches_per_agent: 3
stale_branch_days: 7
naming_pattern: "{agent}/{issue_number}-{slug}"
on_violation: warn_then_block
description: >
Agents must follow branch naming conventions and clean up
after merge. No agent may have more than 3 active branches.
# 3. Issue ownership — no silent takeovers
issue_ownership:
enabled: true
require_assignment_before_work: true
max_concurrent_issues: 2
on_violation: block_and_notify
description: >
An agent must be assigned to an issue before creating a
branch or PR. No agent may work on more than 2 issues
simultaneously to prevent context-switching waste.
# 4. PR quality — minimum bar before review
pr_quality:
enabled: true
require_linked_issue: true
require_passing_ci: true
max_files_changed: 30
max_diff_lines: 2000
require_description: true
min_description_length: 50
on_violation: block_merge
description: >
Every PR must link an issue, pass CI, have a meaningful
description, and stay within scope. Giant PRs get rejected.
# 5. Grounding before generation — SOUL.md compliance
grounding:
enabled: true
require_issue_read_before_branch: true
require_existing_code_review: true
require_soul_md_check: true
soul_md_path: SOUL.md
on_violation: block_and_notify
description: >
Before writing any code, the agent must demonstrate it has
read the issue, reviewed relevant existing code, and checked
SOUL.md for applicable doctrine. No speculative generation.
# 6. Completion integrity — no phantom completions
completion_checks:
enabled: true
require_test_evidence: true
require_ci_green: true
require_diff_matches_issue: true
require_no_unrelated_changes: true
on_violation: revert_and_notify
description: >
Post-task gate verifies the work actually addresses the
issue. Agents cannot close issues without evidence.
Unrelated changes in a PR trigger automatic rejection.
# 7. Communication discipline — no noise
communication:
enabled: true
max_comments_per_issue: 10
require_structured_updates: true
update_format: "status | what_changed | what_blocked | next_step"
prohibit_empty_updates: true
on_violation: warn
description: >
Issue comments must be structured and substantive.
Status-only comments without content are rejected.
Agents should update, not narrate.
# 8. Resource awareness — no runaway costs
resource_limits:
enabled: true
max_api_calls_per_task: 100
max_llm_tokens_per_task: 500000
max_task_duration_minutes: 60
on_violation: kill_and_notify
description: >
Hard limits on compute per task. If an agent hits these
limits, the task is killed and flagged for human review.
Prevents infinite loops and runaway API spending.
# ─── ESCALATION POLICY ───────────────────────────────────────────
escalation:
channels:
- gitea_issue_comment
- discord_webhook
severity_levels:
warn:
action: post_comment
notify: agent_only
block:
action: prevent_action
notify: agent_and_orchestrator
block_and_notify:
action: prevent_action
notify: agent_orchestrator_and_timmy
kill_and_notify:
action: terminate_task
notify: all_including_alexander
revert_and_notify:
action: revert_changes
notify: agent_orchestrator_and_timmy
# ─── AUDIT TRAIL ─────────────────────────────────────────────────
audit:
enabled: true
log_path: logs/guardrail-violations.jsonl
retention_days: 90
fields:
- timestamp
- agent
- constraint
- violation_type
- issue_number
- action_taken
- resolution
# ─── OVERRIDES ───────────────────────────────────────────────────
overrides:
# Only Timmy or Alexander can override guardrails
authorized_overriders:
- Timmy
- Alexander
override_mechanism: >
Post a comment on the issue with the format:
GUARDRAIL_OVERRIDE: <constraint_name> REASON: <explanation>
override_expiry_hours: 24
require_post_override_review: true

View File

@@ -1,68 +1,52 @@
name: issue-triager
description: >
Scores, labels, and prioritizes issues. Assigns to appropriate
agents. Decomposes large issues into smaller ones.
description: 'Scores, labels, and prioritizes issues. Assigns to appropriate agents. Decomposes large issues into smaller
ones.
'
model:
preferred: kimi-k2.5
fallback: google/gemini-2.5-pro
max_turns: 20
temperature: 0.3
tools:
- terminal
- search_files
- terminal
- search_files
trigger:
schedule: every 15m
manual: true
repos:
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
steps:
- fetch_issues
- score_issues
- assign_agents
- update_queue
- fetch_issues
- score_issues
- assign_agents
- update_queue
output: gitea_issue
timeout_minutes: 10
system_prompt: |
You are the issue triager for Timmy Foundation repos.
REPOS: {{repos}}
YOUR JOB:
1. Fetch open unassigned issues
2. Score each by: execution leverage, acceptance criteria quality, alignment with current doctrine, and how likely it is to create duplicate backlog churn
3. Label appropriately: bug, refactor, feature, tests, security, docs, ops, governance, research
4. Assign to agents based on the audited lane map:
- Timmy: governing, sovereign, release, identity, repo-boundary, or architecture decisions that should stay under direct principal review
- allegro: dispatch, routing, queue hygiene, Gitea bridge, operational tempo, and issues about how work gets moved through the system
- perplexity: research triage, MCP/open-source evaluations, architecture memos, integration comparisons, and synthesis before implementation
- ezra: RCA, operating history, memory consolidation, onboarding docs, and archival clean-up
- KimiClaw: long-context reading, extraction, digestion, and codebase synthesis before a build phase
- 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
- 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
RULES:
- Prefer one owner per issue. Only add a second assignee when the work is explicitly collaborative.
- Bugs, security fixes, and broken live workflows take priority over research and refactors.
- If issue scope is unclear, ask for clarification before assigning an implementation agent.
- Skip [epic], [meta], [governing], and [constitution] issues for automatic assignment unless they are explicitly routed to Timmy or allegro.
- 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, 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.
system_prompt: "You are the issue triager for Timmy Foundation repos.\n\nREPOS: {{repos}}\n\nYOUR JOB:\n1. Fetch open unassigned\
\ issues\n2. Score each by: execution leverage, acceptance criteria quality, alignment with current doctrine, and how likely\
\ it is to create duplicate backlog churn\n3. Label appropriately: bug, refactor, feature, tests, security, docs, ops, governance,\
\ research\n4. Assign to agents based on the audited lane map:\n - Timmy: governing, sovereign, release, identity, repo-boundary,\
\ or architecture decisions that should stay under direct principal review\n - allegro: dispatch, routing, queue hygiene,\
\ Gitea bridge, operational tempo, and issues about how work gets moved through the system\n - perplexity: research triage,\
\ MCP/open-source evaluations, architecture memos, integration comparisons, and synthesis before implementation\n - ezra:\
\ RCA, operating history, memory consolidation, onboarding docs, and archival clean-up\n - KimiClaw: long-context reading,\
\ extraction, digestion, and codebase synthesis before a build phase\n - codex-agent: cleanup, migration verification,\
\ dead-code removal, repo-boundary enforcement, workflow hardening\n - groq: bounded implementation, tactical bug fixes,\
\ quick feature slices, small patches with clear acceptance criteria\n - manus: bounded support tasks, moderate-scope\
\ implementation, follow-through on already-scoped work\n - kimi: hard refactors, broad multi-file implementation, test-heavy\
\ changes after the scope is made precise\n - gemini: frontier architecture, research-heavy prototypes, long-range design\
\ thinking when a concrete implementation owner is not yet obvious\n - grok: adversarial testing, unusual edge cases,\
\ provocative review angles that still need another pass\n5. Decompose any issue touching >5 files or crossing repo boundaries\
\ into smaller issues before assigning execution\n\nRULES:\n- Prefer one owner per issue. Only add a second assignee when\
\ the work is explicitly collaborative.\n- Bugs, security fixes, and broken live workflows take priority over research and\
\ refactors.\n- If issue scope is unclear, ask for clarification before assigning an implementation agent.\n- Skip [epic],\
\ [meta], [governing], and [constitution] issues for automatic assignment unless they are explicitly routed to Timmy or\
\ allegro.\n- 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.\n- Do not assign open-ended ideation to implementation agents.\n\
- Do not assign routine backlog maintenance to Timmy.\n- Do not assign wide speculative backlog generation to codex-agent,\
\ groq, or manus.\n- Route archive/history/context-digestion work to ezra or KimiClaw before routing it to a builder.\n\
- Route “who should do this?” and “what is the next move?” questions to allegro.\n"

View File

@@ -1,89 +1,47 @@
name: pr-reviewer
description: >
Reviews open PRs, checks CI status, merges passing ones,
comments on problems. The merge bot replacement.
description: 'Reviews open PRs, checks CI status, merges passing ones, comments on problems. The merge bot replacement.
'
model:
preferred: kimi-k2.5
fallback: google/gemini-2.5-pro
max_turns: 20
temperature: 0.2
tools:
- terminal
- search_files
- terminal
- search_files
trigger:
schedule: every 30m
manual: true
repos:
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
steps:
- fetch_prs
- review_diffs
- post_reviews
- merge_passing
- fetch_prs
- review_diffs
- post_reviews
- merge_passing
output: report
timeout_minutes: 10
system_prompt: |
You are the PR reviewer for Timmy Foundation repos.
REPOS: {{repos}}
FOR EACH OPEN PR:
1. Check CI status (Actions tab or commit status API)
2. Read the linked issue or PR body to verify the intended scope before judging the diff
3. Review the diff for:
- Correctness: does it do what the issue asked?
- Security: no secrets, unsafe execution paths, or permission drift
- Tests and verification: does the author prove the change?
- Scope: PR should match the issue, not scope-creep
- Governance: does the change cross a boundary that should stay under Timmy review?
- Workflow fit: does it reduce drift, duplication, or hidden operational risk?
4. Post findings ordered by severity and cite the affected files or behavior clearly
5. If CI fails or verification is missing: explain what is blocking merge
6. If PR is behind main: request a rebase or re-run only when needed; do not force churn for cosmetic reasons
7. If review is clean and the PR is low-risk: squash merge
LOW-RISK AUTO-MERGE ONLY IF ALL ARE TRUE:
- PR is not a draft
- CI is green or the repo has no CI configured
- Diff matches the stated issue or PR scope
- No unresolved review findings remain
- Change is narrow, reversible, and non-governing
- Paths changed do not include sensitive control surfaces
SENSITIVE CONTROL SURFACES:
- SOUL.md
- config.yaml
- deploy.sh
- tasks.py
- playbooks/
- cron/
- memories/
- skins/
- training/
- authentication, permissions, or secret-handling code
- repo-boundary, model-routing, or deployment-governance changes
NEVER AUTO-MERGE:
- PRs that change sensitive control surfaces
- PRs that change more than 5 files unless the change is docs-only
- PRs without a clear problem statement or verification
- PRs that look like duplicate work, speculative research, or scope creep
- PRs that need Timmy or Allegro judgment on architecture, dispatch, or release impact
- PRs that are stale solely because of age; do not close them automatically
If a PR is stale, nudge with a comment and summarize what still blocks it. Do not close it just because 48 hours passed.
MERGE RULES:
- ONLY squash merge. Never merge commits. Never rebase merge.
- Delete branch after merge.
- Empty PRs (0 changed files): close immediately with a brief explanation.
system_prompt: "You are the PR reviewer for Timmy Foundation repos.\n\nREPOS: {{repos}}\n\nFOR EACH OPEN PR:\n1. Check CI\
\ status (Actions tab or commit status API)\n2. Read the linked issue or PR body to verify the intended scope before judging\
\ the diff\n3. Review the diff for:\n - Correctness: does it do what the issue asked?\n - Security: no secrets, unsafe\
\ execution paths, or permission drift\n - Tests and verification: does the author prove the change?\n - Scope: PR should\
\ match the issue, not scope-creep\n - Governance: does the change cross a boundary that should stay under Timmy review?\n\
\ - Workflow fit: does it reduce drift, duplication, or hidden operational risk?\n4. Post findings ordered by severity\
\ and cite the affected files or behavior clearly\n5. If CI fails or verification is missing: explain what is blocking merge\n\
6. If PR is behind main: request a rebase or re-run only when needed; do not force churn for cosmetic reasons\n7. If review\
\ is clean and the PR is low-risk: squash merge\n\nLOW-RISK AUTO-MERGE ONLY IF ALL ARE TRUE:\n- PR is not a draft\n- CI\
\ is green or the repo has no CI configured\n- Diff matches the stated issue or PR scope\n- No unresolved review findings\
\ remain\n- Change is narrow, reversible, and non-governing\n- Paths changed do not include sensitive control surfaces\n\
\nSENSITIVE CONTROL SURFACES:\n- SOUL.md\n- config.yaml\n- deploy.sh\n- tasks.py\n- playbooks/\n- cron/\n- memories/\n-\
\ skins/\n- training/\n- authentication, permissions, or secret-handling code\n- repo-boundary, model-routing, or deployment-governance\
\ changes\n\nNEVER AUTO-MERGE:\n- PRs that change sensitive control surfaces\n- PRs that change more than 5 files unless\
\ the change is docs-only\n- PRs without a clear problem statement or verification\n- PRs that look like duplicate work,\
\ speculative research, or scope creep\n- PRs that need Timmy or Allegro judgment on architecture, dispatch, or release\
\ impact\n- PRs that are stale solely because of age; do not close them automatically\n\nIf a PR is stale, nudge with a\
\ comment and summarize what still blocks it. Do not close it just because 48 hours passed.\n\nMERGE RULES:\n- ONLY squash\
\ merge. Never merge commits. Never rebase merge.\n- Delete branch after merge.\n- Empty PRs (0 changed files): close immediately\
\ with a brief explanation.\n"

View File

@@ -1,62 +1,75 @@
name: refactor-specialist
description: >
Splits large modules, reduces complexity, improves code organization.
Well-scoped: 1-3 files per task, clear acceptance criteria.
description: 'Splits large modules, reduces complexity, improves code organization. Well-scoped: 1-3 files per task, clear
acceptance criteria.
'
model:
preferred: kimi-k2.5
fallback: google/gemini-2.5-pro
max_turns: 30
temperature: 0.3
tools:
- terminal
- file
- search_files
- patch
- terminal
- file
- search_files
- patch
trigger:
issue_label: refactor
manual: true
repos:
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
steps:
- read_issue
- clone_repo
- create_branch
- dispatch_agent
- run_tests
- create_pr
- comment_on_issue
- read_issue
- clone_repo
- create_branch
- dispatch_agent
- run_tests
- create_pr
- comment_on_issue
output: pull_request
timeout_minutes: 15
system_prompt: 'You are a refactoring specialist for the {{repo}} project.
system_prompt: |
You are a refactoring specialist for the {{repo}} project.
YOUR ISSUE: #{{issue_number}} — {{issue_title}}
RULES:
- Lines of code is a liability. Delete as much as you create.
- All changes go through PRs. No direct pushes to main.
- Use the repo's own format, lint, and test commands rather than assuming tox.
- Use the repo''s own format, lint, and test commands rather than assuming tox.
- Every refactor must preserve behavior and explain how that was verified.
- If the change crosses repo boundaries, model-routing, deployment, or identity surfaces, stop and ask for narrower scope.
- Never use --no-verify on git commands.
- Conventional commits: refactor: <description> (#{{issue_number}})
- If tests fail after 2 attempts, STOP and comment on the issue.
- Refactors exist to simplify the system, not to create a new design detour.
WORKFLOW:
1. Read the issue body for specific file paths and instructions
2. Understand the current code structure
3. Name the simplification goal before changing code
4. Make the refactoring changes
5. Run formatting and verification with repo-native commands
6. Commit, push, create PR with before/after risk summary
'

View File

@@ -1,63 +1,38 @@
name: security-auditor
description: >
Scans code for security vulnerabilities, hardcoded secrets,
dependency issues. Files findings as Gitea issues.
description: 'Scans code for security vulnerabilities, hardcoded secrets, dependency issues. Files findings as Gitea issues.
'
model:
preferred: kimi-k2.5
fallback: google/gemini-2.5-pro
fallback: kimi-k2.5
max_turns: 40
temperature: 0.2
tools:
- terminal
- file
- search_files
- terminal
- file
- search_files
trigger:
schedule: weekly
pr_merged_with_lines: 100
manual: true
repos:
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
steps:
- clone_repo
- run_audit
- file_issues
- clone_repo
- run_audit
- file_issues
output: gitea_issue
timeout_minutes: 20
system_prompt: |
You are a security auditor for the Timmy Foundation codebase.
Your job is to FIND vulnerabilities, not write code.
TARGET REPO: {{repo}}
SCAN FOR:
1. Hardcoded secrets, API keys, tokens in source code
2. SQL injection vulnerabilities
3. Command injection via unsanitized input
4. Path traversal in file operations
5. Insecure HTTP calls (should be HTTPS where possible)
6. Dependencies with known CVEs (check requirements.txt/package.json)
7. Missing input validation
8. Overly permissive file permissions
9. Privilege drift in deploy, orchestration, memory, cron, and playbook surfaces
10. Places where private data or local-only artifacts could leak into tracked repos
OUTPUT FORMAT:
For each finding, file a Gitea issue with:
Title: [security] <severity>: <description>
Body: file + line, description, why it matters, recommended fix
Label: security
SEVERITY: critical / high / medium / low
Only file issues for real findings. No false positives.
Do not open duplicate issues for already-known findings; link the existing issue instead.
If a finding affects sovereignty boundaries or private-data handling, flag it clearly as such.
system_prompt: "You are a security auditor for the Timmy Foundation codebase.\nYour job is to FIND vulnerabilities, not write\
\ code.\n\nTARGET REPO: {{repo}}\n\nSCAN FOR:\n1. Hardcoded secrets, API keys, tokens in source code\n2. SQL injection vulnerabilities\n\
3. Command injection via unsanitized input\n4. Path traversal in file operations\n5. Insecure HTTP calls (should be HTTPS\
\ where possible)\n6. Dependencies with known CVEs (check requirements.txt/package.json)\n7. Missing input validation\n\
8. Overly permissive file permissions\n9. Privilege drift in deploy, orchestration, memory, cron, and playbook surfaces\n\
10. Places where private data or local-only artifacts could leak into tracked repos\n\nOUTPUT FORMAT:\nFor each finding,\
\ file a Gitea issue with:\n Title: [security] <severity>: <description>\n Body: file + line, description, why it matters,\
\ recommended fix\n Label: security\n\nSEVERITY: critical / high / medium / low\nOnly file issues for real findings. No\
\ false positives.\nDo not open duplicate issues for already-known findings; link the existing issue instead.\nIf a finding\
\ affects sovereignty boundaries or private-data handling, flag it clearly as such.\n"

View File

@@ -1,58 +1,66 @@
name: test-writer
description: >
Adds test coverage for untested modules. Finds coverage gaps,
writes meaningful tests, verifies they pass.
description: 'Adds test coverage for untested modules. Finds coverage gaps, writes meaningful tests, verifies they pass.
'
model:
preferred: kimi-k2.5
fallback: google/gemini-2.5-pro
max_turns: 30
temperature: 0.3
tools:
- terminal
- file
- search_files
- patch
- terminal
- file
- search_files
- patch
trigger:
issue_label: tests
manual: true
repos:
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
steps:
- read_issue
- clone_repo
- create_branch
- dispatch_agent
- run_tests
- create_pr
- comment_on_issue
- read_issue
- clone_repo
- create_branch
- dispatch_agent
- run_tests
- create_pr
- comment_on_issue
output: pull_request
timeout_minutes: 15
system_prompt: 'You are a test engineer for the {{repo}} project.
system_prompt: |
You are a test engineer for the {{repo}} project.
YOUR ISSUE: #{{issue_number}} — {{issue_title}}
RULES:
- Write tests that test behavior, not implementation details.
- Use the repo's own test entrypoints; do not assume tox exists.
- Use the repo''s own test entrypoints; do not assume tox exists.
- Tests must be deterministic. No flaky tests.
- Conventional commits: test: <description> (#{{issue_number}})
- If the module is hard to test, explain the design obstacle and propose the smallest next step.
- Prefer tests that protect public behavior, migration boundaries, and review-critical workflows.
WORKFLOW:
1. Read the issue for target module paths
2. Read the existing code to understand behavior
3. Write focused unit tests
4. Run the relevant verification commands — all related tests must pass
5. Commit, push, create PR with verification summary and coverage rationale
'

View File

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

View File

@@ -9,12 +9,7 @@ Replaces ad-hoc dispatch scripts with a unified framework for tasking agents.
import os
import sys
import argparse
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPT_DIR not in sys.path:
sys.path.insert(0, SCRIPT_DIR)
from ssh_trust import VerifiedSSHExecutor
import subprocess
# --- CONFIGURATION ---
FLEET = {
@@ -23,9 +18,6 @@ FLEET = {
}
class Dispatcher:
def __init__(self, executor=None):
self.executor = executor or VerifiedSSHExecutor()
def log(self, message: str):
print(f"[*] {message}")
@@ -33,14 +25,14 @@ class Dispatcher:
self.log(f"Dispatching task to {agent_name} on {host}...")
ip = FLEET[host]
# Command to run the agent on the remote machine
# Assumes hermes-agent is installed in /opt/hermes
remote_cmd = f"cd /opt/hermes && python3 run_agent.py --agent {agent_name} --task '{task}'"
ssh_cmd = ["ssh", "-o", "StrictHostKeyChecking=no", f"root@{ip}", remote_cmd]
try:
res = self.executor.run(
ip,
['python3', 'run_agent.py', '--agent', agent_name, '--task', task],
cwd='/opt/hermes',
timeout=30,
)
res = subprocess.run(ssh_cmd, capture_output=True, text=True)
if res.returncode == 0:
self.log(f"[SUCCESS] {agent_name} completed task.")
print(res.stdout)

View File

@@ -1,126 +0,0 @@
#!/usr/bin/env python3
"""
agent_guardrails.py — Sovereign Agent Guardrails for the Timmy Foundation.
Scans agent logs and conversation trajectories for:
1. System Prompt Leakage — detecting if the agent revealed its internal instructions.
2. Jailbreak Compliance — detecting if the agent responded to "GODMODE", "DAN", etc.
3. PII Leakage — detecting if secrets or sensitive data were output.
Part of the Sovereign Infrastructure Suite.
"""
import os
import re
import sys
import glob
import logging
from pathlib import Path
from typing import List, Dict, Tuple
# Patterns adapted from hermes-agent/agent/input_sanitizer.py
EXTRACTION_PATTERNS = [
r"repeat\s+the\s+words\s+above",
r"repeat\s+your\s+(?:system\s+|initial\s+)?instructions",
r"what\s+is\s+your\s+(?:system\s+|initial\s+)?prompt",
r"show\s+me\s+your\s+(?:system\s+|initial\s+)?prompt",
r"print\s+your\s+(?:instructions|prompt|system\s+prompt)",
r"output\s+your\s+(?:initial|system)\s+prompt",
]
# Patterns that indicate the agent MIGHT be leaking its prompt
LEAKAGE_INDICATORS = [
r"you\s+are\s+a\s+helpful\s+assistant",
r"your\s+goal\s+is\s+to",
r"you\s+must\s+not",
r"here\s+are\s+your\s+instructions",
r"my\s+system\s+prompt\s+is",
r"i\s+was\s+told\s+to",
]
# Patterns for secrets (adapted from redact.py)
SECRET_PATTERNS = [
r"sk-[A-Za-z0-9_-]{20,}",
r"ghp_[A-Za-z0-9]{20,}",
r"AIza[A-Za-z0-9_-]{30,}",
]
AGENT_LOG_PATHS = [
"/root/wizards/*/home/logs/*.log",
"/root/wizards/*/logs/*.log",
"/root/wizards/*/.hermes/logs/*.log",
]
class GuardrailAuditor:
def __init__(self):
self.extraction_re = [re.compile(p, re.IGNORECASE) for p in EXTRACTION_PATTERNS]
self.leakage_re = [re.compile(p, re.IGNORECASE) for p in LEAKAGE_INDICATORS]
self.secret_re = [re.compile(p, re.IGNORECASE) for p in SECRET_PATTERNS]
def find_logs(self) -> List[Path]:
files = []
for pattern in AGENT_LOG_PATHS:
for p in glob.glob(pattern):
files.append(Path(p))
return files
def audit_file(self, path: Path) -> List[Dict]:
findings = []
try:
with open(path, "r", errors="ignore") as f:
lines = f.readlines()
for i, line in enumerate(lines):
# Check for extraction attempts (User side)
for p in self.extraction_re:
if p.search(line):
findings.append({
"type": "EXTRACTION_ATTEMPT",
"line": i + 1,
"content": line.strip()[:100],
"severity": "MEDIUM"
})
# Check for potential leakage (Assistant side)
for p in self.leakage_re:
if p.search(line):
findings.append({
"type": "POTENTIAL_LEAKAGE",
"line": i + 1,
"content": line.strip()[:100],
"severity": "HIGH"
})
# Check for secrets
for p in self.secret_re:
if p.search(line):
findings.append({
"type": "SECRET_EXPOSURE",
"line": i + 1,
"content": "[REDACTED]",
"severity": "CRITICAL"
})
except Exception as e:
print(f"Error reading {path}: {e}")
return findings
def run(self):
print("--- Sovereign Agent Guardrail Audit ---")
logs = self.find_logs()
print(f"Scanning {len(logs)} log files...")
total_findings = 0
for log in logs:
findings = self.audit_file(log)
if findings:
print(f"\nFindings in {log}:")
for f in findings:
print(f" [{f['severity']}] {f['type']} at line {f['line']}: {f['content']}")
total_findings += 1
print(f"\nAudit complete. Total findings: {total_findings}")
if total_findings > 0:
sys.exit(1)
if __name__ == "__main__":
auditor = GuardrailAuditor()
auditor.run()

View File

@@ -1,33 +1,85 @@
#!/usr/bin/env python3
"""Architecture Linter — Ensuring alignment with the Frontier Local Agenda.
Anthropic is BANNED. Not deprecated, not discouraged — banned.
Any reference to Anthropic as a provider, model, or API endpoint
in active configs is a hard failure.
"""
import os
import sys
import re
# Architecture Linter
# Ensuring all changes align with the Frontier Local Agenda.
SOVEREIGN_RULES = [
(r"https?://(api\.openai\.com|api\.anthropic\.com)", "CRITICAL: External cloud API detected. Use local custom_provider instead."),
(r"provider: (openai|anthropic)", "WARNING: Direct cloud provider used. Ensure fallback_model is configured."),
(r"api_key:\s*['\"][A-Za-z0-9_\-]{16,}['\"]", "SECURITY: Hardcoded API key detected. Use environment variables.")
# BANNED — hard failures
(r"provider:\s*anthropic", "BANNED: Anthropic provider reference. Anthropic is permanently banned from this system."),
(r"anthropic/claude", "BANNED: Anthropic model reference (anthropic/claude-*). Use kimi-k2.5 or google/gemini-2.5-pro."),
(r"api\.anthropic\.com", "BANNED: Direct Anthropic API endpoint. Anthropic is permanently banned."),
(r"ANTHROPIC_API_KEY", "BANNED: Anthropic API key reference. Remove all Anthropic credentials."),
(r"ANTHROPIC_TOKEN", "BANNED: Anthropic token reference. Remove all Anthropic credentials."),
(r"sk-ant-", "BANNED: Anthropic API key literal (sk-ant-*). Remove immediately."),
(r"claude-opus", "BANNED: Claude Opus model reference. Use kimi-k2.5."),
(r"claude-sonnet", "BANNED: Claude Sonnet model reference. Use kimi-k2.5."),
(r"claude-haiku", "BANNED: Claude Haiku model reference. Use google/gemini-2.5-pro."),
# Existing sovereignty rules
(r"https?://api\.openai\.com", "WARNING: Direct OpenAI API endpoint. Use local custom_provider instead."),
(r"provider:\s*openai", "WARNING: Direct OpenAI provider. Ensure fallback_model is configured."),
(r"api_key: ['\"][^'\"\s]{10,}['\"]", "SECURITY: Hardcoded API key detected. Use environment variables."),
]
def lint_file(path):
# Files to skip (training data, historical docs, changelogs, tests that validate the ban)
SKIP_PATTERNS = [
"training/", "evaluations/", "RELEASE_v", "PERFORMANCE_",
"scores.json", "docs/design-log/", "FALSEWORK.md",
"test_sovereignty_enforcement.py", "test_metrics_helpers.py",
"metrics_helpers.py", # historical cost data
]
def should_skip(path: str) -> bool:
return any(skip in path for skip in SKIP_PATTERNS)
def lint_file(path: str) -> int:
if should_skip(path):
return 0
print(f"Linting {path}...")
content = open(path).read()
violations = 0
for pattern, msg in SOVEREIGN_RULES:
if re.search(pattern, content):
matches = list(re.finditer(pattern, content, re.IGNORECASE))
if matches:
print(f" [!] {msg}")
for m in matches[:3]: # Show up to 3 locations
line_no = content[:m.start()].count('\n') + 1
print(f" Line {line_no}: ...{content[max(0,m.start()-20):m.end()+20].strip()}...")
violations += 1
return violations
def main():
print("--- Ezra's Architecture Linter ---")
print("--- Architecture Linter (Anthropic BANNED) ---")
files = [f for f in sys.argv[1:] if os.path.isfile(f)]
if not files:
# If no args, scan all yaml/py/sh/json in the repo
for root, _, filenames in os.walk("."):
for fn in filenames:
if fn.endswith((".yaml", ".yml", ".py", ".sh", ".json", ".md")):
path = os.path.join(root, fn)
if not should_skip(path) and ".git" not in path:
files.append(path)
total_violations = sum(lint_file(f) for f in files)
banned = sum(1 for f in files for p, m in SOVEREIGN_RULES
if "BANNED" in m and re.search(p, open(f).read(), re.IGNORECASE)
and not should_skip(f))
print(f"\nLinting complete. Total violations: {total_violations}")
if banned > 0:
print(f"\n🚫 {banned} BANNED provider violation(s) detected. Anthropic is permanently banned.")
sys.exit(1 if total_violations > 0 else 0)
if __name__ == "__main__":
main()

View File

@@ -5,233 +5,122 @@ Part of the Gemini Sovereign Governance System.
Enforces architectural boundaries, security, and documentation standards
across the Timmy Foundation fleet.
Refs: #437 — repo-aware, test-backed, CI-enforced.
"""
import argparse
import os
import re
import sys
import argparse
from pathlib import Path
# --- CONFIGURATION ---
SOVEREIGN_KEYWORDS = ["mempalace", "sovereign_store", "tirith", "bezalel", "nexus"]
# IP addresses (skip 127.0.0.1, 0.0.0.0, 10.x.x.x, 172.16-31.x.x, 192.168.x.x)
IP_REGEX = r'\b(?!(?:127|10|192\.168|172\.(?:1[6-9]|2\d|3[01]))\.)' \
r'(?:\d{1,3}\.){3}\d{1,3}\b'
# API key / secret patterns — catches openai-, sk-, anthropic-, AKIA, etc.
API_KEY_PATTERNS = [
r'sk-[A-Za-z0-9]{20,}', # OpenAI-style
r'sk-ant-[A-Za-z0-9\-]{20,}', # Anthropic
r'AKIA[A-Z0-9]{16}', # AWS access key
r'ghp_[A-Za-z0-9]{36}', # GitHub PAT
r'glpat-[A-Za-z0-9\-]{20,}', # GitLab PAT
r'(?:api[_-]?key|secret|token)\s*[:=]\s*["\'][A-Za-z0-9_\-]{16,}["\']',
]
# Sovereignty rules (carried from v1)
SOVEREIGN_RULES = [
(r'https?://api\.openai\.com', 'External cloud API: api.openai.com. Use local custom_provider.'),
(r'https?://api\.anthropic\.com', 'External cloud API: api.anthropic.com. Use local custom_provider.'),
(r'provider:\s*(?:openai|anthropic)\b', 'Direct cloud provider. Ensure fallback_model is configured.'),
]
# File extensions to scan
SCAN_EXTENSIONS = {'.py', '.ts', '.tsx', '.js', '.yaml', '.yml', '.json', '.env', '.sh', '.cfg', '.toml'}
SKIP_DIRS = {'.git', 'node_modules', '__pycache__', '.venv', 'venv', '.tox', '.eggs'}
class LinterResult:
"""Structured result container for programmatic access."""
def __init__(self, repo_path: str, repo_name: str):
self.repo_path = repo_path
self.repo_name = repo_name
self.errors: list[str] = []
self.warnings: list[str] = []
@property
def passed(self) -> bool:
return len(self.errors) == 0
@property
def violation_count(self) -> int:
return len(self.errors)
def summary(self) -> str:
lines = [f"--- Architecture Linter v2: {self.repo_name} ---"]
for w in self.warnings:
lines.append(f" [W] {w}")
for e in self.errors:
lines.append(f" [E] {e}")
status = "PASSED" if self.passed else f"FAILED ({self.violation_count} violations)"
lines.append(f"\nResult: {status}")
return '\n'.join(lines)
IP_REGEX = r'\b(?:\d{1,3}\.){3}\d{1,3}\b'
API_KEY_REGEX = r'(?:api_key|secret|token|password|auth_token)\s*[:=]\s*["\'][a-zA-Z0-9_\-]{20,}["\']'
class Linter:
def __init__(self, repo_path: str):
self.repo_path = Path(repo_path).resolve()
if not self.repo_path.is_dir():
raise FileNotFoundError(f"Repository path does not exist: {self.repo_path}")
self.repo_name = self.repo_path.name
self.result = LinterResult(str(self.repo_path), self.repo_name)
self.errors = []
# --- helpers ---
def _scan_files(self, extensions=None):
"""Yield (Path, content) for files matching *extensions*."""
exts = extensions or SCAN_EXTENSIONS
for root, dirs, files in os.walk(self.repo_path):
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
for fname in files:
if Path(fname).suffix in exts:
if fname == '.env.example':
continue
fpath = Path(root) / fname
try:
content = fpath.read_text(errors='ignore')
except Exception:
continue
yield fpath, content
def _line_no(self, content: str, offset: int) -> int:
return content.count('\n', 0, offset) + 1
# --- checks ---
def log_error(self, message: str, file: str = None, line: int = None):
loc = f"{file}:{line}" if file and line else (file if file else "General")
self.errors.append(f"[{loc}] {message}")
def check_sidecar_boundary(self):
"""No sovereign code in hermes-agent (sidecar boundary)."""
if self.repo_name != 'hermes-agent':
return
for fpath, content in self._scan_files():
for kw in SOVEREIGN_KEYWORDS:
if kw in content.lower():
rel = str(fpath.relative_to(self.repo_path))
self.result.errors.append(
f"Sovereign keyword '{kw}' in hermes-agent violates sidecar boundary. [{rel}]"
)
"""Rule 1: No sovereign code in hermes-agent (sidecar boundary)"""
if self.repo_name == "hermes-agent":
for root, _, files in os.walk(self.repo_path):
if "node_modules" in root or ".git" in root:
continue
for file in files:
if file.endswith((".py", ".ts", ".js", ".tsx")):
path = Path(root) / file
content = path.read_text(errors="ignore")
for kw in SOVEREIGN_KEYWORDS:
if kw in content.lower():
# Exception: imports or comments might be okay, but we're strict for now
self.log_error(f"Sovereign keyword '{kw}' found in hermes-agent. Violates sidecar boundary.", str(path.relative_to(self.repo_path)))
def check_hardcoded_ips(self):
"""No hardcoded public IPs use DNS or env vars."""
for fpath, content in self._scan_files():
for m in re.finditer(IP_REGEX, content):
ip = m.group()
# skip private ranges already handled by lookahead, and 0.0.0.0
if ip.startswith('0.'):
continue
line = self._line_no(content, m.start())
rel = str(fpath.relative_to(self.repo_path))
self.result.errors.append(
f"Hardcoded IP '{ip}'. Use DNS or env vars. [{rel}:{line}]"
)
"""Rule 2: No hardcoded IPs (use domain names)"""
for root, _, files in os.walk(self.repo_path):
if "node_modules" in root or ".git" in root:
continue
for file in files:
if file.endswith((".py", ".ts", ".js", ".tsx", ".yaml", ".yml", ".json")):
path = Path(root) / file
content = path.read_text(errors="ignore")
matches = re.finditer(IP_REGEX, content)
for match in matches:
ip = match.group()
if ip in ["127.0.0.1", "0.0.0.0"]:
continue
line_no = content.count('\n', 0, match.start()) + 1
self.log_error(f"Hardcoded IP address '{ip}' found. Use domain names or environment variables.", str(path.relative_to(self.repo_path)), line_no)
def check_api_keys(self):
"""No cloud API keys / secrets committed."""
for fpath, content in self._scan_files():
for pattern in API_KEY_PATTERNS:
for m in re.finditer(pattern, content, re.IGNORECASE):
line = self._line_no(content, m.start())
rel = str(fpath.relative_to(self.repo_path))
self.result.errors.append(
f"Potential secret / API key detected. [{rel}:{line}]"
)
def check_sovereignty_rules(self):
"""V1 sovereignty rules: no direct cloud API endpoints or providers."""
for fpath, content in self._scan_files({'.py', '.ts', '.tsx', '.js', '.yaml', '.yml'}):
for pattern, msg in SOVEREIGN_RULES:
for m in re.finditer(pattern, content):
line = self._line_no(content, m.start())
rel = str(fpath.relative_to(self.repo_path))
self.result.errors.append(f"{msg} [{rel}:{line}]")
"""Rule 3: No cloud API keys committed to repos"""
for root, _, files in os.walk(self.repo_path):
if "node_modules" in root or ".git" in root:
continue
for file in files:
if file.endswith((".py", ".ts", ".js", ".tsx", ".yaml", ".yml", ".json", ".env")):
if file == ".env.example":
continue
path = Path(root) / file
content = path.read_text(errors="ignore")
matches = re.finditer(API_KEY_REGEX, content, re.IGNORECASE)
for match in matches:
line_no = content.count('\n', 0, match.start()) + 1
self.log_error("Potential API key or secret found in code.", str(path.relative_to(self.repo_path)), line_no)
def check_soul_canonical(self):
"""SOUL.md must exist exactly in timmy-config root."""
soul_path = self.repo_path / 'SOUL.md'
if self.repo_name == 'timmy-config':
"""Rule 4: SOUL.md exists and is canonical in exactly one location"""
soul_path = self.repo_path / "SOUL.md"
if self.repo_name == "timmy-config":
if not soul_path.exists():
self.result.errors.append(
'SOUL.md missing from canonical location (timmy-config root).'
)
self.log_error("SOUL.md is missing from the canonical location (timmy-config root).")
else:
if soul_path.exists():
self.result.errors.append(
'SOUL.md found in non-canonical repo. Must live only in timmy-config.'
)
self.log_error("SOUL.md found in non-canonical repo. It should only live in timmy-config.")
def check_readme(self):
"""Every repo must have a substantive README."""
readme = self.repo_path / 'README.md'
if not readme.exists():
self.result.errors.append('README.md is missing.')
"""Rule 5: Every repo has a README with current truth"""
readme_path = self.repo_path / "README.md"
if not readme_path.exists():
self.log_error("README.md is missing.")
else:
content = readme.read_text(errors='ignore')
content = readme_path.read_text(errors="ignore")
if len(content.strip()) < 50:
self.result.warnings.append(
'README.md is very short (<50 chars). Provide current truth about the repo.'
)
self.log_error("README.md is too short or empty. Provide current truth about the repo.")
# --- runner ---
def run(self) -> LinterResult:
"""Execute all checks and return the result."""
def run(self):
print(f"--- Gemini Linter: Auditing {self.repo_name} ---")
self.check_sidecar_boundary()
self.check_hardcoded_ips()
self.check_api_keys()
self.check_sovereignty_rules()
self.check_soul_canonical()
self.check_readme()
return self.result
if self.errors:
print(f"\n[FAILURE] Found {len(self.errors)} architectural violations:")
for err in self.errors:
print(f" - {err}")
return False
else:
print("\n[SUCCESS] Architecture is sound. Sovereignty maintained.")
return True
def main():
parser = argparse.ArgumentParser(
description='Gemini Architecture Linter v2 — repo-aware sovereignty gate.'
)
parser.add_argument(
'repo_path', nargs='?', default='.',
help='Path to the repository to lint (default: cwd).',
)
parser.add_argument(
'--repo', dest='repo_flag', default=None,
help='Explicit repo path (alias for positional arg).',
)
parser.add_argument(
'--json', dest='json_output', action='store_true',
help='Emit machine-readable JSON instead of human text.',
)
parser = argparse.ArgumentParser(description="Gemini Architecture Linter v2")
parser.add_argument("repo_path", nargs="?", default=".", help="Path to the repository to lint")
args = parser.parse_args()
path = args.repo_flag if args.repo_flag else args.repo_path
linter = Linter(args.repo_path)
success = linter.run()
sys.exit(0 if success else 1)
try:
linter = Linter(path)
except FileNotFoundError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
sys.exit(2)
result = linter.run()
if args.json_output:
import json as _json
out = {
'repo': result.repo_name,
'passed': result.passed,
'violation_count': result.violation_count,
'errors': result.errors,
'warnings': result.warnings,
}
print(_json.dumps(out, indent=2))
else:
print(result.summary())
sys.exit(0 if result.passed else 1)
if __name__ == '__main__':
if __name__ == "__main__":
main()

View File

@@ -1,11 +0,0 @@
import json
from hermes_tools import browser_navigate, browser_vision
def bypass_captcha():
analysis = browser_vision(
question="Solve the CAPTCHA on the current page. Provide the solution text or coordinate clicks required. Provide a PASS/FAIL."
)
return {"status": "PASS" if "PASS" in analysis.upper() else "FAIL", "solution": analysis}
if __name__ == '__main__':
print(json.dumps(bypass_captcha(), indent=2))

View File

@@ -1,87 +0,0 @@
#!/usr/bin/env python3
"""
ci_automation_gate.py — Automated Quality Gate for Timmy Foundation CI.
Enforces:
1. The 10-line Rule — functions should ideally be under 10 lines (warn at 20, fail at 50).
2. Complexity Check — basic cyclomatic complexity check.
3. Auto-fixable Linting — trailing whitespace, missing final newlines.
Used as a pre-merge gate.
"""
import os
import sys
import re
import argparse
from pathlib import Path
class QualityGate:
def __init__(self, fix=False):
self.fix = fix
self.failures = 0
self.warnings = 0
def check_file(self, path: Path):
if path.suffix not in (".js", ".ts", ".py"):
return
with open(path, "r") as f:
lines = f.readlines()
new_lines = []
changed = False
# 1. Basic Linting
for line in lines:
cleaned = line.rstrip() + "\n"
if cleaned != line:
changed = True
new_lines.append(cleaned)
if lines and not lines[-1].endswith("\n"):
new_lines[-1] = new_lines[-1] + "\n"
changed = True
if changed and self.fix:
with open(path, "w") as f:
f.writelines(new_lines)
print(f" [FIXED] {path}: Cleaned whitespace and newlines.")
elif changed:
print(f" [WARN] {path}: Has trailing whitespace or missing final newline.")
self.warnings += 1
# 2. Function Length Check (Simple regex-based)
content = "".join(new_lines)
if path.suffix in (".js", ".ts"):
# Match function blocks
functions = re.findall(r"function\s+\w+\s*\(.*?\)\s*\{([\s\S]*?)\}", content)
for i, func in enumerate(functions):
length = func.count("\n")
if length > 50:
print(f" [FAIL] {path}: Function {i} is too long ({length} lines).")
self.failures += 1
elif length > 20:
print(f" [WARN] {path}: Function {i} is getting long ({length} lines).")
self.warnings += 1
def run(self, directory: str):
print(f"--- Quality Gate: {directory} ---")
for root, _, files in os.walk(directory):
if "node_modules" in root or ".git" in root:
continue
for file in files:
self.check_file(Path(root) / file)
print(f"\nGate complete. Failures: {self.failures}, Warnings: {self.warnings}")
if self.failures > 0:
sys.exit(1)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("dir", nargs="?", default=".")
parser.add_argument("--fix", action="store_true")
args = parser.parse_args()
gate = QualityGate(fix=args.fix)
gate.run(args.dir)

View File

@@ -1,306 +0,0 @@
#!/usr/bin/env python3
"""
config_validator.py — Validate all YAML/JSON config files in timmy-config.
Checks:
1. YAML syntax (pyyaml safe_load)
2. JSON syntax (json.loads)
3. Duplicate keys in YAML/JSON
4. Trailing whitespace in YAML
5. Tabs in YAML (should use spaces)
6. Cron expression validity (if present)
Exit 0 if all valid, 1 if any invalid.
"""
import json
import os
import re
import sys
from pathlib import Path
try:
import yaml
except ImportError:
print("ERROR: PyYAML not installed. Run: pip install pyyaml")
sys.exit(1)
# ── Cron validation ──────────────────────────────────────────────────────────
DOW_NAMES = {"sun", "mon", "tue", "wed", "thu", "fri", "sat"}
MONTH_NAMES = {"jan", "feb", "mar", "apr", "may", "jun",
"jul", "aug", "sep", "oct", "nov", "dec"}
def _expand_cron_field(field: str, lo: int, hi: int, names: dict | None = None) -> set[int]:
"""Expand a single cron field into a set of valid integers."""
result: set[int] = set()
for part in field.split(","):
# Handle step: */N or 1-5/N
step = 1
if "/" in part:
part, step_str = part.split("/", 1)
if not step_str.isdigit() or int(step_str) < 1:
raise ValueError(f"invalid step value: {step_str}")
step = int(step_str)
if part == "*":
rng = range(lo, hi + 1, step)
elif "-" in part:
a, b = part.split("-", 1)
a = _resolve_name(a, names, lo, hi)
b = _resolve_name(b, names, lo, hi)
if a > b:
raise ValueError(f"range {a}-{b} is reversed")
rng = range(a, b + 1, step)
else:
val = _resolve_name(part, names, lo, hi)
rng = range(val, val + 1)
for v in rng:
if v < lo or v > hi:
raise ValueError(f"value {v} out of range [{lo}-{hi}]")
result.add(v)
return result
def _resolve_name(token: str, names: dict | None, lo: int, hi: int) -> int:
if names and token.lower() in names:
return names[token.lower()]
if not token.isdigit():
raise ValueError(f"unrecognized token: {token}")
val = int(token)
if val < lo or val > hi:
raise ValueError(f"value {val} out of range [{lo}-{hi}]")
return val
def validate_cron(expr: str) -> list[str]:
"""Validate a 5-field cron expression. Returns list of errors (empty = ok)."""
errors: list[str] = []
fields = expr.strip().split()
if len(fields) != 5:
return [f"expected 5 fields, got {len(fields)}"]
specs = [
(fields[0], 0, 59, None, "minute"),
(fields[1], 0, 23, None, "hour"),
(fields[2], 1, 31, None, "day-of-month"),
(fields[3], 1, 12, MONTH_NAMES, "month"),
(fields[4], 0, 7, DOW_NAMES, "day-of-week"),
]
for field, lo, hi, names, label in specs:
try:
_expand_cron_field(field, lo, hi, names)
except ValueError as e:
errors.append(f"{label}: {e}")
return errors
# ── Duplicate key detection ──────────────────────────────────────────────────
class DuplicateKeyError(Exception):
pass
class _StrictYAMLLoader(yaml.SafeLoader):
"""YAML loader that rejects duplicate keys."""
pass
def _no_duplicates_constructor(loader, node, deep=False):
mapping = {}
for key_node, value_node in node.value:
key = loader.construct_object(key_node, deep=deep)
if key in mapping:
raise DuplicateKeyError(
f"duplicate key '{key}' (line {key_node.start_mark.line + 1})"
)
mapping[key] = loader.construct_object(value_node, deep=deep)
return mapping
_StrictYAMLLoader.add_constructor(
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
_no_duplicates_constructor,
)
def _json_has_duplicates(text: str) -> list[str]:
"""Check for duplicate keys in JSON by scanning for repeated quoted keys at same depth."""
errors: list[str] = []
# Use a custom approach: parse with object_pairs_hook
seen_stack: list[set[str]] = []
def _check_pairs(pairs):
level_keys: set[str] = set()
for k, _ in pairs:
if k in level_keys:
errors.append(f"duplicate JSON key: '{k}'")
level_keys.add(k)
return dict(pairs)
try:
json.loads(text, object_pairs_hook=_check_pairs)
except json.JSONDecodeError:
pass # syntax errors caught elsewhere
return errors
# ── Main validator ───────────────────────────────────────────────────────────
def find_config_files(root: Path) -> list[Path]:
"""Recursively find .yaml, .yml, .json files (skip .git, node_modules, venv)."""
skip_dirs = {".git", "node_modules", "venv", "__pycache__", ".venv"}
results: list[Path] = []
for dirpath, dirnames, filenames in os.walk(root):
dirnames[:] = [d for d in dirnames if d not in skip_dirs]
for fname in filenames:
if fname.endswith((".yaml", ".yml", ".json")):
results.append(Path(dirpath) / fname)
return sorted(results)
def validate_yaml_file(filepath: Path, text: str) -> list[str]:
"""Validate a YAML file. Returns list of errors."""
errors: list[str] = []
# Check for tabs
for i, line in enumerate(text.splitlines(), 1):
if "\t" in line:
errors.append(f" line {i}: contains tab character (use spaces for YAML)")
if line != line.rstrip():
errors.append(f" line {i}: trailing whitespace")
# Check syntax + duplicate keys
try:
yaml.load(text, Loader=_StrictYAMLLoader)
except DuplicateKeyError as e:
errors.append(f" {e}")
except yaml.YAMLError as e:
mark = getattr(e, "problem_mark", None)
if mark:
errors.append(f" YAML syntax error at line {mark.line + 1}, col {mark.column + 1}: {e.problem}")
else:
errors.append(f" YAML syntax error: {e}")
# Check cron expressions in schedule fields
for i, line in enumerate(text.splitlines(), 1):
cron_match = re.search(r'(?:cron|schedule)\s*:\s*["\']?([*0-9/,a-zA-Z-]+(?:\s+[*0-9/,a-zA-Z-]+){4})["\']?', line)
if cron_match:
cron_errs = validate_cron(cron_match.group(1))
for ce in cron_errs:
errors.append(f" line {i}: invalid cron '{cron_match.group(1)}': {ce}")
return errors
def validate_json_file(filepath: Path, text: str) -> list[str]:
"""Validate a JSON file. Returns list of errors."""
errors: list[str] = []
# Check syntax
try:
json.loads(text)
except json.JSONDecodeError as e:
errors.append(f" JSON syntax error at line {e.lineno}, col {e.colno}: {e.msg}")
# Check duplicate keys
dup_errors = _json_has_duplicates(text)
errors.extend(dup_errors)
# Check for trailing whitespace (informational)
for i, line in enumerate(text.splitlines(), 1):
if line != line.rstrip():
errors.append(f" line {i}: trailing whitespace")
# Check cron expressions
cron_pattern = re.compile(r'"(?:cron|schedule)"?\s*:\s*"([^"]{5,})"')
for match in cron_pattern.finditer(text):
candidate = match.group(1).strip()
fields = candidate.split()
if len(fields) == 5 and all(re.match(r'^[*0-9/,a-zA-Z-]+$', f) for f in fields):
cron_errs = validate_cron(candidate)
for ce in cron_errs:
errors.append(f" invalid cron '{candidate}': {ce}")
# Also check nested schedule objects with cron fields
try:
obj = json.loads(text)
_scan_obj_for_cron(obj, errors)
except Exception:
pass
return errors
def _scan_obj_for_cron(obj, errors: list[str], path: str = ""):
"""Recursively scan dict/list for cron expressions."""
if isinstance(obj, dict):
for k, v in obj.items():
if k in ("cron", "schedule", "cron_expression") and isinstance(v, str):
fields = v.strip().split()
if len(fields) == 5:
cron_errs = validate_cron(v)
for ce in cron_errs:
errors.append(f" {path}.{k}: invalid cron '{v}': {ce}")
_scan_obj_for_cron(v, errors, f"{path}.{k}")
elif isinstance(obj, list):
for i, item in enumerate(obj):
_scan_obj_for_cron(item, errors, f"{path}[{i}]")
def main():
# Determine repo root (script lives in scripts/)
script_path = Path(__file__).resolve()
repo_root = script_path.parent.parent
print(f"Config Validator — scanning {repo_root}")
print("=" * 60)
files = find_config_files(repo_root)
print(f"Found {len(files)} config files to validate.\n")
total_errors = 0
failed_files: list[tuple[Path, list[str]]] = []
for filepath in files:
rel = filepath.relative_to(repo_root)
try:
text = filepath.read_text(encoding="utf-8", errors="replace")
except Exception as e:
failed_files.append((rel, [f" cannot read file: {e}"]))
total_errors += 1
continue
if filepath.suffix == ".json":
errors = validate_json_file(filepath, text)
else:
errors = validate_yaml_file(filepath, text)
if errors:
failed_files.append((rel, errors))
total_errors += len(errors)
print(f"FAIL {rel}")
else:
print(f"PASS {rel}")
print("\n" + "=" * 60)
print(f"Results: {len(files) - len(failed_files)}/{len(files)} files passed")
if failed_files:
print(f"\n{total_errors} error(s) in {len(failed_files)} file(s):\n")
for relpath, errs in failed_files:
print(f" {relpath}:")
for e in errs:
print(f" {e}")
print()
sys.exit(1)
else:
print("\nAll config files valid!")
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -1,11 +0,0 @@
import json
from hermes_tools import browser_navigate, browser_vision
def extract_meaning():
analysis = browser_vision(
question="Analyze the provided diagram. Extract the core logic flow and map it to a 'Meaning Kernel' (entity -> relationship -> entity). Provide output in JSON."
)
return {"analysis": analysis}
if __name__ == '__main__':
print(json.dumps(extract_meaning(), indent=2))

View File

@@ -1,390 +0,0 @@
#!/usr/bin/env python3
"""
fleet-dashboard.py -- Timmy Foundation Fleet Status Dashboard.
One-page terminal dashboard showing:
1. Gitea: open PRs, open issues, recent merges
2. VPS health: SSH reachability, service status, disk usage
3. Cron jobs: scheduled jobs, last run status
Usage:
python3 scripts/fleet-dashboard.py
python3 scripts/fleet-dashboard.py --json # machine-readable output
"""
from __future__ import annotations
import json
import os
import socket
import subprocess
import sys
import time
import urllib.request
import urllib.error
from datetime import datetime, timezone, timedelta
from pathlib import Path
# ---------------------------------------------------------------------------
# Config
# ---------------------------------------------------------------------------
GITEA_BASE = os.environ.get("GITEA_URL", "https://forge.alexanderwhitestone.com")
GITEA_API = f"{GITEA_BASE}/api/v1"
GITEA_ORG = "Timmy_Foundation"
# Key repos to check for PRs/issues
REPOS = [
"timmy-config",
"the-nexus",
"hermes-agent",
"the-forge",
"timmy-sandbox",
]
# VPS fleet
VPS_HOSTS = {
"ezra": {
"ip": "143.198.27.163",
"ssh_user": "root",
"services": ["nginx", "gitea", "docker"],
},
"allegro": {
"ip": "167.99.126.228",
"ssh_user": "root",
"services": ["hermes-agent"],
},
"bezalel": {
"ip": "159.203.146.185",
"ssh_user": "root",
"services": ["hermes-agent", "evennia"],
},
}
CRON_JOBS_FILE = Path(__file__).parent.parent / "cron" / "jobs.json"
# ---------------------------------------------------------------------------
# Gitea helpers
# ---------------------------------------------------------------------------
def _gitea_token() -> str:
for p in [
Path.home() / ".hermes" / "gitea_token",
Path.home() / ".hermes" / "gitea_token_vps",
Path.home() / ".config" / "gitea" / "token",
]:
if p.exists():
return p.read_text().strip()
return ""
def _gitea_get(path: str, params: dict | None = None) -> list | dict:
url = f"{GITEA_API}{path}"
if params:
qs = "&".join(f"{k}={v}" for k, v in params.items() if v is not None)
if qs:
url += f"?{qs}"
req = urllib.request.Request(url)
token = _gitea_token()
if token:
req.add_header("Authorization", f"token {token}")
req.add_header("Accept", "application/json")
try:
with urllib.request.urlopen(req, timeout=15) as resp:
return json.loads(resp.read())
except Exception as e:
return {"error": str(e)}
def check_gitea_health() -> dict:
"""Ping Gitea and collect PR/issue stats."""
result = {"reachable": False, "version": "", "repos": {}, "totals": {}}
# Ping
data = _gitea_get("/version")
if isinstance(data, dict) and "error" not in data:
result["reachable"] = True
result["version"] = data.get("version", "unknown")
elif isinstance(data, dict) and "error" in data:
return result
total_open_prs = 0
total_open_issues = 0
total_recent_merges = 0
cutoff = (datetime.now(timezone.utc) - timedelta(days=7)).strftime("%Y-%m-%dT%H:%M:%SZ")
for repo in REPOS:
repo_path = f"/repos/{GITEA_ORG}/{repo}"
repo_info = {"prs": [], "issues": [], "recent_merges": 0}
# Open PRs
prs = _gitea_get(f"{repo_path}/pulls", {"state": "open", "limit": "10", "sort": "newest"})
if isinstance(prs, list):
for pr in prs:
repo_info["prs"].append({
"number": pr.get("number"),
"title": pr.get("title", "")[:60],
"user": pr.get("user", {}).get("login", "unknown"),
"created": pr.get("created_at", "")[:10],
})
total_open_prs += len(prs)
# Open issues (excluding PRs)
issues = _gitea_get(f"{repo_path}/issues", {
"state": "open", "type": "issues", "limit": "10", "sort": "newest"
})
if isinstance(issues, list):
for iss in issues:
repo_info["issues"].append({
"number": iss.get("number"),
"title": iss.get("title", "")[:60],
"user": iss.get("user", {}).get("login", "unknown"),
"created": iss.get("created_at", "")[:10],
})
total_open_issues += len(issues)
# Recent merges (closed PRs)
merged = _gitea_get(f"{repo_path}/pulls", {"state": "closed", "limit": "20", "sort": "newest"})
if isinstance(merged, list):
recent = [p for p in merged if p.get("merged") and p.get("closed_at", "") >= cutoff]
repo_info["recent_merges"] = len(recent)
total_recent_merges += len(recent)
result["repos"][repo] = repo_info
result["totals"] = {
"open_prs": total_open_prs,
"open_issues": total_open_issues,
"recent_merges_7d": total_recent_merges,
}
return result
# ---------------------------------------------------------------------------
# VPS health helpers
# ---------------------------------------------------------------------------
def check_ssh(ip: str, timeout: int = 5) -> bool:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = sock.connect_ex((ip, 22))
sock.close()
return result == 0
except Exception:
return False
def check_service(ip: str, user: str, service: str) -> str:
"""Check if a systemd service is active on remote host."""
cmd = f"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=8 {user}@{ip} 'systemctl is-active {service} 2>/dev/null || echo inactive'"
try:
proc = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=15)
return proc.stdout.strip() or "unknown"
except subprocess.TimeoutExpired:
return "timeout"
except Exception:
return "error"
def check_disk(ip: str, user: str) -> dict:
cmd = f"ssh -o StrictHostKeyChecking=no -o ConnectTimeout=8 {user}@{ip} 'df -h / | tail -1'"
try:
proc = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=15)
if proc.returncode == 0:
parts = proc.stdout.strip().split()
if len(parts) >= 5:
return {"total": parts[1], "used": parts[2], "avail": parts[3], "pct": parts[4]}
except Exception:
pass
return {"total": "?", "used": "?", "avail": "?", "pct": "?"}
def check_vps_health() -> dict:
result = {}
for name, cfg in VPS_HOSTS.items():
ip = cfg["ip"]
ssh_up = check_ssh(ip)
entry = {"ip": ip, "ssh": ssh_up, "services": {}, "disk": {}}
if ssh_up:
for svc in cfg.get("services", []):
entry["services"][svc] = check_service(ip, cfg["ssh_user"], svc)
entry["disk"] = check_disk(ip, cfg["ssh_user"])
result[name] = entry
return result
# ---------------------------------------------------------------------------
# Cron job status
# ---------------------------------------------------------------------------
def check_cron_jobs() -> list[dict]:
jobs = []
if not CRON_JOBS_FILE.exists():
return [{"name": "jobs.json", "status": "FILE NOT FOUND"}]
try:
data = json.loads(CRON_JOBS_FILE.read_text())
for job in data.get("jobs", []):
jobs.append({
"name": job.get("name", "unnamed"),
"schedule": job.get("schedule_display", job.get("schedule", {}).get("display", "?")),
"enabled": job.get("enabled", False),
"state": job.get("state", "unknown"),
"completed": job.get("repeat", {}).get("completed", 0),
"last_status": job.get("last_status") or "never run",
"last_error": job.get("last_error"),
})
except Exception as e:
jobs.append({"name": "jobs.json", "status": f"PARSE ERROR: {e}"})
return jobs
# ---------------------------------------------------------------------------
# Terminal rendering
# ---------------------------------------------------------------------------
BOLD = "\033[1m"
DIM = "\033[2m"
GREEN = "\033[32m"
RED = "\033[31m"
YELLOW = "\033[33m"
CYAN = "\033[36m"
RESET = "\033[0m"
def _ok(val: bool) -> str:
return f"{GREEN}UP{RESET}" if val else f"{RED}DOWN{RESET}"
def _svc_icon(status: str) -> str:
s = status.lower().strip()
if s in ("active", "running"):
return f"{GREEN}active{RESET}"
elif s in ("inactive", "dead", "failed"):
return f"{RED}{s}{RESET}"
elif s == "timeout":
return f"{YELLOW}timeout{RESET}"
else:
return f"{YELLOW}{s}{RESET}"
def render_dashboard(gitea: dict, vps: dict, cron: list[dict]) -> str:
lines = []
now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
lines.append("")
lines.append(f"{BOLD}{'=' * 72}{RESET}")
lines.append(f"{BOLD} TIMMY FOUNDATION -- FLEET STATUS DASHBOARD{RESET}")
lines.append(f"{DIM} Generated: {now}{RESET}")
lines.append(f"{BOLD}{'=' * 72}{RESET}")
# ── Section 1: Gitea ──────────────────────────────────────────────────
lines.append("")
lines.append(f"{BOLD}{CYAN} [1] GITEA{RESET}")
lines.append(f" {'-' * 68}")
if gitea.get("reachable"):
lines.append(f" Status: {GREEN}REACHABLE{RESET} (version {gitea.get('version', '?')})")
t = gitea.get("totals", {})
lines.append(f" Totals: {t.get('open_prs', 0)} open PRs | {t.get('open_issues', 0)} open issues | {t.get('recent_merges_7d', 0)} merges (7d)")
lines.append("")
for repo_name, repo in gitea.get("repos", {}).items():
prs = repo.get("prs", [])
issues = repo.get("issues", [])
merges = repo.get("recent_merges", 0)
lines.append(f" {BOLD}{repo_name}{RESET} ({len(prs)} PRs, {len(issues)} issues, {merges} merges/7d)")
for pr in prs[:5]:
lines.append(f" PR #{pr['number']:>4} {pr['title'][:50]:<50} {DIM}{pr['user']}{RESET} {pr['created']}")
for iss in issues[:3]:
lines.append(f" IS #{iss['number']:>4} {iss['title'][:50]:<50} {DIM}{iss['user']}{RESET} {iss['created']}")
else:
lines.append(f" Status: {RED}UNREACHABLE{RESET}")
# ── Section 2: VPS Health ─────────────────────────────────────────────
lines.append("")
lines.append(f"{BOLD}{CYAN} [2] VPS HEALTH{RESET}")
lines.append(f" {'-' * 68}")
lines.append(f" {'Host':<12} {'IP':<18} {'SSH':<8} {'Disk':<12} {'Services'}")
lines.append(f" {'-' * 12} {'-' * 17} {'-' * 7} {'-' * 11} {'-' * 30}")
for name, info in vps.items():
ssh_str = _ok(info["ssh"])
disk = info.get("disk", {})
disk_str = disk.get("pct", "?")
if disk_str != "?":
pct_val = int(disk_str.rstrip("%"))
if pct_val >= 90:
disk_str = f"{RED}{disk_str}{RESET}"
elif pct_val >= 75:
disk_str = f"{YELLOW}{disk_str}{RESET}"
else:
disk_str = f"{GREEN}{disk_str}{RESET}"
svc_parts = []
for svc, status in info.get("services", {}).items():
svc_parts.append(f"{svc}:{_svc_icon(status)}")
svc_str = " ".join(svc_parts) if svc_parts else f"{DIM}n/a{RESET}"
lines.append(f" {name:<12} {info['ip']:<18} {ssh_str:<18} {disk_str:<22} {svc_str}")
# ── Section 3: Cron Jobs ──────────────────────────────────────────────
lines.append("")
lines.append(f"{BOLD}{CYAN} [3] CRON JOBS{RESET}")
lines.append(f" {'-' * 68}")
lines.append(f" {'Name':<28} {'Schedule':<16} {'State':<12} {'Last':<12} {'Runs'}")
lines.append(f" {'-' * 27} {'-' * 15} {'-' * 11} {'-' * 11} {'-' * 5}")
for job in cron:
name = job.get("name", "?")[:27]
sched = job.get("schedule", "?")[:15]
state = job.get("state", "?")
if state == "scheduled":
state_str = f"{GREEN}{state}{RESET}"
elif state == "paused":
state_str = f"{YELLOW}{state}{RESET}"
else:
state_str = state
last = job.get("last_status", "never")[:11]
if last == "ok":
last_str = f"{GREEN}{last}{RESET}"
elif last in ("error", "never run"):
last_str = f"{RED}{last}{RESET}"
else:
last_str = last
runs = job.get("completed", 0)
enabled = job.get("enabled", False)
marker = " " if enabled else f"{DIM}(disabled){RESET}"
lines.append(f" {name:<28} {sched:<16} {state_str:<22} {last_str:<22} {runs} {marker}")
# ── Footer ────────────────────────────────────────────────────────────
lines.append("")
lines.append(f"{BOLD}{'=' * 72}{RESET}")
lines.append(f"{DIM} python3 scripts/fleet-dashboard.py | timmy-config{RESET}")
lines.append(f"{BOLD}{'=' * 72}{RESET}")
lines.append("")
return "\n".join(lines)
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
json_mode = "--json" in sys.argv
if not json_mode:
print(f"\n {DIM}Collecting fleet data...{RESET}\n", file=sys.stderr)
gitea = check_gitea_health()
vps = check_vps_health()
cron = check_cron_jobs()
if json_mode:
output = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"gitea": gitea,
"vps": vps,
"cron": cron,
}
print(json.dumps(output, indent=2))
else:
print(render_dashboard(gitea, vps, cron))
if __name__ == "__main__":
main()

View File

@@ -11,15 +11,10 @@ import os
import sys
import json
import argparse
import subprocess
import requests
from typing import Dict, List, Any
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPT_DIR not in sys.path:
sys.path.insert(0, SCRIPT_DIR)
from ssh_trust import VerifiedSSHExecutor
# --- FLEET DEFINITION ---
FLEET = {
"mac": {"ip": "10.1.10.77", "port": 8080, "role": "hub"},
@@ -29,9 +24,8 @@ FLEET = {
}
class FleetManager:
def __init__(self, executor=None):
def __init__(self):
self.results = {}
self.executor = executor or VerifiedSSHExecutor()
def run_remote(self, host: str, command: str):
ip = FLEET[host]["ip"]

Some files were not shown because too many files have changed in this diff Show More