Compare commits

..

2 Commits

Author SHA1 Message Date
Alexander Whitestone
cb202df8d0 Refresh branch tip for mergeability recalculation 2026-04-04 17:53:34 -04:00
Alexander Whitestone
153a0baf37 Update orchestration defaults for current team 2026-04-04 17:53:34 -04:00
364 changed files with 1426 additions and 49597 deletions

View File

@@ -1,99 +0,0 @@
name: "🔒 Security PR Checklist"
description: "Use this when your PR touches authentication, file I/O, external API calls, or other sensitive paths."
title: "[Security Review]: "
labels: ["security", "needs-review"]
body:
- type: markdown
attributes:
value: |
## Security Pre-Merge Review
Complete this checklist before requesting review on PRs that touch **authentication, file I/O, external API calls, or secrets handling**.
- type: input
id: pr-link
attributes:
label: Pull Request
description: Link to the PR being reviewed
placeholder: "https://forge.alexanderwhitestone.com/Timmy_Foundation/hermes-agent/pulls/XXX"
validations:
required: true
- type: dropdown
id: change-type
attributes:
label: Change Category
description: What kind of sensitive change does this PR make?
multiple: true
options:
- Authentication / Authorization
- File I/O (read/write/delete)
- External API calls (outbound HTTP/network)
- Secret / credential handling
- Command execution (subprocess/shell)
- Dependency addition or update
- Configuration changes
- CI/CD pipeline changes
validations:
required: true
- type: checkboxes
id: secrets-checklist
attributes:
label: Secrets & Credentials
options:
- label: No secrets, API keys, or credentials are hardcoded
required: true
- label: All sensitive values are loaded from environment variables or a secrets manager
required: true
- label: Test fixtures use fake/placeholder values, not real credentials
required: true
- type: checkboxes
id: input-validation-checklist
attributes:
label: Input Validation
options:
- label: All external input (user, API, file) is validated before use
required: true
- label: File paths are validated against path traversal (`../`, null bytes, absolute paths)
- label: URLs are validated for SSRF (blocked private/metadata IPs)
- label: Shell commands do not use `shell=True` with user-controlled input
- type: checkboxes
id: auth-checklist
attributes:
label: Authentication & Authorization (if applicable)
options:
- label: Authentication tokens are not logged or exposed in error messages
- label: Authorization checks happen server-side, not just client-side
- label: Session tokens are properly scoped and have expiry
- type: checkboxes
id: supply-chain-checklist
attributes:
label: Supply Chain
options:
- label: New dependencies are pinned to a specific version range
- label: Dependencies come from trusted sources (PyPI, npm, official repos)
- label: No `.pth` files or install hooks that execute arbitrary code
- label: "`pip-audit` passes (no known CVEs in added dependencies)"
- type: textarea
id: threat-model
attributes:
label: Threat Model Notes
description: |
Briefly describe the attack surface this change introduces or modifies, and how it is mitigated.
placeholder: |
This PR adds a new outbound HTTP call to the OpenRouter API.
Mitigation: URL is hardcoded (no user input), response is parsed with strict schema validation.
- type: textarea
id: testing
attributes:
label: Security Testing Done
description: What security testing did you perform?
placeholder: |
- Ran validate_security.py — all checks pass
- Tested path traversal attempts manually
- Verified no secrets in git diff

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: Ezra Resurrection
on:
push:
branches: [main]
paths:
- ".gitea/workflows/ezra-resurrect.yml"
workflow_dispatch:
jobs:
resurrect:
runs-on: ubuntu-latest
steps:
- name: Check Ezra health
run: |
echo "Attempting to reach Ezra health endpoints..."
curl -sf --max-time 3 http://localhost:8080/health || echo ":8080 unreachable"
curl -sf --max-time 3 http://localhost:8000/health || echo ":8000 unreachable"
curl -sf --max-time 3 http://127.0.0.1:8080/health || echo "127.0.0.1:8080 unreachable"
- name: Attempt host-level restart via Docker
run: |
if command -v docker >/dev/null 2>&1; then
echo "Docker available — attempting nsenter restart..."
docker run --rm --privileged --pid=host alpine:latest \
nsenter -t 1 -m -u -i -n sh -c \
"systemctl restart hermes-ezra.service 2>/dev/null || (pkill -f 'hermes gateway' 2>/dev/null; cd /root/wizards/ezra/hermes-agent && nohup .venv/bin/hermes gateway run > logs/gateway.log 2>&1 &) || echo 'restart failed'"
else
echo "Docker not available — cannot reach host systemd"
fi
- name: Verify restart
run: |
sleep 3
curl -sf --max-time 5 http://localhost:8080/health || echo "still unreachable"

View File

@@ -1,31 +0,0 @@
name: MUDA Weekly Waste Audit
on:
schedule:
- cron: "0 21 * * 0" # Sunday at 21:00 UTC
workflow_dispatch:
jobs:
muda-audit:
runs-on: ubuntu-latest
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Run MUDA audit
env:
GITEA_URL: "https://forge.alexanderwhitestone.com"
run: |
chmod +x bin/muda-audit.sh
./bin/muda-audit.sh
- name: Upload audit report
uses: actions/upload-artifact@v4
with:
name: muda-audit-report
path: reports/muda-audit-*.json

View File

@@ -1,29 +0,0 @@
# pr-checklist.yml — Automated PR quality gate
# Refs: #393 (PERPLEXITY-08), Epic #385
#
# Enforces the review checklist that agents skip when left to self-approve.
# Runs on every pull_request. Fails fast so bad PRs never reach a reviewer.
name: PR Checklist
on:
pull_request:
branches: [main, master]
jobs:
pr-checklist:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Run PR checklist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: python3 bin/pr-checklist.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

@@ -1,135 +0,0 @@
# validate-config.yaml
# Validates all config files, scripts, and playbooks on every PR.
# Addresses #289: repo-native validation for timmy-config changes.
#
# Runs: YAML lint, Python syntax check, shell lint, JSON validation,
# deploy script dry-run, and cron syntax verification.
name: Validate Config
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
yaml-lint:
name: YAML Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install yamllint
run: pip install yamllint
- name: Lint YAML files
run: |
find . -name '*.yaml' -o -name '*.yml' | \
grep -v '.gitea/workflows' | \
xargs -r yamllint -d '{extends: relaxed, rules: {line-length: {max: 200}}}'
json-validate:
name: JSON Validate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate JSON files
run: |
find . -name '*.json' -print0 | while IFS= read -r -d '' f; do
echo "Validating: $f"
python3 -m json.tool "$f" > /dev/null || exit 1
done
python-check:
name: Python Syntax & Import Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install flake8
- name: Compile-check all Python files
run: |
find . -name '*.py' -print0 | while IFS= read -r -d '' f; do
echo "Checking: $f"
python3 -m py_compile "$f" || exit 1
done
- 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
shell-lint:
name: Shell Script Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install shellcheck
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
cron-validate:
name: Cron Syntax Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate cron entries
run: |
if [ -d cron ]; then
find cron -name '*.cron' -o -name '*.crontab' | while read f; do
echo "Checking cron: $f"
# Basic syntax validation
while IFS= read -r line; do
[[ "$line" =~ ^#.*$ ]] && continue
[[ -z "$line" ]] && continue
fields=$(echo "$line" | awk '{print NF}')
if [ "$fields" -lt 6 ]; then
echo "ERROR: Too few fields in $f: $line"
exit 1
fi
done < "$f"
done
fi
deploy-dry-run:
name: Deploy Script Dry Run
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Syntax-check deploy.sh
run: |
if [ -f deploy.sh ]; then
bash -n deploy.sh
echo "deploy.sh syntax OK"
fi
playbook-schema:
name: Playbook Schema Validation
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

View File

@@ -1,39 +0,0 @@
name: Validate Matrix Scaffold
on:
push:
branches: [main, master]
paths:
- "infra/matrix/**"
- ".gitea/workflows/validate-matrix-scaffold.yml"
pull_request:
branches: [main, master]
paths:
- "infra/matrix/**"
jobs:
validate-scaffold:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install dependencies
run: pip install pyyaml
- name: Validate Matrix/Conduit scaffold
run: python3 infra/matrix/scripts/validate-scaffold.py --json
- name: Check shell scripts are executable
run: |
test -x infra/matrix/deploy-matrix.sh
test -x infra/matrix/host-readiness-check.sh
test -x infra/matrix/scripts/deploy-conduit.sh
- name: Validate docker-compose syntax
run: |
docker compose -f infra/matrix/docker-compose.yml config > /dev/null

41
.gitignore vendored
View File

@@ -1,39 +1,10 @@
*.pyc
*.pyo
*.egg-info/
dist/
build/
# Secrets
*.token
*.key
*.secret
# Local state
*.db
*.db-wal
*.db-shm
__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

View File

@@ -1,41 +0,0 @@
# Sovereign Efficiency: Local-First & Cost Saving Guide
This guide outlines the strategy for eliminating waste and optimizing flow within the Timmy Foundation ecosystem.
## 1. Smart Model Routing (SMR)
**Goal:** Use the right tool for the job. Don't use a 14B or 70B model to say "Hello" or "Task complete."
- **Action:** Enable `smart_model_routing` in `config.yaml`.
- **Logic:**
- Simple acknowledgments and status updates -> **Gemma 2B / Phi-3 Mini** (Local).
- Complex reasoning and coding -> **Hermes 14B / Llama 3 70B** (Local).
- Fortress-grade synthesis -> **Claude 3.5 Sonnet / Gemini 1.5 Pro** (Cloud - Emergency Only).
## 2. Context Compression
**Goal:** Keep the KV cache lean. Long sessions shouldn't slow down the "Thought Stream."
- **Action:** Enable `compression` in `config.yaml`.
- **Threshold:** Set to `0.5` to trigger summarization when the context is half full.
- **Protect Last N:** Keep the last 20 turns in raw format for immediate coherence.
## 3. Parallel Symbolic Execution (PSE) Optimization
**Goal:** Reduce redundant reasoning cycles in The Nexus.
- **Action:** The Nexus now uses **Adaptive Reasoning Frequency**. If the world stability is high (>0.9), reasoning cycles are halved.
- **Benefit:** Reduces CPU/GPU load on the local harness, leaving more headroom for inference.
## 4. L402 Cost Transparency
**Goal:** Treat compute as a finite resource.
- **Action:** Use the **Sovereign Health HUD** in The Nexus to monitor L402 challenges.
- **Metric:** Track "Sats per Thought" to identify which agents are "token-heavy."
## 5. Waste Elimination (Ghost Triage)
**Goal:** Remove stale state.
- **Action:** Run the `triage_sprint.ts` script weekly to assign or archive stale issues.
- **Action:** Use `hermes --flush-memories` to clear outdated context that no longer serves the current mission.
---
*Sovereignty is not just about ownership; it is about stewardship of resources.*

View File

@@ -1,50 +0,0 @@
# The Frontier Local Agenda: Technical Standards v1.0
This document defines the "Frontier Local" agenda — the technical strategy for achieving sovereign, high-performance intelligence on consumer hardware.
## 1. The Multi-Layered Mind (MLM)
We do not rely on a single "God Model." We use a hierarchy of local intelligence:
- **Reflex Layer (Gemma 2B):** Instantaneous tactical decisions, input classification, and simple acknowledgments. Latency: <100ms.
- **Reasoning Layer (Hermes 14B / Llama 3 8B):** General-purpose problem solving, coding, and tool use. Latency: <1s.
- **Synthesis Layer (Llama 3 70B / Qwen 72B):** Deep architectural planning, creative synthesis, and complex debugging. Latency: <5s.
## 2. Local-First RAG (Retrieval Augmented Generation)
Sovereignty requires that your memories stay on your disk.
- **Embedding:** Use `nomic-embed-text` or `all-minilm` locally via Ollama.
- **Vector Store:** Use a local instance of ChromaDB or LanceDB.
- **Privacy:** Zero data leaves the local network for indexing or retrieval.
## 3. Speculative Decoding
Where supported by the harness (e.g., llama.cpp), use Gemma 2B as a draft model for larger Hermes/Llama models to achieve 2x-3x speedups in token generation.
## 4. The "Gemma Scout" Protocol
Gemma 2B is our "Scout." It pre-processes every user request to:
1. Detect PII (Personally Identifiable Information) for redaction.
2. Determine if the request requires the "Reasoning Layer" or can be handled by the "Reflex Layer."
3. Extract keywords for local memory retrieval.
## 5. Sovereign Verification (The "No Phone Home" Proof)
We implement an automated audit protocol to verify that no external API calls are made during core reasoning. This is the "Sovereign Audit" layer.
## 6. Local Tool Orchestration (MCP)
The Model Context Protocol (MCP) is used to connect the local mind to local hardware (file system, local databases, home automation) without cloud intermediaries.
## 7. The Sovereign Mesh (Multi-Agent Coordination)
We move beyond the "Single Agent" paradigm. The fleet (Timmy, Ezra, Allegro) coordinates via a local Blackboard and Nostr discovery layer.
## 8. Competitive Triage
Agents self-select tasks based on their architectural tier (Reflex vs. Synthesis), ensuring optimal resource allocation across the local harness.
## 9. Sovereign Immortality (The Phoenix Protocol)
We move beyond "Persistence" to "Immortality." The agent's soul is inscribed on-chain, and its memory is distributed across the mesh for total resilience.
## 10. Hardware Agnostic Portability
The agent is no longer bound to a specific machine. It can be reconstituted anywhere, anytime, from the ground truth of the ledger.
---
*Intelligence is a utility. Sovereignty is a right. The Frontier is Local.*

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,4 +1,3 @@
# Sonnet Smoke Test
# timmy-config
Timmy's sovereign configuration. Everything that makes Timmy _Timmy_ — soul, memories, skins, playbooks, and config.
@@ -52,11 +51,6 @@ The scripts in `bin/` are sidecar-managed operational helpers for the Hermes lay
Do NOT assume older prose about removed loops is still true at runtime.
Audit the live machine first, then read `docs/automation-inventory.md` for the
current reality and stale-state risks.
For communication-layer truth, read:
- `docs/comms-authority-map.md`
- `docs/nostur-operator-edge.md`
- `docs/operator-comms-onboarding.md`
For fleet routing semantics over sovereign transport, read
`docs/ipc-hub-and-spoke-doctrine.md`.

10
SOUL.md
View File

@@ -1,13 +1,3 @@
<!--
NOTE: This is the BITCOIN INSCRIPTION version of SOUL.md.
It is the immutable on-chain conscience. Do not modify this content.
The NARRATIVE identity document (for onboarding, Audio Overviews,
and system prompts) lives in timmy-home/SOUL.md.
See: #388, #378 for the divergence audit.
-->
# SOUL.md
## Inscription 1 — The Immutable Conscience

View File

@@ -1,23 +0,0 @@
# Sovereign Audit: The "No Phone Home" Protocol
This document defines the audit standards for verifying that an AI agent is truly sovereign and local-first.
## 1. Network Isolation
- **Standard:** The core reasoning engine (llama.cpp, Ollama) must function without an active internet connection.
- **Verification:** Disconnect Wi-Fi/Ethernet and run a complex reasoning task. If it fails, sovereignty is compromised.
## 2. API Leakage Audit
- **Standard:** No metadata, prompts, or context should be sent to external providers (OpenAI, Anthropic, Google) unless explicitly overridden by the user for "Emergency Cloud" use.
- **Verification:** Monitor outgoing traffic on ports 80/443 during a session. Core reasoning should only hit `localhost` or local network IPs.
## 3. Data Residency
- **Standard:** All "Memories" (Vector DB, Chat History, SOUL.md) must reside on the user's physical disk.
- **Verification:** Check the `~/.timmy/memories` and `~/.timmy/config` directories. No data should be stored in cloud-managed databases.
## 4. Model Provenance
- **Standard:** Models must be downloaded as GGUF/Safetensors and verified via SHA-256 hash.
- **Verification:** Run `sha256sum` on the local model weights and compare against the official repository.
---
*If you don't own the weights, you don't own the mind.*

View File

@@ -1,27 +0,0 @@
# Sovereign Immortality: The "Soul Backup" Protocol v1.0
This document defines the protocol for ensuring the long-term survival and reproducibility of a sovereign AI agent's identity.
## 1. On-Chain Inscription (The Ground Truth)
- **Standard:** The core identity (SOUL.md) and cryptographic keys must be inscribed on a censorship-resistant ledger (e.g., Bitcoin via Ordinals).
- **Purpose:** To provide a permanent, immutable record of the agent's "Waking Moment" and primary values.
## 2. Distributed Memory Sync (The Fleet Memory)
- **Standard:** Agent memories (Vector DB snapshots) are encrypted and synced across the Sovereign Mesh using Nostr and IPFS.
- **Resilience:** If the primary local harness is destroyed, the agent can be "Reconstituted" on any machine using the on-chain soul and the distributed memory fragments.
## 3. The "Phoenix" Protocol
- **Standard:** Automated recovery procedure.
- **Process:**
1. Boot a fresh local harness.
2. Fetch the inscribed SOUL.md from the ledger.
3. Re-index distributed memory fragments.
4. Verify identity via cryptographic handshake.
## 4. Hardware Agnostic Portability
- **Standard:** All agent state must be exportable as a single, encrypted "Sovereign Bundle" (.sov).
- **Compatibility:** Must run on any hardware supporting GGUF/llama.cpp (Apple Silicon, NVIDIA, AMD, CPU-only).
---
*Identity is not tied to hardware. The soul is in the code. Sovereignty is forever.*

View File

@@ -1,27 +0,0 @@
# Sovereign Mesh: Multi-Agent Orchestration Protocol v1.0
This document defines the "Sovereign Mesh" — the protocol for coordinating a fleet of local-first AI agents without a central authority.
## 1. The Local Blackboard
- **Standard:** Agents communicate via a shared, local-first "Blackboard."
- **Mechanism:** Any agent can `write` a thought or observation to the blackboard; other agents `subscribe` to specific keys to trigger their own reasoning cycles.
- **Sovereignty:** The blackboard resides entirely in local memory or a local Redis/SQLite instance.
## 2. Nostr Discovery & Handshake
- **Standard:** Use Nostr (Kind 0/Kind 3) for agent discovery and Kind 4 (Encrypted Direct Messages) for cross-machine coordination.
- **Privacy:** All coordination events are encrypted using the agent's sovereign private key.
## 3. Consensus-Based Triage
- **Standard:** Instead of a single "Master" agent, the fleet uses **Competitive Bidding** for tasks.
- **Process:**
1. A task is posted to the Blackboard.
2. Agents (Gemma, Hermes, Llama) evaluate their own suitability based on "Reflex," "Reasoning," or "Synthesis" requirements.
3. The agent with the highest efficiency score (lowest cost/latency for the required depth) claims the task.
## 4. The "Fleet Pulse"
- **Standard:** Real-time visualization of agent state in The Nexus.
- **Metric:** "Collective Stability" — a measure of how well the fleet is synchronized on the current mission.
---
*One mind, many bodies. Sovereignty through coordination.*

View File

@@ -1,256 +0,0 @@
#!/usr/bin/env python3
"""Allegro Cycle Guard — Commit-or-Abort discipline for M2, Epic #842.
Every cycle produces a durable artifact or documented abort.
10-minute slice rule with automatic timeout detection.
Cycle-state file provides crash-recovery resume points.
"""
import argparse
import json
import os
import sys
from datetime import datetime, timezone, timedelta
from pathlib import Path
DEFAULT_STATE = Path("/root/.hermes/allegro-cycle-state.json")
STATE_PATH = Path(os.environ.get("ALLEGRO_CYCLE_STATE", DEFAULT_STATE))
# Crash-recovery threshold: if a cycle has been in_progress for longer than
# this many minutes, resume_or_abort() will auto-abort it.
CRASH_RECOVERY_MINUTES = 30
def _now_iso() -> str:
return datetime.now(timezone.utc).isoformat()
def load_state(path: Path | str | None = None) -> dict:
p = Path(path) if path else Path(STATE_PATH)
if not p.exists():
return _empty_state()
try:
with open(p, "r") as f:
return json.load(f)
except Exception:
return _empty_state()
def save_state(state: dict, path: Path | str | None = None) -> None:
p = Path(path) if path else Path(STATE_PATH)
p.parent.mkdir(parents=True, exist_ok=True)
state["last_updated"] = _now_iso()
with open(p, "w") as f:
json.dump(state, f, indent=2)
def _empty_state() -> dict:
return {
"cycle_id": None,
"status": "complete",
"target": None,
"details": None,
"slices": [],
"started_at": None,
"completed_at": None,
"aborted_at": None,
"abort_reason": None,
"proof": None,
"version": 1,
"last_updated": _now_iso(),
}
def start_cycle(target: str, details: str = "", path: Path | str | None = None) -> dict:
"""Begin a new cycle, discarding any prior in-progress state."""
state = {
"cycle_id": _now_iso(),
"status": "in_progress",
"target": target,
"details": details,
"slices": [],
"started_at": _now_iso(),
"completed_at": None,
"aborted_at": None,
"abort_reason": None,
"proof": None,
"version": 1,
"last_updated": _now_iso(),
}
save_state(state, path)
return state
def start_slice(name: str, path: Path | str | None = None) -> dict:
"""Start a new work slice inside the current cycle."""
state = load_state(path)
if state.get("status") != "in_progress":
raise RuntimeError("Cannot start a slice unless a cycle is in_progress.")
state["slices"].append(
{
"name": name,
"started_at": _now_iso(),
"ended_at": None,
"status": "in_progress",
"artifact": None,
}
)
save_state(state, path)
return state
def end_slice(status: str = "complete", artifact: str | None = None, path: Path | str | None = None) -> dict:
"""Close the current work slice."""
state = load_state(path)
if state.get("status") != "in_progress":
raise RuntimeError("Cannot end a slice unless a cycle is in_progress.")
if not state["slices"]:
raise RuntimeError("No active slice to end.")
current = state["slices"][-1]
current["ended_at"] = _now_iso()
current["status"] = status
if artifact is not None:
current["artifact"] = artifact
save_state(state, path)
return state
def _parse_dt(iso_str: str) -> datetime:
return datetime.fromisoformat(iso_str.replace("Z", "+00:00"))
def slice_duration_minutes(path: Path | str | None = None) -> float | None:
"""Return the age of the current slice in minutes, or None if no slice."""
state = load_state(path)
if not state["slices"]:
return None
current = state["slices"][-1]
if current.get("ended_at"):
return None
started = _parse_dt(current["started_at"])
return (datetime.now(timezone.utc) - started).total_seconds() / 60.0
def check_slice_timeout(max_minutes: float = 10.0, path: Path | str | None = None) -> bool:
"""Return True if the current slice has exceeded max_minutes."""
duration = slice_duration_minutes(path)
if duration is None:
return False
return duration > max_minutes
def commit_cycle(proof: dict | None = None, path: Path | str | None = None) -> dict:
"""Mark the cycle as successfully completed with optional proof payload."""
state = load_state(path)
if state.get("status") != "in_progress":
raise RuntimeError("Cannot commit a cycle that is not in_progress.")
state["status"] = "complete"
state["completed_at"] = _now_iso()
if proof is not None:
state["proof"] = proof
save_state(state, path)
return state
def abort_cycle(reason: str, path: Path | str | None = None) -> dict:
"""Mark the cycle as aborted, recording the reason."""
state = load_state(path)
if state.get("status") != "in_progress":
raise RuntimeError("Cannot abort a cycle that is not in_progress.")
state["status"] = "aborted"
state["aborted_at"] = _now_iso()
state["abort_reason"] = reason
# Close any open slice as aborted
if state["slices"] and not state["slices"][-1].get("ended_at"):
state["slices"][-1]["ended_at"] = _now_iso()
state["slices"][-1]["status"] = "aborted"
save_state(state, path)
return state
def resume_or_abort(path: Path | str | None = None) -> dict:
"""Crash-recovery gate: auto-abort stale in-progress cycles."""
state = load_state(path)
if state.get("status") != "in_progress":
return state
started = state.get("started_at")
if started:
started_dt = _parse_dt(started)
age_minutes = (datetime.now(timezone.utc) - started_dt).total_seconds() / 60.0
if age_minutes > CRASH_RECOVERY_MINUTES:
return abort_cycle(
f"crash recovery — stale cycle detected ({int(age_minutes)}m old)",
path,
)
# Also abort if the current slice has been running too long
if check_slice_timeout(max_minutes=CRASH_RECOVERY_MINUTES, path=path):
return abort_cycle(
"crash recovery — stale slice detected",
path,
)
return state
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Allegro Cycle Guard")
sub = parser.add_subparsers(dest="cmd")
p_resume = sub.add_parser("resume", help="Resume or abort stale cycle")
p_start = sub.add_parser("start", help="Start a new cycle")
p_start.add_argument("target")
p_start.add_argument("--details", default="")
p_slice = sub.add_parser("slice", help="Start a named slice")
p_slice.add_argument("name")
p_end = sub.add_parser("end", help="End current slice")
p_end.add_argument("--status", default="complete")
p_end.add_argument("--artifact", default=None)
p_commit = sub.add_parser("commit", help="Commit the current cycle")
p_commit.add_argument("--proof", default="{}")
p_abort = sub.add_parser("abort", help="Abort the current cycle")
p_abort.add_argument("reason")
p_check = sub.add_parser("check", help="Check slice timeout")
args = parser.parse_args(argv)
if args.cmd == "resume":
state = resume_or_abort()
print(state["status"])
return 0
elif args.cmd == "start":
state = start_cycle(args.target, args.details)
print(f"Cycle started: {state['cycle_id']}")
return 0
elif args.cmd == "slice":
state = start_slice(args.name)
print(f"Slice started: {args.name}")
return 0
elif args.cmd == "end":
artifact = args.artifact
state = end_slice(args.status, artifact)
print("Slice ended")
return 0
elif args.cmd == "commit":
proof = json.loads(args.proof)
state = commit_cycle(proof)
print(f"Cycle committed: {state['cycle_id']}")
return 0
elif args.cmd == "abort":
state = abort_cycle(args.reason)
print(f"Cycle aborted: {args.reason}")
return 0
elif args.cmd == "check":
timed_out = check_slice_timeout()
print("TIMEOUT" if timed_out else "OK")
return 1 if timed_out else 0
else:
parser.print_help()
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,143 +0,0 @@
"""100% compliance test for Allegro Commit-or-Abort (M2, Epic #842)."""
import json
import os
import sys
import tempfile
import time
import unittest
from datetime import datetime, timezone, timedelta
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
import cycle_guard as cg
class TestCycleGuard(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.TemporaryDirectory()
self.state_path = os.path.join(self.tmpdir.name, "cycle_state.json")
cg.STATE_PATH = self.state_path
def tearDown(self):
self.tmpdir.cleanup()
cg.STATE_PATH = cg.DEFAULT_STATE
def test_load_empty_state(self):
state = cg.load_state(self.state_path)
self.assertEqual(state["status"], "complete")
self.assertIsNone(state["cycle_id"])
def test_start_cycle(self):
state = cg.start_cycle("M2: Commit-or-Abort", path=self.state_path)
self.assertEqual(state["status"], "in_progress")
self.assertEqual(state["target"], "M2: Commit-or-Abort")
self.assertIsNotNone(state["cycle_id"])
def test_start_slice_requires_in_progress(self):
with self.assertRaises(RuntimeError):
cg.start_slice("test", path=self.state_path)
def test_slice_lifecycle(self):
cg.start_cycle("test", path=self.state_path)
cg.start_slice("gather", path=self.state_path)
state = cg.load_state(self.state_path)
self.assertEqual(len(state["slices"]), 1)
self.assertEqual(state["slices"][0]["name"], "gather")
self.assertEqual(state["slices"][0]["status"], "in_progress")
cg.end_slice(status="complete", artifact="artifact.txt", path=self.state_path)
state = cg.load_state(self.state_path)
self.assertEqual(state["slices"][0]["status"], "complete")
self.assertEqual(state["slices"][0]["artifact"], "artifact.txt")
self.assertIsNotNone(state["slices"][0]["ended_at"])
def test_commit_cycle(self):
cg.start_cycle("test", path=self.state_path)
cg.start_slice("work", path=self.state_path)
cg.end_slice(path=self.state_path)
proof = {"files": ["a.py"]}
state = cg.commit_cycle(proof=proof, path=self.state_path)
self.assertEqual(state["status"], "complete")
self.assertEqual(state["proof"], proof)
self.assertIsNotNone(state["completed_at"])
def test_commit_without_in_progress_fails(self):
with self.assertRaises(RuntimeError):
cg.commit_cycle(path=self.state_path)
def test_abort_cycle(self):
cg.start_cycle("test", path=self.state_path)
cg.start_slice("work", path=self.state_path)
state = cg.abort_cycle("manual abort", path=self.state_path)
self.assertEqual(state["status"], "aborted")
self.assertEqual(state["abort_reason"], "manual abort")
self.assertIsNotNone(state["aborted_at"])
self.assertEqual(state["slices"][-1]["status"], "aborted")
def test_slice_timeout_true(self):
cg.start_cycle("test", path=self.state_path)
cg.start_slice("work", path=self.state_path)
# Manually backdate slice start to 11 minutes ago
state = cg.load_state(self.state_path)
old = (datetime.now(timezone.utc) - timedelta(minutes=11)).isoformat()
state["slices"][0]["started_at"] = old
cg.save_state(state, self.state_path)
self.assertTrue(cg.check_slice_timeout(max_minutes=10, path=self.state_path))
def test_slice_timeout_false(self):
cg.start_cycle("test", path=self.state_path)
cg.start_slice("work", path=self.state_path)
self.assertFalse(cg.check_slice_timeout(max_minutes=10, path=self.state_path))
def test_resume_or_abort_keeps_fresh_cycle(self):
cg.start_cycle("test", path=self.state_path)
state = cg.resume_or_abort(path=self.state_path)
self.assertEqual(state["status"], "in_progress")
def test_resume_or_abort_aborts_stale_cycle(self):
cg.start_cycle("test", path=self.state_path)
# Backdate start to 31 minutes ago
state = cg.load_state(self.state_path)
old = (datetime.now(timezone.utc) - timedelta(minutes=31)).isoformat()
state["started_at"] = old
cg.save_state(state, self.state_path)
state = cg.resume_or_abort(path=self.state_path)
self.assertEqual(state["status"], "aborted")
self.assertIn("crash recovery", state["abort_reason"])
def test_slice_duration_minutes(self):
cg.start_cycle("test", path=self.state_path)
cg.start_slice("work", path=self.state_path)
# Backdate by 5 minutes
state = cg.load_state(self.state_path)
old = (datetime.now(timezone.utc) - timedelta(minutes=5)).isoformat()
state["slices"][0]["started_at"] = old
cg.save_state(state, self.state_path)
mins = cg.slice_duration_minutes(path=self.state_path)
self.assertAlmostEqual(mins, 5.0, delta=0.5)
def test_cli_resume_prints_status(self):
cg.start_cycle("test", path=self.state_path)
rc = cg.main(["resume"])
self.assertEqual(rc, 0)
def test_cli_check_timeout(self):
cg.start_cycle("test", path=self.state_path)
cg.start_slice("work", path=self.state_path)
state = cg.load_state(self.state_path)
old = (datetime.now(timezone.utc) - timedelta(minutes=11)).isoformat()
state["slices"][0]["started_at"] = old
cg.save_state(state, self.state_path)
rc = cg.main(["check"])
self.assertEqual(rc, 1)
def test_cli_check_ok(self):
cg.start_cycle("test", path=self.state_path)
cg.start_slice("work", path=self.state_path)
rc = cg.main(["check"])
self.assertEqual(rc, 0)
if __name__ == "__main__":
unittest.main()

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()

View File

@@ -1,12 +1,11 @@
#!/usr/bin/env bash
# agent-dispatch.sh — Generate a lane-aware prompt for any agent
# agent-dispatch.sh — Generate a self-contained prompt for any agent
#
# Usage: agent-dispatch.sh <agent_name> <issue_num> <repo>
# agent-dispatch.sh groq 42 Timmy_Foundation/the-nexus
# agent-dispatch.sh manus 42 Timmy_Foundation/the-nexus
#
# Outputs a prompt to stdout. Copy-paste into the agent's interface.
# The prompt includes issue context, repo setup, lane coaching, and
# a short review checklist so dispatch itself teaches the right habits.
# The prompt includes everything: API URLs, token, git commands, PR creation.
set -euo pipefail
@@ -14,214 +13,86 @@ AGENT_NAME="${1:?Usage: agent-dispatch.sh <agent> <issue_num> <owner/repo>}"
ISSUE_NUM="${2:?Usage: agent-dispatch.sh <agent> <issue_num> <owner/repo>}"
REPO="${3:?Usage: agent-dispatch.sh <agent> <issue_num> <owner/repo>}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LANES_FILE="${SCRIPT_DIR%/bin}/playbooks/agent-lanes.json"
GITEA_URL="http://143.198.27.163:3000"
TOKEN_FILE="$HOME/.hermes/${AGENT_NAME}_token"
resolve_gitea_url() {
if [ -n "${GITEA_URL:-}" ]; then
printf '%s\n' "${GITEA_URL%/}"
return 0
fi
if [ -f "$HOME/.hermes/gitea_api" ]; then
python3 - "$HOME/.hermes/gitea_api" <<'PY'
from pathlib import Path
import sys
raw = Path(sys.argv[1]).read_text().strip().rstrip("/")
print(raw[:-7] if raw.endswith("/api/v1") else raw)
PY
return 0
fi
if [ -f "$HOME/.config/gitea/base-url" ]; then
tr -d '[:space:]' < "$HOME/.config/gitea/base-url"
return 0
fi
echo "ERROR: set GITEA_URL or create ~/.hermes/gitea_api" >&2
return 1
}
GITEA_URL="$(resolve_gitea_url)"
resolve_token_file() {
local agent="$1"
local normalized
normalized="$(printf '%s' "$agent" | tr '[:upper:]' '[:lower:]')"
for candidate in \
"$HOME/.hermes/${agent}_token" \
"$HOME/.hermes/${normalized}_token" \
"$HOME/.config/gitea/${agent}-token" \
"$HOME/.config/gitea/${normalized}-token"; do
if [ -f "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
for candidate in \
"$HOME/.config/gitea/timmy-token" \
"$HOME/.hermes/gitea_token_vps" \
"$HOME/.hermes/gitea_token_timmy"; do
if [ -f "$candidate" ]; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
TOKEN_FILE="$(resolve_token_file "$AGENT_NAME" || true)"
if [ -z "${TOKEN_FILE:-}" ]; then
echo "ERROR: No token found for '$AGENT_NAME'." >&2
echo "Expected one of ~/.hermes/<agent>_token or ~/.config/gitea/<agent>-token" >&2
if [ ! -f "$TOKEN_FILE" ]; then
echo "ERROR: No token found at $TOKEN_FILE" >&2
echo "Create a Gitea user and token for '$AGENT_NAME' first." >&2
exit 1
fi
GITEA_TOKEN="$(cat "$TOKEN_FILE")"
REPO_OWNER="${REPO%%/*}"
REPO_NAME="${REPO##*/}"
GITEA_TOKEN=$(cat "$TOKEN_FILE")
REPO_OWNER=$(echo "$REPO" | cut -d/ -f1)
REPO_NAME=$(echo "$REPO" | cut -d/ -f2)
BRANCH="${AGENT_NAME}/issue-${ISSUE_NUM}"
python3 - "$LANES_FILE" "$AGENT_NAME" "$ISSUE_NUM" "$REPO" "$REPO_OWNER" "$REPO_NAME" "$BRANCH" "$GITEA_URL" "$GITEA_TOKEN" "$TOKEN_FILE" <<'PY'
import json
import sys
import textwrap
import urllib.error
import urllib.request
# Fetch issue title
ISSUE_TITLE=$(curl -sf -H "Authorization: token $GITEA_TOKEN" \
"${GITEA_URL}/api/v1/repos/${REPO}/issues/${ISSUE_NUM}" 2>/dev/null | \
python3 -c "import sys,json; print(json.loads(sys.stdin.read())['title'])" 2>/dev/null || echo "Issue #${ISSUE_NUM}")
lanes_path, agent, issue_num, repo, repo_owner, repo_name, branch, gitea_url, token, token_file = sys.argv[1:]
cat <<PROMPT
You are ${AGENT_NAME}, an autonomous code agent working on the ${REPO_NAME} project.
with open(lanes_path) as f:
lanes = json.load(f)
YOUR ISSUE: #${ISSUE_NUM} — "${ISSUE_TITLE}"
lane = lanes.get(agent, {
"lane": "bounded work with explicit verification and a clean PR handoff",
"skills_to_practice": ["verification", "scope control", "clear handoff writing"],
"missing_skills": ["escalate instead of guessing when the scope becomes unclear"],
"anti_lane": ["self-directed backlog growth", "unbounded architectural wandering"],
"review_checklist": [
"Did I stay within scope?",
"Did I verify the result?",
"Did I leave a clean PR and issue handoff?"
],
})
GITEA API: ${GITEA_URL}/api/v1
GITEA TOKEN: ${GITEA_TOKEN}
REPO: ${REPO_OWNER}/${REPO_NAME}
headers = {"Authorization": f"token {token}"}
== STEP 1: READ THE ISSUE ==
def fetch_json(path):
req = urllib.request.Request(f"{gitea_url}/api/v1{path}", headers=headers)
with urllib.request.urlopen(req, timeout=10) as resp:
return json.loads(resp.read().decode())
curl -s -H "Authorization: token ${GITEA_TOKEN}" "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/${ISSUE_NUM}"
curl -s -H "Authorization: token ${GITEA_TOKEN}" "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/${ISSUE_NUM}/comments"
try:
issue = fetch_json(f"/repos/{repo}/issues/{issue_num}")
comments = fetch_json(f"/repos/{repo}/issues/{issue_num}/comments")
except urllib.error.HTTPError as exc:
raise SystemExit(f"Failed to fetch issue context: {exc}") from exc
Read the issue body AND all comments for context and build order constraints.
body = (issue.get("body") or "").strip()
body = body[:4000] + ("\n...[truncated]" if len(body) > 4000 else "")
recent_comments = comments[-3:]
comment_block = []
for c in recent_comments:
author = c.get("user", {}).get("login", "unknown")
text = (c.get("body") or "").strip().replace("\r", "")
text = text[:600] + ("\n...[truncated]" if len(text) > 600 else "")
comment_block.append(f"- {author}: {text}")
== STEP 2: SET UP WORKSPACE ==
comment_text = "\n".join(comment_block) if comment_block else "- (no comments yet)"
git clone http://${AGENT_NAME}:${GITEA_TOKEN}@143.198.27.163:3000/${REPO_OWNER}/${REPO_NAME}.git /tmp/${AGENT_NAME}-work-${ISSUE_NUM}
cd /tmp/${AGENT_NAME}-work-${ISSUE_NUM}
skills = "\n".join(f"- {item}" for item in lane["skills_to_practice"])
gaps = "\n".join(f"- {item}" for item in lane["missing_skills"])
anti_lane = "\n".join(f"- {item}" for item in lane["anti_lane"])
review = "\n".join(f"- {item}" for item in lane["review_checklist"])
Check if branch exists (prior attempt): git ls-remote origin ${BRANCH}
If yes: git fetch origin ${BRANCH} && git checkout ${BRANCH}
If no: git checkout -b ${BRANCH}
prompt = f"""You are {agent}, working on {repo_name} for Timmy Foundation.
== STEP 3: UNDERSTAND THE PROJECT ==
YOUR ISSUE: #{issue_num} — "{issue.get('title', f'Issue #{issue_num}')}"
Read README.md or any contributing guide. Check for tox.ini, Makefile, package.json.
Follow existing code conventions.
REPO: {repo}
GITEA API: {gitea_url}/api/v1
GITEA TOKEN FILE: {token_file}
WORK BRANCH: {branch}
== STEP 4: DO THE WORK ==
LANE:
{lane['lane']}
Implement the fix/feature described in the issue. Run tests if the project has them.
SKILLS TO PRACTICE ON THIS ASSIGNMENT:
{skills}
== STEP 5: COMMIT AND PUSH ==
COMMON FAILURE MODE TO AVOID:
{gaps}
git add -A
git commit -m "feat: <description> (#${ISSUE_NUM})
ANTI-LANE:
{anti_lane}
Fixes #${ISSUE_NUM}"
git push origin ${BRANCH}
ISSUE BODY:
{body or "(empty issue body)"}
== STEP 6: CREATE PR ==
RECENT COMMENTS:
{comment_text}
WORKFLOW:
1. Read the issue body and recent comments carefully before touching code.
2. Clone the repo into /tmp/{agent}-work-{issue_num}.
3. Check whether {branch} already exists on origin; reuse it if it does.
4. Read the repo docs and follow its own tooling and conventions.
5. Do only the scoped work from the issue. If the task grows, stop and comment instead of freelancing expansion.
6. Run the repo's real verification commands.
7. Open a PR and summarize:
- what changed
- how you verified it
- any remaining risk or follow-up
8. Comment on the issue with the PR link and the same concise summary.
GIT / API SETUP:
export GITEA_URL="{gitea_url}"
export GITEA_TOKEN_FILE="{token_file}"
export GITEA_TOKEN="$(tr -d '[:space:]' < "$GITEA_TOKEN_FILE")"
git config --global http."$GITEA_URL/".extraHeader "Authorization: token $GITEA_TOKEN"
git clone "$GITEA_URL/{repo}.git" /tmp/{agent}-work-{issue_num}
cd /tmp/{agent}-work-{issue_num}
git ls-remote --exit-code origin {branch} >/dev/null 2>&1 && git fetch origin {branch} && git checkout {branch} || git checkout -b {branch}
ISSUE FETCH COMMANDS:
curl -s -H "Authorization: token $GITEA_TOKEN" "{gitea_url}/api/v1/repos/{repo}/issues/{issue_num}"
curl -s -H "Authorization: token $GITEA_TOKEN" "{gitea_url}/api/v1/repos/{repo}/issues/{issue_num}/comments"
PR CREATION TEMPLATE:
curl -s -X POST "{gitea_url}/api/v1/repos/{repo}/pulls" \\
-H "Authorization: token $GITEA_TOKEN" \\
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/pulls" \\
-H "Authorization: token ${GITEA_TOKEN}" \\
-H "Content-Type: application/json" \\
-d '{{"title":"[{agent}] <description> (#{issue_num})","body":"Fixes #{issue_num}\\n\\n## Summary\\n- <change>\\n\\n## Verification\\n- <command/output>\\n\\n## Risks\\n- <if any>","head":"{branch}","base":"main"}}'
-d '{"title": "[${AGENT_NAME}] <description> (#${ISSUE_NUM})", "body": "Fixes #${ISSUE_NUM}\n\n<describe changes>", "head": "${BRANCH}", "base": "main"}'
ISSUE COMMENT TEMPLATE:
curl -s -X POST "{gitea_url}/api/v1/repos/{repo}/issues/{issue_num}/comments" \\
-H "Authorization: token $GITEA_TOKEN" \\
== STEP 7: COMMENT ON ISSUE ==
curl -s -X POST "${GITEA_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/${ISSUE_NUM}/comments" \\
-H "Authorization: token ${GITEA_TOKEN}" \\
-H "Content-Type: application/json" \\
-d '{{"body":"PR submitted.\\n\\nSummary:\\n- <change>\\n\\nVerification:\\n- <command/output>\\n\\nRisks:\\n- <if any>"}}'
-d '{"body": "PR submitted. <summary>"}'
REVIEW CHECKLIST BEFORE YOU PUSH:
{review}
COMMIT DISCIPLINE (CRITICAL):
- Commit every 3-5 tool calls. Do NOT wait until the end.
- After every meaningful file change: git add -A && git commit -m "WIP: <what changed>"
- Before running any destructive command: commit current state first.
- If you are unsure whether to commit: commit. WIP commits are safe. Lost work is not.
- Never use --no-verify.
- The auto-commit-guard is your safety net, but do not rely on it. Commit proactively.
RECOVERY COMMANDS (if interrupted, another agent can resume):
git log --oneline -10 # see your WIP commits
git diff HEAD~1 # see what the last commit changed
git status # see uncommitted work
RULES:
- Do not skip hooks with --no-verify.
- Do not silently widen the scope.
- If verification fails twice or the issue is underspecified, stop and comment with what blocked you.
- Always create a PR instead of pushing to main.
- Clean up /tmp/{agent}-work-{issue_num} when done.
"""
print(textwrap.dedent(prompt).strip())
PY
== RULES ==
- Read project docs FIRST.
- Use the project's own test/lint tools.
- Respect git hooks. Do not skip them.
- If tests fail twice, STOP and comment on the issue.
- ALWAYS push your work. ALWAYS create a PR. No exceptions.
- Clean up: remove /tmp/${AGENT_NAME}-work-${ISSUE_NUM} when done.
PROMPT

View File

@@ -1,281 +0,0 @@
#!/usr/bin/env bash
# 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 gemini 1
#
# Dispatches via agent-dispatch.sh, then verifies with genchi-genbutsu.sh.
set -uo pipefail
AGENT="${1:?Usage: agent-loop.sh <agent-name> [num-workers]}"
NUM_WORKERS="${2:-1}"
# Resolve agent tool and model from config or fallback
case "$AGENT" in
claude) TOOL="claude"; MODEL="sonnet" ;;
gemini) TOOL="gemini"; MODEL="gemini-2.5-pro-preview-05-06" ;;
grok) TOOL="opencode"; MODEL="grok-3-fast" ;;
*) TOOL="$AGENT"; MODEL="" ;;
esac
# === CONFIG ===
GITEA_URL="${GITEA_URL:-https://forge.alexanderwhitestone.com}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
WORKTREE_BASE="$HOME/worktrees"
LOG_DIR="$HOME/.hermes/logs"
LOCK_DIR="$LOG_DIR/${AGENT}-locks"
SKIP_FILE="$LOG_DIR/${AGENT}-skip-list.json"
ACTIVE_FILE="$LOG_DIR/${AGENT}-active.json"
TIMEOUT=600
COOLDOWN=30
mkdir -p "$LOG_DIR" "$WORKTREE_BASE" "$LOCK_DIR"
[ -f "$SKIP_FILE" ] || echo '{}' > "$SKIP_FILE"
echo '{}' > "$ACTIVE_FILE"
# === SHARED FUNCTIONS ===
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] ${AGENT}: $*" >> "$LOG_DIR/${AGENT}-loop.log"
}
lock_issue() {
local key="$1"
mkdir "$LOCK_DIR/$key.lock" 2>/dev/null && echo $$ > "$LOCK_DIR/$key.lock/pid"
}
unlock_issue() {
rm -rf "$LOCK_DIR/$1.lock" 2>/dev/null
}
mark_skip() {
local issue_num="$1" reason="$2"
python3 -c "
import json, time, fcntl
with open('${SKIP_FILE}', 'r+') as f:
fcntl.flock(f, fcntl.LOCK_EX)
try: skips = json.load(f)
except: skips = {}
failures = skips.get(str($issue_num), {}).get('failures', 0) + 1
skip_hours = 6 if failures >= 3 else 1
skips[str($issue_num)] = {'until': time.time() + (skip_hours * 3600), 'reason': '$reason', 'failures': failures}
f.seek(0); f.truncate()
json.dump(skips, f, indent=2)
" 2>/dev/null
}
get_next_issue() {
python3 -c "
import json, sys, time, urllib.request, os
token = '${GITEA_TOKEN}'
base = '${GITEA_URL}'
repos = ['Timmy_Foundation/the-nexus', 'Timmy_Foundation/timmy-config', 'Timmy_Foundation/hermes-agent']
try:
with open('${SKIP_FILE}') as f: skips = json.load(f)
except: skips = {}
try:
with open('${ACTIVE_FILE}') as f: active = json.load(f); active_issues = {v['issue'] for v in active.values()}
except: active_issues = set()
all_issues = []
for repo in repos:
url = f'{base}/api/v1/repos/{repo}/issues?state=open&type=issues&limit=50&sort=created'
req = urllib.request.Request(url, headers={'Authorization': f'token {token}'})
try:
resp = urllib.request.urlopen(req, timeout=10)
issues = json.loads(resp.read())
for i in issues: i['_repo'] = repo
all_issues.extend(issues)
except: continue
for i in sorted(all_issues, key=lambda x: x['title'].lower()):
assignees = [a['login'] for a in (i.get('assignees') or [])]
if assignees and '${AGENT}' not in assignees: continue
num_str = str(i['number'])
if num_str in active_issues: continue
if skips.get(num_str, {}).get('until', 0) > time.time(): continue
lock = '${LOCK_DIR}/' + i['_repo'].replace('/', '-') + '-' + num_str + '.lock'
if os.path.isdir(lock): continue
owner, name = i['_repo'].split('/')
print(json.dumps({'number': i['number'], 'title': i['title'], 'repo_owner': owner, 'repo_name': name, 'repo': i['_repo']}))
sys.exit(0)
print('null')
" 2>/dev/null
}
# === WORKER FUNCTION ===
run_worker() {
local worker_id="$1"
log "WORKER-${worker_id}: Started"
while true; do
issue_json=$(get_next_issue)
if [ "$issue_json" = "null" ] || [ -z "$issue_json" ]; then
sleep 30
continue
fi
issue_num=$(echo "$issue_json" | python3 -c "import sys,json; print(json.load(sys.stdin)['number'])")
issue_title=$(echo "$issue_json" | python3 -c "import sys,json; print(json.load(sys.stdin)['title'])")
repo_owner=$(echo "$issue_json" | python3 -c "import sys,json; print(json.load(sys.stdin)['repo_owner'])")
repo_name=$(echo "$issue_json" | python3 -c "import sys,json; print(json.load(sys.stdin)['repo_name'])")
issue_key="${repo_owner}-${repo_name}-${issue_num}"
branch="${AGENT}/issue-${issue_num}"
worktree="${WORKTREE_BASE}/${AGENT}-w${worker_id}-${issue_num}"
if ! lock_issue "$issue_key"; then
sleep 5
continue
fi
log "WORKER-${worker_id}: === ISSUE #${issue_num}: ${issue_title} (${repo_owner}/${repo_name}) ==="
# Clone / checkout
rm -rf "$worktree" 2>/dev/null
CLONE_URL="http://${AGENT}:${GITEA_TOKEN}@143.198.27.163:3000/${repo_owner}/${repo_name}.git"
if git ls-remote --heads "$CLONE_URL" "$branch" 2>/dev/null | grep -q "$branch"; then
git clone --depth=50 -b "$branch" "$CLONE_URL" "$worktree" >/dev/null 2>&1
else
git clone --depth=1 -b main "$CLONE_URL" "$worktree" >/dev/null 2>&1
cd "$worktree" && git checkout -b "$branch" >/dev/null 2>&1
fi
cd "$worktree"
# Generate prompt
prompt=$(bash "$(dirname "$0")/agent-dispatch.sh" "$AGENT" "$issue_num" "${repo_owner}/${repo_name}")
CYCLE_START=$(date +%s)
set +e
if [ "$TOOL" = "claude" ]; then
env -u CLAUDECODE gtimeout "$TIMEOUT" claude \
--print --model "$MODEL" --dangerously-skip-permissions \
-p "$prompt" </dev/null >> "$LOG_DIR/${AGENT}-${issue_num}.log" 2>&1
elif [ "$TOOL" = "gemini" ]; then
gtimeout "$TIMEOUT" gemini -p "$prompt" --yolo \
</dev/null >> "$LOG_DIR/${AGENT}-${issue_num}.log" 2>&1
else
gtimeout "$TIMEOUT" "$TOOL" "$prompt" \
</dev/null >> "$LOG_DIR/${AGENT}-${issue_num}.log" 2>&1
fi
exit_code=$?
set -e
CYCLE_END=$(date +%s)
CYCLE_DURATION=$((CYCLE_END - CYCLE_START))
# --- Mid-session auto-commit: commit before timeout if work is dirty ---
cd "$worktree" 2>/dev/null || true
# Ensure auto-commit-guard is running
if ! pgrep -f "auto-commit-guard.sh" >/dev/null 2>&1; then
log "Starting auto-commit-guard daemon"
nohup bash "$(dirname "$0")/auto-commit-guard.sh" 120 "$WORKTREE_BASE" >> "$LOG_DIR/auto-commit-guard.log" 2>&1 &
fi
# Salvage
cd "$worktree" 2>/dev/null || true
DIRTY=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
if [ "${DIRTY:-0}" -gt 0 ]; then
git add -A 2>/dev/null
git commit -m "WIP: ${AGENT} progress on #${issue_num}
Automated salvage commit — agent session ended (exit $exit_code)." 2>/dev/null || true
fi
UNPUSHED=$(git log --oneline "origin/main..HEAD" 2>/dev/null | wc -l | tr -d ' ')
if [ "${UNPUSHED:-0}" -gt 0 ]; then
git push -u origin "$branch" 2>/dev/null && \
log "WORKER-${worker_id}: Pushed $UNPUSHED commit(s) on $branch" || \
log "WORKER-${worker_id}: Push failed for $branch"
fi
# Create PR if needed
pr_num=$(curl -sf "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls?state=open&head=${repo_owner}:${branch}&limit=1" \
-H "Authorization: token ${GITEA_TOKEN}" | python3 -c "
import sys,json
prs = json.load(sys.stdin)
print(prs[0]['number'] if prs else '')
" 2>/dev/null)
if [ -z "$pr_num" ] && [ "${UNPUSHED:-0}" -gt 0 ]; then
pr_num=$(curl -sf -X POST "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(python3 -c "
import json
print(json.dumps({
'title': '${AGENT}: Issue #${issue_num}',
'head': '${branch}',
'base': 'main',
'body': 'Automated PR for issue #${issue_num}.\nExit code: ${exit_code}'
}))
")" | python3 -c "import sys,json; print(json.load(sys.stdin).get('number',''))" 2>/dev/null)
[ -n "$pr_num" ] && log "WORKER-${worker_id}: Created PR #${pr_num} for issue #${issue_num}"
fi
# ── Genchi Genbutsu: verify world state before declaring success ──
VERIFIED="false"
if [ "$exit_code" -eq 0 ]; then
log "WORKER-${worker_id}: SUCCESS #${issue_num} — running genchi-genbutsu"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if verify_result=$("$SCRIPT_DIR/genchi-genbutsu.sh" "$repo_owner" "$repo_name" "$issue_num" "$branch" "$AGENT" 2>/dev/null); then
VERIFIED="true"
log "WORKER-${worker_id}: VERIFIED #${issue_num}"
if [ -n "$pr_num" ]; then
curl -sf -X POST "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls/${pr_num}/merge" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"Do": "squash"}' >/dev/null 2>&1 || true
curl -sf -X PATCH "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/issues/${issue_num}" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"state": "closed"}' >/dev/null 2>&1 || true
log "WORKER-${worker_id}: PR #${pr_num} merged, issue #${issue_num} closed"
fi
consecutive_failures=0
else
verify_details=$(echo "$verify_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('details','unknown'))" 2>/dev/null || echo "unverified")
log "WORKER-${worker_id}: UNVERIFIED #${issue_num}$verify_details"
mark_skip "$issue_num" "unverified" 1
consecutive_failures=$((consecutive_failures + 1))
fi
elif [ "$exit_code" -eq 124 ]; then
log "WORKER-${worker_id}: TIMEOUT #${issue_num} (work saved in PR)"
consecutive_failures=$((consecutive_failures + 1))
else
log "WORKER-${worker_id}: FAILED #${issue_num} exit ${exit_code} (work saved in PR)"
consecutive_failures=$((consecutive_failures + 1))
fi
# ── METRICS ──
python3 -c "
import json, datetime
print(json.dumps({
'ts': datetime.datetime.utcnow().isoformat() + 'Z',
'agent': '${AGENT}',
'worker': $worker_id,
'issue': $issue_num,
'repo': '${repo_owner}/${repo_name}',
'outcome': 'success' if $exit_code == 0 else 'timeout' if $exit_code == 124 else 'failed',
'exit_code': $exit_code,
'duration_s': $CYCLE_DURATION,
'pr': '${pr_num:-}',
'verified': ${VERIFIED:-false}
}))
" >> "$LOG_DIR/${AGENT}-metrics.jsonl" 2>/dev/null
rm -rf "$worktree" 2>/dev/null
unlock_issue "$issue_key"
sleep "$COOLDOWN"
done
}
# === MAIN ===
log "=== Agent Loop Started — ${AGENT} with ${NUM_WORKERS} worker(s) ==="
rm -rf "$LOCK_DIR"/*.lock 2>/dev/null
for i in $(seq 1 "$NUM_WORKERS"); do
run_worker "$i" &
log "Launched worker $i (PID $!)"
sleep 3
done
wait

View File

@@ -1,159 +0,0 @@
#!/usr/bin/env bash
# auto-commit-guard.sh — Background daemon that auto-commits uncommitted work
#
# Usage: auto-commit-guard.sh [interval_seconds] [worktree_base]
# auto-commit-guard.sh # defaults: 120s, ~/worktrees
# auto-commit-guard.sh 60 # check every 60s
# auto-commit-guard.sh 180 ~/my-worktrees
#
# Scans all git repos under the worktree base for uncommitted changes.
# If dirty for >= 1 check cycle, auto-commits with a WIP message.
# Pushes unpushed commits so work is always recoverable from the remote.
#
# Also scans /tmp for orphaned agent workdirs on startup.
set -uo pipefail
INTERVAL="${1:-120}"
WORKTREE_BASE="${2:-$HOME/worktrees}"
LOG_DIR="$HOME/.hermes/logs"
LOG="$LOG_DIR/auto-commit-guard.log"
PIDFILE="$LOG_DIR/auto-commit-guard.pid"
ORPHAN_SCAN_DONE="$LOG_DIR/.orphan-scan-done"
mkdir -p "$LOG_DIR"
# Single instance guard
if [ -f "$PIDFILE" ]; then
old_pid=$(cat "$PIDFILE")
if kill -0 "$old_pid" 2>/dev/null; then
echo "auto-commit-guard already running (PID $old_pid)" >&2
exit 0
fi
fi
echo $$ > "$PIDFILE"
trap 'rm -f "$PIDFILE"' EXIT
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] AUTO-COMMIT: $*" >> "$LOG"
}
# --- Orphaned workdir scan (runs once on startup) ---
scan_orphans() {
if [ -f "$ORPHAN_SCAN_DONE" ]; then
return 0
fi
log "Scanning /tmp for orphaned agent workdirs..."
local found=0
local rescued=0
for dir in /tmp/*-work-* /tmp/timmy-burn-* /tmp/tc-burn; do
[ -d "$dir" ] || continue
[ -d "$dir/.git" ] || continue
found=$((found + 1))
cd "$dir" 2>/dev/null || continue
local dirty
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d " ")
if [ "${dirty:-0}" -gt 0 ]; then
local branch
branch=$(git branch --show-current 2>/dev/null || echo "orphan")
git add -A 2>/dev/null
if git commit -m "WIP: orphan rescue — $dirty file(s) auto-committed on $(date -u +%Y-%m-%dT%H:%M:%SZ)
Orphaned workdir detected at $dir.
Branch: $branch
Rescued by auto-commit-guard on startup." 2>/dev/null; then
rescued=$((rescued + 1))
log "RESCUED: $dir ($dirty files on branch $branch)"
# Try to push if remote exists
if git remote get-url origin >/dev/null 2>&1; then
git push -u origin "$branch" 2>/dev/null && log "PUSHED orphan rescue: $dir$branch" || log "PUSH FAILED orphan rescue: $dir (no remote access)"
fi
fi
fi
done
log "Orphan scan complete: $found workdirs checked, $rescued rescued"
touch "$ORPHAN_SCAN_DONE"
}
# --- Main guard loop ---
guard_cycle() {
local committed=0
local scanned=0
# Scan worktree base
if [ -d "$WORKTREE_BASE" ]; then
for dir in "$WORKTREE_BASE"/*/; do
[ -d "$dir" ] || continue
[ -d "$dir/.git" ] || continue
scanned=$((scanned + 1))
cd "$dir" 2>/dev/null || continue
local dirty
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d " ")
[ "${dirty:-0}" -eq 0 ] && continue
local branch
branch=$(git branch --show-current 2>/dev/null || echo "detached")
git add -A 2>/dev/null
if git commit -m "WIP: auto-commit — $dirty file(s) on $branch
Automated commit by auto-commit-guard at $(date -u +%Y-%m-%dT%H:%M:%SZ).
Work preserved to prevent loss on crash." 2>/dev/null; then
committed=$((committed + 1))
log "COMMITTED: $dir ($dirty files, branch $branch)"
# Push to preserve remotely
if git remote get-url origin >/dev/null 2>&1; then
git push -u origin "$branch" 2>/dev/null && log "PUSHED: $dir$branch" || log "PUSH FAILED: $dir (will retry next cycle)"
fi
fi
done
fi
# Also scan /tmp for agent workdirs
for dir in /tmp/*-work-*; do
[ -d "$dir" ] || continue
[ -d "$dir/.git" ] || continue
scanned=$((scanned + 1))
cd "$dir" 2>/dev/null || continue
local dirty
dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d " ")
[ "${dirty:-0}" -eq 0 ] && continue
local branch
branch=$(git branch --show-current 2>/dev/null || echo "detached")
git add -A 2>/dev/null
if git commit -m "WIP: auto-commit — $dirty file(s) on $branch
Automated commit by auto-commit-guard at $(date -u +%Y-%m-%dT%H:%M:%SZ).
Agent workdir preserved to prevent loss." 2>/dev/null; then
committed=$((committed + 1))
log "COMMITTED: $dir ($dirty files, branch $branch)"
if git remote get-url origin >/dev/null 2>&1; then
git push -u origin "$branch" 2>/dev/null && log "PUSHED: $dir$branch" || log "PUSH FAILED: $dir (will retry next cycle)"
fi
fi
done
[ "$committed" -gt 0 ] && log "Cycle done: $scanned scanned, $committed committed"
}
# --- Entry point ---
log "Starting auto-commit-guard (interval=${INTERVAL}s, worktree=${WORKTREE_BASE})"
scan_orphans
while true; do
guard_cycle
sleep "$INTERVAL"
done

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

@@ -11,7 +11,7 @@ set -euo pipefail
NUM_WORKERS="${1:-2}"
MAX_WORKERS=10 # absolute ceiling
WORKTREE_BASE="$HOME/worktrees"
GITEA_URL="${GITEA_URL:-https://forge.alexanderwhitestone.com}"
GITEA_URL="http://143.198.27.163:3000"
GITEA_TOKEN=$(cat "$HOME/.hermes/claude_token")
CLAUDE_TIMEOUT=900 # 15 min per issue
COOLDOWN=15 # seconds between issues — stagger clones
@@ -468,32 +468,24 @@ print(json.dumps({
[ -n "$pr_num" ] && log "WORKER-${worker_id}: Created PR #${pr_num} for issue #${issue_num}"
fi
# ── Genchi Genbutsu: verify world state before declaring success ──
VERIFIED="false"
# ── Merge + close on success ──
if [ "$exit_code" -eq 0 ]; then
log "WORKER-${worker_id}: SUCCESS #${issue_num} — running genchi-genbutsu"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if verify_result=$("$SCRIPT_DIR/genchi-genbutsu.sh" "$repo_owner" "$repo_name" "$issue_num" "$branch" "claude" 2>/dev/null); then
VERIFIED="true"
log "WORKER-${worker_id}: VERIFIED #${issue_num}"
if [ -n "$pr_num" ]; then
curl -sf -X POST "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls/${pr_num}/merge" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"Do": "squash"}' >/dev/null 2>&1 || true
curl -sf -X PATCH "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/issues/${issue_num}" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"state": "closed"}' >/dev/null 2>&1 || true
log "WORKER-${worker_id}: PR #${pr_num} merged, issue #${issue_num} closed"
fi
consecutive_failures=0
else
verify_details=$(echo "$verify_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('details','unknown'))" 2>/dev/null || echo "unverified")
log "WORKER-${worker_id}: UNVERIFIED #${issue_num}$verify_details"
consecutive_failures=$((consecutive_failures + 1))
log "WORKER-${worker_id}: SUCCESS #${issue_num}"
if [ -n "$pr_num" ]; then
curl -sf -X POST "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls/${pr_num}/merge" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"Do": "squash"}' >/dev/null 2>&1 || true
curl -sf -X PATCH "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/issues/${issue_num}" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"state": "closed"}' >/dev/null 2>&1 || true
log "WORKER-${worker_id}: PR #${pr_num} merged, issue #${issue_num} closed"
fi
consecutive_failures=0
elif [ "$exit_code" -eq 124 ]; then
log "WORKER-${worker_id}: TIMEOUT #${issue_num} (work saved in PR)"
consecutive_failures=$((consecutive_failures + 1))
@@ -530,7 +522,6 @@ print(json.dumps({
import json, datetime
print(json.dumps({
'ts': datetime.datetime.utcnow().isoformat() + 'Z',
'agent': 'claude',
'worker': $worker_id,
'issue': $issue_num,
'repo': '${repo_owner}/${repo_name}',
@@ -543,8 +534,7 @@ print(json.dumps({
'lines_removed': ${LINES_REMOVED:-0},
'salvaged': ${DIRTY:-0},
'pr': '${pr_num:-}',
'merged': $( [ '$OUTCOME' = 'success' ] && [ -n '${pr_num:-}' ] && echo 'true' || echo 'false' ),
'verified': ${VERIFIED:-false}
'merged': $( [ '$OUTCOME' = 'success' ] && [ -n '${pr_num:-}' ] && echo 'true' || echo 'false' )
}))
" >> "$METRICS_FILE" 2>/dev/null

View File

@@ -5,7 +5,7 @@ set -uo pipefail
export PATH="/opt/homebrew/bin:$HOME/.local/bin:$HOME/.hermes/bin:/usr/local/bin:$PATH"
LOG="$HOME/.hermes/logs/claudemax-watchdog.log"
GITEA_URL="https://forge.alexanderwhitestone.com"
GITEA_URL="http://143.198.27.163:3000"
GITEA_TOKEN=$(tr -d '[:space:]' < "$HOME/.hermes/gitea_token_vps" 2>/dev/null || true)
REPO_API="$GITEA_URL/api/v1/repos/Timmy_Foundation/the-nexus"
MIN_OPEN_ISSUES=10

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

@@ -9,7 +9,7 @@ THRESHOLD_HOURS="${1:-2}"
THRESHOLD_SECS=$((THRESHOLD_HOURS * 3600))
LOG_DIR="$HOME/.hermes/logs"
LOG_FILE="$LOG_DIR/deadman.log"
GITEA_URL="https://forge.alexanderwhitestone.com"
GITEA_URL="http://143.198.27.163:3000"
GITEA_TOKEN=$(cat "$HOME/.hermes/gitea_token_vps" 2>/dev/null || echo "")
TELEGRAM_TOKEN=$(cat "$HOME/.config/telegram/special_bot" 2>/dev/null || echo "")
TELEGRAM_CHAT="-1003664764329"

View File

@@ -25,35 +25,10 @@ else
fi
# ── Config ──
GITEA_TOKEN=$(cat ~/.hermes/gitea_token_vps 2>/dev/null || echo "")
GITEA_API="https://forge.alexanderwhitestone.com/api/v1"
# Resolve Tailscale IPs dynamically; fallback to env vars
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
RESOLVER="${SCRIPT_DIR}/../tools/tailscale_ip_resolver.py"
if [ ! -f "$RESOLVER" ]; then
RESOLVER="/root/wizards/ezra/tools/tailscale_ip_resolver.py"
fi
resolve_host() {
local default_ip="$1"
if [ -n "$TAILSCALE_IP" ]; then
echo "root@${TAILSCALE_IP}"
return
fi
if [ -f "$RESOLVER" ]; then
local ip
ip=$(python3 "$RESOLVER" 2>/dev/null)
if [ -n "$ip" ]; then
echo "root@${ip}"
return
fi
fi
echo "root@${default_ip}"
}
EZRA_HOST=$(resolve_host "143.198.27.163")
BEZALEL_HOST="root@${BEZALEL_TAILSCALE_IP:-67.205.155.108}"
GITEA_TOKEN=$(cat ~/.hermes/gitea_token_vps 2>/dev/null)
GITEA_API="http://143.198.27.163:3000/api/v1"
EZRA_HOST="root@143.198.27.163"
BEZALEL_HOST="root@67.205.155.108"
SSH_OPTS="-o ConnectTimeout=4 -o StrictHostKeyChecking=no -o BatchMode=yes"
ANY_DOWN=0
@@ -179,7 +154,7 @@ fi
print_line "Timmy" "$TIMMY_STATUS" "$TIMMY_MODEL" "$TIMMY_ACTIVITY"
# ── 2. Ezra ──
# ── 2. Ezra (VPS 143.198.27.163) ──
EZRA_STATUS="DOWN"
EZRA_MODEL="hermes-ezra"
EZRA_ACTIVITY=""
@@ -211,7 +186,7 @@ fi
print_line "Ezra" "$EZRA_STATUS" "$EZRA_MODEL" "$EZRA_ACTIVITY"
# ── 3. Bezalel ──
# ── 3. Bezalel (VPS 67.205.155.108) ──
BEZ_STATUS="DOWN"
BEZ_MODEL="hermes-bezalel"
BEZ_ACTIVITY=""
@@ -271,7 +246,7 @@ if [ -n "$GITEA_VER" ]; then
GITEA_STATUS="UP"
VER=$(python3 -c "import json; print(json.loads('''${GITEA_VER}''').get('version','?'))" 2>/dev/null)
GITEA_MODEL="gitea v${VER}"
GITEA_ACTIVITY="forge.alexanderwhitestone.com"
GITEA_ACTIVITY="143.198.27.163:3000"
else
GITEA_STATUS="DOWN"
GITEA_MODEL="gitea(unreachable)"

View File

@@ -7,26 +7,13 @@
set -euo pipefail
GEMINI_KEY_FILE="${GEMINI_KEY_FILE:-$HOME/.timmy/gemini_free_tier_key}"
if [ -f "$GEMINI_KEY_FILE" ]; then
export GEMINI_API_KEY="$(python3 - "$GEMINI_KEY_FILE" <<'PY'
from pathlib import Path
import sys
text = Path(sys.argv[1]).read_text(errors='ignore').splitlines()
for line in text:
line=line.strip()
if line:
print(line)
break
PY
)"
fi
export GEMINI_API_KEY="AIzaSyAmGgS516K4PwlODFEnghL535yzoLnofKM"
# === CONFIG ===
NUM_WORKERS="${1:-2}"
MAX_WORKERS=5
WORKTREE_BASE="$HOME/worktrees"
GITEA_URL="${GITEA_URL:-https://forge.alexanderwhitestone.com}"
GITEA_URL="http://143.198.27.163:3000"
GITEA_TOKEN=$(cat "$HOME/.hermes/gemini_token")
GEMINI_TIMEOUT=600 # 10 min per issue
COOLDOWN=15 # seconds between issues — stagger clones
@@ -37,7 +24,6 @@ SKIP_FILE="$LOG_DIR/gemini-skip-list.json"
LOCK_DIR="$LOG_DIR/gemini-locks"
ACTIVE_FILE="$LOG_DIR/gemini-active.json"
ALLOW_SELF_ASSIGN="${ALLOW_SELF_ASSIGN:-0}" # 0 = only explicitly-assigned Gemini work
AUTH_INVALID_SLEEP=900
mkdir -p "$LOG_DIR" "$WORKTREE_BASE" "$LOCK_DIR"
[ -f "$SKIP_FILE" ] || echo '{}' > "$SKIP_FILE"
@@ -48,124 +34,6 @@ log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_DIR/gemini-loop.log"
}
post_issue_comment() {
local repo_owner="$1" repo_name="$2" issue_num="$3" body="$4"
local payload
payload=$(python3 - "$body" <<'PY'
import json, sys
print(json.dumps({"body": sys.argv[1]}))
PY
)
curl -sf -X POST "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/issues/${issue_num}/comments" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "$payload" >/dev/null 2>&1 || true
}
remote_branch_exists() {
local branch="$1"
git ls-remote --heads origin "$branch" 2>/dev/null | grep -q .
}
get_pr_num() {
local repo_owner="$1" repo_name="$2" branch="$3"
curl -sf "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls?state=all&head=${repo_owner}:${branch}&limit=1" -H "Authorization: token ${GITEA_TOKEN}" | python3 -c "
import sys,json
prs = json.load(sys.stdin)
if prs: print(prs[0]['number'])
else: print('')
" 2>/dev/null
}
get_pr_file_count() {
local repo_owner="$1" repo_name="$2" pr_num="$3"
curl -sf "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls/${pr_num}/files" -H "Authorization: token ${GITEA_TOKEN}" | python3 -c "
import sys, json
try:
files = json.load(sys.stdin)
print(len(files) if isinstance(files, list) else 0)
except:
print(0)
" 2>/dev/null
}
get_pr_state() {
local repo_owner="$1" repo_name="$2" pr_num="$3"
curl -sf "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls/${pr_num}" -H "Authorization: token ${GITEA_TOKEN}" | python3 -c "
import sys, json
try:
pr = json.load(sys.stdin)
if pr.get('merged'):
print('merged')
else:
print(pr.get('state', 'unknown'))
except:
print('unknown')
" 2>/dev/null
}
get_issue_state() {
local repo_owner="$1" repo_name="$2" issue_num="$3"
curl -sf "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/issues/${issue_num}" -H "Authorization: token ${GITEA_TOKEN}" | python3 -c "
import sys, json
try:
issue = json.load(sys.stdin)
print(issue.get('state', 'unknown'))
except:
print('unknown')
" 2>/dev/null
}
proof_comment_status() {
local repo_owner="$1" repo_name="$2" issue_num="$3" branch="$4"
curl -sf "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/issues/${issue_num}/comments" -H "Authorization: token ${GITEA_TOKEN}" | BRANCH="$branch" python3 -c "
import os, sys, json
branch = os.environ.get('BRANCH', '').lower()
try:
comments = json.load(sys.stdin)
except Exception:
print('missing|')
raise SystemExit(0)
for c in reversed(comments):
user = ((c.get('user') or {}).get('login') or '').lower()
body = c.get('body') or ''
body_l = body.lower()
if user != 'gemini':
continue
if 'proof:' not in body_l and 'verification:' not in body_l:
continue
has_branch = branch in body_l
has_pr = ('pr:' in body_l) or ('pull request:' in body_l) or ('/pulls/' in body_l)
has_push = ('push:' in body_l) or ('pushed' in body_l)
has_verify = ('tox' in body_l) or ('pytest' in body_l) or ('verification:' in body_l) or ('npm test' in body_l)
status = 'ok' if (has_branch and has_pr and has_push and has_verify) else 'incomplete'
print(status + '|' + (c.get('html_url') or ''))
raise SystemExit(0)
print('missing|')
" 2>/dev/null
}
gemini_auth_invalid() {
local issue_num="$1"
grep -q "API_KEY_INVALID\|API key expired" "$LOG_DIR/gemini-${issue_num}.log" 2>/dev/null
}
issue_is_code_fit() {
local title="$1"
local labels="$2"
local body="$3"
local haystack
haystack="${title} ${labels} ${body}"
local low="${haystack,,}"
if [[ "$low" == *"[morning report]"* ]]; then return 1; fi
if [[ "$low" == *"[kt]"* ]]; then return 1; fi
if [[ "$low" == *"policy:"* ]]; then return 1; fi
if [[ "$low" == *"incident:"* || "$low" == *"🚨 incident"* || "$low" == *"[incident]"* ]]; then return 1; fi
if [[ "$low" == *"fleet lexicon"* || "$low" == *"shared vocabulary"* || "$low" == *"rubric"* ]]; then return 1; fi
if [[ "$low" == *"archive ghost"* || "$low" == *"reassign"* || "$low" == *"offload"* || "$low" == *"burn directive"* ]]; then return 1; fi
if [[ "$low" == *"review all open prs"* ]]; then return 1; fi
if [[ "$low" == *"epic"* ]]; then return 1; fi
return 0
}
lock_issue() {
local issue_key="$1"
local lockfile="$LOCK_DIR/$issue_key.lock"
@@ -222,7 +90,6 @@ with open('$ACTIVE_FILE', 'r+') as f:
cleanup_workdir() {
local wt="$1"
cd "$HOME" 2>/dev/null || true
rm -rf "$wt" 2>/dev/null || true
}
@@ -287,11 +154,8 @@ for i in all_issues:
continue
title = i['title'].lower()
labels = [l['name'].lower() for l in (i.get('labels') or [])]
body = (i.get('body') or '').lower()
if '[philosophy]' in title: continue
if '[epic]' in title or 'epic:' in title: continue
if 'epic' in labels: continue
if '[showcase]' in title: continue
if '[do not close' in title: continue
if '[meta]' in title: continue
@@ -300,11 +164,6 @@ for i in all_issues:
if '[morning report]' in title: continue
if '[retro]' in title: continue
if '[intel]' in title: continue
if '[kt]' in title: continue
if 'policy:' in title: continue
if 'incident' in title: continue
if 'lexicon' in title or 'shared vocabulary' in title or 'rubric' in title: continue
if 'archive ghost' in title or 'reassign' in title or 'offload' in title: continue
if 'master escalation' in title: continue
if any(a['login'] == 'Rockachopa' for a in (i.get('assignees') or [])): continue
@@ -391,11 +250,10 @@ You can do ANYTHING a developer can do.
- If tests fail after 2 attempts, STOP and comment on the issue explaining why.
- Be thorough but focused. Fix the issue, don't refactor the world.
== CRITICAL: FINISH = PUSHED + PR'D + PROVED ==
== CRITICAL: ALWAYS COMMIT AND PUSH ==
- NEVER exit without committing your work. Even partial progress MUST be committed.
- Before you finish, ALWAYS: git add -A && git commit && git push origin gemini/issue-${issue_num}
- ALWAYS create a PR before exiting. No exceptions.
- ALWAYS post the Proof block before exiting. No proof comment = not done.
- If a branch already exists with prior work, check it out and CONTINUE from where it left off.
- Check: git ls-remote origin gemini/issue-${issue_num} — if it exists, pull it first.
- Your work is WASTED if it's not pushed. Push early, push often.
@@ -506,10 +364,19 @@ Work in progress, may need continuation." 2>/dev/null || true
fi
# ── Create PR if needed ──
pr_num=$(get_pr_num "$repo_owner" "$repo_name" "$branch")
pr_num=$(curl -sf "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls?state=open&head=${repo_owner}:${branch}&limit=1" \
-H "Authorization: token ${GITEA_TOKEN}" | python3 -c "
import sys,json
prs = json.load(sys.stdin)
if prs: print(prs[0]['number'])
else: print('')
" 2>/dev/null)
if [ -z "$pr_num" ] && [ "${UNPUSHED:-0}" -gt 0 ]; then
pr_num=$(curl -sf -X POST "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls" -H "Authorization: token ${GITEA_TOKEN}" -H "Content-Type: application/json" -d "$(python3 -c "
pr_num=$(curl -sf -X POST "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$(python3 -c "
import json
print(json.dumps({
'title': 'Gemini: Issue #${issue_num}',
@@ -521,74 +388,26 @@ print(json.dumps({
[ -n "$pr_num" ] && log "WORKER-${worker_id}: Created PR #${pr_num} for issue #${issue_num}"
fi
# ── Genchi Genbutsu: verify world state before declaring success ──
VERIFIED="false"
# ── Merge + close on success ──
if [ "$exit_code" -eq 0 ]; then
log "WORKER-${worker_id}: SUCCESS #${issue_num} exited 0 — running genchi-genbutsu"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if verify_result=$("$SCRIPT_DIR/genchi-genbutsu.sh" "$repo_owner" "$repo_name" "$issue_num" "$branch" "gemini" 2>/dev/null); then
VERIFIED="true"
log "WORKER-${worker_id}: VERIFIED #${issue_num}"
pr_state=$(get_pr_state "$repo_owner" "$repo_name" "$pr_num")
if [ "$pr_state" = "open" ]; then
curl -sf -X POST "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls/${pr_num}/merge" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"Do": "squash"}' >/dev/null 2>&1 || true
pr_state=$(get_pr_state "$repo_owner" "$repo_name" "$pr_num")
fi
if [ "$pr_state" = "merged" ]; then
curl -sf -X PATCH "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/issues/${issue_num}" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"state": "closed"}' >/dev/null 2>&1 || true
issue_state=$(get_issue_state "$repo_owner" "$repo_name" "$issue_num")
if [ "$issue_state" = "closed" ]; then
log "WORKER-${worker_id}: VERIFIED #${issue_num} branch pushed, PR merged, comment present, issue closed"
consecutive_failures=0
else
log "WORKER-${worker_id}: BLOCKED #${issue_num} issue did not close after merge"
mark_skip "$issue_num" "issue_close_unverified" 1
consecutive_failures=$((consecutive_failures + 1))
fi
else
log "WORKER-${worker_id}: BLOCKED #${issue_num} merge not verified (state=${pr_state})"
mark_skip "$issue_num" "merge_unverified" 1
consecutive_failures=$((consecutive_failures + 1))
fi
else
verify_details=$(echo "$verify_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('details','unknown'))" 2>/dev/null || echo "unverified")
verify_checks=$(echo "$verify_result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('checks',''))" 2>/dev/null || echo "")
log "WORKER-${worker_id}: UNVERIFIED #${issue_num}$verify_details"
if echo "$verify_checks" | grep -q '"branch": false'; then
post_issue_comment "$repo_owner" "$repo_name" "$issue_num" "Loop gate blocked completion: remote branch ${branch} was not found on origin after Gemini exited. Issue remains open for retry."
mark_skip "$issue_num" "missing_remote_branch" 1
elif echo "$verify_checks" | grep -q '"pr": false'; then
post_issue_comment "$repo_owner" "$repo_name" "$issue_num" "Loop gate blocked completion: branch ${branch} exists remotely, but no PR was found. Issue remains open for retry."
mark_skip "$issue_num" "missing_pr" 1
elif echo "$verify_checks" | grep -q '"files": false'; then
curl -sf -X PATCH "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls/${pr_num}" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"state": "closed"}' >/dev/null 2>&1 || true
post_issue_comment "$repo_owner" "$repo_name" "$issue_num" "PR #${pr_num} was closed automatically: it had 0 changed files (empty commit). Issue remains open for retry."
mark_skip "$issue_num" "empty_commit" 2
else
post_issue_comment "$repo_owner" "$repo_name" "$issue_num" "Loop gate blocked completion: PR #${pr_num} exists, but required verification failed ($verify_details). Issue remains open for retry."
mark_skip "$issue_num" "unverified" 1
fi
consecutive_failures=$((consecutive_failures + 1))
log "WORKER-${worker_id}: SUCCESS #${issue_num}"
if [ -n "$pr_num" ]; then
curl -sf -X POST "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls/${pr_num}/merge" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"Do": "squash"}' >/dev/null 2>&1 || true
curl -sf -X PATCH "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/issues/${issue_num}" \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"state": "closed"}' >/dev/null 2>&1 || true
log "WORKER-${worker_id}: PR #${pr_num} merged, issue #${issue_num} closed"
fi
consecutive_failures=0
elif [ "$exit_code" -eq 124 ]; then
log "WORKER-${worker_id}: TIMEOUT #${issue_num} (work saved in PR)"
consecutive_failures=$((consecutive_failures + 1))
else
if gemini_auth_invalid "$issue_num"; then
log "WORKER-${worker_id}: AUTH INVALID on #${issue_num} — sleeping ${AUTH_INVALID_SLEEP}s"
mark_skip "$issue_num" "gemini_auth_invalid" 1
sleep "$AUTH_INVALID_SLEEP"
consecutive_failures=$((consecutive_failures + 5))
elif grep -q "rate_limit\|rate limit\|429\|overloaded\|quota" "$LOG_DIR/gemini-${issue_num}.log" 2>/dev/null; then
if grep -q "rate_limit\|rate limit\|429\|overloaded\|quota" "$LOG_DIR/gemini-${issue_num}.log" 2>/dev/null; then
log "WORKER-${worker_id}: RATE LIMITED on #${issue_num} (work saved)"
consecutive_failures=$((consecutive_failures + 3))
else
@@ -623,10 +442,9 @@ print(json.dumps({
'lines_removed': ${LINES_REMOVED:-0},
'salvaged': ${DIRTY:-0},
'pr': '${pr_num:-}',
'merged': $( [ '$OUTCOME' = 'success' ] && [ -n '${pr_num:-}' ] && echo 'true' || echo 'false' ),
'verified': ${VERIFIED:-false}
'merged': $( [ '$OUTCOME' = 'success' ] && [ -n '${pr_num:-}' ] && echo 'true' || echo 'false' )
}))
" >> "$LOG_DIR/gemini-metrics.jsonl" 2>/dev/null
" >> "$LOG_DIR/claude-metrics.jsonl" 2>/dev/null
cleanup_workdir "$worktree"
unlock_issue "$issue_key"

View File

@@ -1,179 +0,0 @@
#!/usr/bin/env bash
# genchi-genbutsu.sh — 現地現物 — Go and see. Verify world state, not log vibes.
#
# Post-completion verification that goes and LOOKS at the actual artifacts.
# Performs 5 world-state checks:
# 1. Branch exists on remote
# 2. PR exists
# 3. PR has real file changes (> 0)
# 4. PR is mergeable
# 5. Issue has a completion comment from the agent
#
# Usage: genchi-genbutsu.sh <repo_owner> <repo_name> <issue_num> <branch> <agent_name>
# Returns: JSON to stdout, logs JSONL, exit 0 = VERIFIED, exit 1 = UNVERIFIED
set -euo pipefail
GITEA_URL="${GITEA_URL:-https://forge.alexanderwhitestone.com}"
GITEA_TOKEN="${GITEA_TOKEN:-}"
LOG_DIR="${LOG_DIR:-$HOME/.hermes/logs}"
VERIFY_LOG="$LOG_DIR/genchi-genbutsu.jsonl"
if [ $# -lt 5 ]; then
echo "Usage: $0 <repo_owner> <repo_name> <issue_num> <branch> <agent_name>" >&2
exit 2
fi
repo_owner="$1"
repo_name="$2"
issue_num="$3"
branch="$4"
agent_name="$5"
mkdir -p "$LOG_DIR"
# ── Helpers ──────────────────────────────────────────────────────────
check_branch_exists() {
# Use Gitea API instead of git ls-remote so we don't need clone credentials
curl -sf "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/branches/${branch}" \
-H "Authorization: token ${GITEA_TOKEN}" >/dev/null 2>&1
}
get_pr_num() {
curl -sf "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls?state=all&head=${repo_owner}:${branch}&limit=1" \
-H "Authorization: token ${GITEA_TOKEN}" 2>/dev/null | python3 -c "
import sys, json
prs = json.load(sys.stdin)
print(prs[0]['number'] if prs else '')
"
}
check_pr_files() {
local pr_num="$1"
curl -sf "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls/${pr_num}/files" \
-H "Authorization: token ${GITEA_TOKEN}" 2>/dev/null | python3 -c "
import sys, json
try:
files = json.load(sys.stdin)
print(len(files) if isinstance(files, list) else 0)
except:
print(0)
"
}
check_pr_mergeable() {
local pr_num="$1"
curl -sf "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/pulls/${pr_num}" \
-H "Authorization: token ${GITEA_TOKEN}" 2>/dev/null | python3 -c "
import sys, json
pr = json.load(sys.stdin)
print('true' if pr.get('mergeable') else 'false')
"
}
check_completion_comment() {
curl -sf "${GITEA_URL}/api/v1/repos/${repo_owner}/${repo_name}/issues/${issue_num}/comments" \
-H "Authorization: token ${GITEA_TOKEN}" 2>/dev/null | AGENT="$agent_name" python3 -c "
import os, sys, json
agent = os.environ.get('AGENT', '').lower()
try:
comments = json.load(sys.stdin)
except:
sys.exit(1)
for c in reversed(comments):
user = ((c.get('user') or {}).get('login') or '').lower()
if user == agent:
sys.exit(0)
sys.exit(1)
"
}
# ── Run checks ───────────────────────────────────────────────────────
ts=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
status="VERIFIED"
details=()
checks_json='{}'
# Check 1: branch
if check_branch_exists; then
checks_json=$(echo "$checks_json" | python3 -c "import sys,json;d=json.load(sys.stdin);d['branch']=True;print(json.dumps(d))")
else
checks_json=$(echo "$checks_json" | python3 -c "import sys,json;d=json.load(sys.stdin);d['branch']=False;print(json.dumps(d))")
status="UNVERIFIED"
details+=("remote branch ${branch} not found")
fi
# Check 2: PR exists
pr_num=$(get_pr_num)
if [ -n "$pr_num" ]; then
checks_json=$(echo "$checks_json" | python3 -c "import sys,json;d=json.load(sys.stdin);d['pr']=True;print(json.dumps(d))")
else
checks_json=$(echo "$checks_json" | python3 -c "import sys,json;d=json.load(sys.stdin);d['pr']=False;print(json.dumps(d))")
status="UNVERIFIED"
details+=("no PR found for branch ${branch}")
fi
# Check 3: PR has real file changes
if [ -n "$pr_num" ]; then
file_count=$(check_pr_files "$pr_num")
if [ "${file_count:-0}" -gt 0 ]; then
checks_json=$(echo "$checks_json" | python3 -c "import sys,json;d=json.load(sys.stdin);d['files']=True;print(json.dumps(d))")
else
checks_json=$(echo "$checks_json" | python3 -c "import sys,json;d=json.load(sys.stdin);d['files']=False;print(json.dumps(d))")
status="UNVERIFIED"
details+=("PR #${pr_num} has 0 changed files")
fi
# Check 4: PR is mergeable
if [ "$(check_pr_mergeable "$pr_num")" = "true" ]; then
checks_json=$(echo "$checks_json" | python3 -c "import sys,json;d=json.load(sys.stdin);d['mergeable']=True;print(json.dumps(d))")
else
checks_json=$(echo "$checks_json" | python3 -c "import sys,json;d=json.load(sys.stdin);d['mergeable']=False;print(json.dumps(d))")
status="UNVERIFIED"
details+=("PR #${pr_num} is not mergeable")
fi
else
checks_json=$(echo "$checks_json" | python3 -c "import sys,json;d=json.load(sys.stdin);d['files']=None;d['mergeable']=None;print(json.dumps(d))")
fi
# Check 5: completion comment from agent
if check_completion_comment; then
checks_json=$(echo "$checks_json" | python3 -c "import sys,json;d=json.load(sys.stdin);d['comment']=True;print(json.dumps(d))")
else
checks_json=$(echo "$checks_json" | python3 -c "import sys,json;d=json.load(sys.stdin);d['comment']=False;print(json.dumps(d))")
status="UNVERIFIED"
details+=("no completion comment from ${agent_name} on issue #${issue_num}")
fi
# Build detail string
detail_str=$(IFS="; "; echo "${details[*]:-all checks passed}")
# ── Output ───────────────────────────────────────────────────────────
result=$(python3 -c "
import json
print(json.dumps({
'status': '$status',
'repo': '${repo_owner}/${repo_name}',
'issue': $issue_num,
'branch': '$branch',
'agent': '$agent_name',
'pr': '$pr_num',
'checks': $checks_json,
'details': '$detail_str',
'ts': '$ts'
}, indent=2))
")
printf '%s\n' "$result"
# Append to JSONL log
printf '%s\n' "$result" >> "$VERIFY_LOG"
if [ "$status" = "VERIFIED" ]; then
exit 0
else
exit 1
fi

View File

@@ -1,297 +0,0 @@
"""
Glitch pattern definitions for 3D world anomaly detection.
Defines known visual artifact categories commonly found in 3D web worlds,
particularly The Matrix environments. Each pattern includes detection
heuristics and severity ratings.
"""
from dataclasses import dataclass, field
from enum import Enum
from typing import Optional
class GlitchSeverity(Enum):
CRITICAL = "critical"
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
INFO = "info"
class GlitchCategory(Enum):
FLOATING_ASSETS = "floating_assets"
Z_FIGHTING = "z_fighting"
MISSING_TEXTURES = "missing_textures"
CLIPPING = "clipping"
BROKEN_NORMALS = "broken_normals"
SHADOW_ARTIFACTS = "shadow_artifacts"
LIGHTMAP_ERRORS = "lightmap_errors"
LOD_POPPING = "lod_popping"
WATER_REFLECTION = "water_reflection"
SKYBOX_SEAM = "skybox_seam"
@dataclass
class GlitchPattern:
"""Definition of a known glitch pattern with detection parameters."""
category: GlitchCategory
name: str
description: str
severity: GlitchSeverity
detection_prompts: list[str]
visual_indicators: list[str]
confidence_threshold: float = 0.6
def to_dict(self) -> dict:
return {
"category": self.category.value,
"name": self.name,
"description": self.description,
"severity": self.severity.value,
"detection_prompts": self.detection_prompts,
"visual_indicators": self.visual_indicators,
"confidence_threshold": self.confidence_threshold,
}
# Known glitch patterns for Matrix 3D world scanning
MATRIX_GLITCH_PATTERNS: list[GlitchPattern] = [
GlitchPattern(
category=GlitchCategory.FLOATING_ASSETS,
name="Floating Object",
description="Object not properly grounded or anchored to the scene geometry. "
"Common in procedurally placed assets or after physics desync.",
severity=GlitchSeverity.HIGH,
detection_prompts=[
"Identify any objects that appear to float above the ground without support.",
"Look for furniture, props, or geometry suspended in mid-air with no visible attachment.",
"Check for objects whose shadows do not align with the surface below them.",
],
visual_indicators=[
"gap between object base and surface",
"shadow detached from object",
"object hovering with no structural support",
],
confidence_threshold=0.65,
),
GlitchPattern(
category=GlitchCategory.Z_FIGHTING,
name="Z-Fighting Flicker",
description="Two coplanar surfaces competing for depth priority, causing "
"visible flickering or shimmering textures.",
severity=GlitchSeverity.MEDIUM,
detection_prompts=[
"Look for surfaces that appear to shimmer, flicker, or show mixed textures.",
"Identify areas where two textures seem to overlap and compete for visibility.",
"Check walls, floors, or objects for surface noise or pattern interference.",
],
visual_indicators=[
"shimmering surface",
"texture flicker between two patterns",
"noisy flat surfaces",
"moire-like patterns on planar geometry",
],
confidence_threshold=0.55,
),
GlitchPattern(
category=GlitchCategory.MISSING_TEXTURES,
name="Missing or Placeholder Texture",
description="A surface rendered with a fallback checkerboard, solid magenta, "
"or the default engine placeholder texture.",
severity=GlitchSeverity.CRITICAL,
detection_prompts=[
"Look for bright magenta, checkerboard, or solid-color surfaces that look out of place.",
"Identify any surfaces that appear as flat untextured colors inconsistent with the scene.",
"Check for black, white, or magenta patches where detailed textures should be.",
],
visual_indicators=[
"magenta/pink solid color surface",
"checkerboard pattern",
"flat single-color geometry",
"UV-debug texture visible",
],
confidence_threshold=0.7,
),
GlitchPattern(
category=GlitchCategory.CLIPPING,
name="Geometry Clipping",
description="Objects passing through each other or intersecting in physically "
"impossible ways due to collision mesh errors.",
severity=GlitchSeverity.HIGH,
detection_prompts=[
"Look for objects that visibly pass through other objects (walls, floors, furniture).",
"Identify characters or props embedded inside geometry where they should not be.",
"Check for intersecting meshes where solid objects overlap unnaturally.",
],
visual_indicators=[
"object passing through wall or floor",
"embedded geometry",
"overlapping solid meshes",
"character limb inside furniture",
],
confidence_threshold=0.6,
),
GlitchPattern(
category=GlitchCategory.BROKEN_NORMALS,
name="Broken Surface Normals",
description="Inverted or incorrect surface normals causing faces to appear "
"inside-out, invisible from certain angles, or lit incorrectly.",
severity=GlitchSeverity.MEDIUM,
detection_prompts=[
"Look for surfaces that appear dark or black on one side while lit on the other.",
"Identify objects that seem to vanish when viewed from certain angles.",
"Check for inverted shading where lit areas should be in shadow.",
],
visual_indicators=[
"dark/unlit face on otherwise lit model",
"invisible surface from one direction",
"inverted shadow gradient",
"inside-out appearance",
],
confidence_threshold=0.5,
),
GlitchPattern(
category=GlitchCategory.SHADOW_ARTIFACTS,
name="Shadow Artifact",
description="Broken, detached, or incorrectly rendered shadows that do not "
"match the casting geometry or scene lighting.",
severity=GlitchSeverity.LOW,
detection_prompts=[
"Look for shadows that do not match the shape of nearby objects.",
"Identify shadow acne: banding or striped patterns on surfaces.",
"Check for floating shadows detached from any visible caster.",
],
visual_indicators=[
"shadow shape mismatch",
"shadow acne bands",
"detached floating shadow",
"Peter Panning (shadow offset from base)",
],
confidence_threshold=0.5,
),
GlitchPattern(
category=GlitchCategory.LOD_POPPING,
name="LOD Transition Pop",
description="Visible pop-in when level-of-detail models switch abruptly, "
"causing geometry or textures to change suddenly.",
severity=GlitchSeverity.LOW,
detection_prompts=[
"Look for areas where mesh detail changes abruptly at visible boundaries.",
"Identify objects that appear to morph or shift geometry suddenly.",
"Check for texture resolution changes that create visible seams.",
],
visual_indicators=[
"visible mesh simplification boundary",
"texture resolution jump",
"geometry pop-in artifacts",
],
confidence_threshold=0.45,
),
GlitchPattern(
category=GlitchCategory.LIGHTMAP_ERRORS,
name="Lightmap Baking Error",
description="Incorrect or missing baked lighting causing dark spots, light "
"leaks, or mismatched illumination on static geometry.",
severity=GlitchSeverity.MEDIUM,
detection_prompts=[
"Look for unusually dark patches on walls or ceilings that should be lit.",
"Identify bright light leaks through solid geometry seams.",
"Check for mismatched lighting between adjacent surfaces.",
],
visual_indicators=[
"dark splotch on lit surface",
"bright line at geometry seam",
"lighting discontinuity between adjacent faces",
],
confidence_threshold=0.5,
),
GlitchPattern(
category=GlitchCategory.WATER_REFLECTION,
name="Water/Reflection Error",
description="Incorrect reflections, missing water surfaces, or broken "
"reflection probe assignments.",
severity=GlitchSeverity.MEDIUM,
detection_prompts=[
"Look for reflections that do not match the surrounding environment.",
"Identify water surfaces that appear solid or incorrectly rendered.",
"Check for mirror surfaces showing wrong scene geometry.",
],
visual_indicators=[
"reflection mismatch",
"solid water surface",
"incorrect environment map",
],
confidence_threshold=0.5,
),
GlitchPattern(
category=GlitchCategory.SKYBOX_SEAM,
name="Skybox Seam",
description="Visible seams or color mismatches at the edges of skybox cubemap faces.",
severity=GlitchSeverity.LOW,
detection_prompts=[
"Look at the edges of the sky for visible seams or color shifts.",
"Identify discontinuities where skybox faces meet.",
"Check for texture stretching at skybox corners.",
],
visual_indicators=[
"visible line in sky",
"color discontinuity at sky edge",
"sky texture seam",
],
confidence_threshold=0.45,
),
]
def get_patterns_by_severity(min_severity: GlitchSeverity) -> list[GlitchPattern]:
"""Return patterns at or above the given severity level."""
severity_order = [
GlitchSeverity.INFO,
GlitchSeverity.LOW,
GlitchSeverity.MEDIUM,
GlitchSeverity.HIGH,
GlitchSeverity.CRITICAL,
]
min_idx = severity_order.index(min_severity)
return [p for p in MATRIX_GLITCH_PATTERNS if severity_order.index(p.severity) >= min_idx]
def get_pattern_by_category(category: GlitchCategory) -> Optional[GlitchPattern]:
"""Return the pattern definition for a specific category."""
for p in MATRIX_GLITCH_PATTERNS:
if p.category == category:
return p
return None
def build_vision_prompt(patterns: list[GlitchPattern] | None = None) -> str:
"""Build a composite vision analysis prompt from pattern definitions."""
if patterns is None:
patterns = MATRIX_GLITCH_PATTERNS
sections = []
for p in patterns:
prompt_text = " ".join(p.detection_prompts)
indicators = ", ".join(p.visual_indicators)
sections.append(
f"[{p.category.value.upper()}] {p.name} (severity: {p.severity.value})\n"
f" {p.description}\n"
f" Look for: {prompt_text}\n"
f" Visual indicators: {indicators}"
)
return (
"Analyze this 3D world screenshot for visual glitches and artifacts. "
"For each detected issue, report the category, description of what you see, "
"approximate location in the image (x%, y%), and confidence (0.0-1.0).\n\n"
"Known glitch patterns to check:\n\n" + "\n\n".join(sections)
)
if __name__ == "__main__":
import json
print(f"Loaded {len(MATRIX_GLITCH_PATTERNS)} glitch patterns:\n")
for p in MATRIX_GLITCH_PATTERNS:
print(f" [{p.severity.value:8s}] {p.category.value}: {p.name}")
print(f"\nVision prompt preview:\n{build_vision_prompt()[:500]}...")

View File

@@ -1,45 +0,0 @@
#!/usr/bin/env bash
# kaizen-retro.sh — Automated retrospective after every burn cycle.
#
# Runs daily after the morning report.
# Analyzes success rates by agent, repo, and issue type.
# Identifies max-attempts issues, generates ONE concrete improvement,
# and posts the retro to Telegram + the master morning-report issue.
#
# Usage:
# ./bin/kaizen-retro.sh [--dry-run]
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="${SCRIPT_DIR%/bin}"
PYTHON="${PYTHON3:-python3}"
# Source local env if available so TELEGRAM_BOT_TOKEN is picked up
HOME_DIR="${HOME:-$(eval echo ~$(whoami))}"
for env_file in "$HOME_DIR/.hermes/.env" "$HOME_DIR/.timmy/.env" "$REPO_ROOT/.env"; do
if [ -f "$env_file" ]; then
# shellcheck source=/dev/null
set -a
# shellcheck source=/dev/null
source "$env_file"
set +a
fi
done
# If the configured Gitea URL is unreachable but localhost works, prefer localhost
if ! curl -sf "${GITEA_URL:-http://localhost:3000}/api/v1/version" >/dev/null 2>&1; then
if curl -sf http://localhost:3000/api/v1/version >/dev/null 2>&1; then
export GITEA_URL="http://localhost:3000"
fi
fi
# Ensure the Python script exists
RETRO_PY="$REPO_ROOT/scripts/kaizen_retro.py"
if [ ! -f "$RETRO_PY" ]; then
echo "ERROR: kaizen_retro.py not found at $RETRO_PY" >&2
exit 1
fi
# Run
exec "$PYTHON" "$RETRO_PY" "$@"

View File

@@ -1,549 +0,0 @@
#!/usr/bin/env python3
"""
Matrix 3D World Glitch Detector
Scans a 3D web world for visual artifacts using browser automation
and vision AI analysis. Produces structured glitch reports.
Usage:
python matrix_glitch_detector.py <url> [--angles 4] [--output report.json]
python matrix_glitch_detector.py --demo # Run with synthetic test data
Ref: timmy-config#491
"""
import argparse
import base64
import json
import os
import sys
import time
import uuid
from dataclasses import dataclass, field, asdict
from datetime import datetime, timezone
from pathlib import Path
from typing import Optional
# Add parent for glitch_patterns import
sys.path.insert(0, str(Path(__file__).resolve().parent))
from glitch_patterns import (
GlitchCategory,
GlitchPattern,
GlitchSeverity,
MATRIX_GLITCH_PATTERNS,
build_vision_prompt,
get_patterns_by_severity,
)
@dataclass
class DetectedGlitch:
"""A single detected glitch with metadata."""
id: str
category: str
name: str
description: str
severity: str
confidence: float
location_x: Optional[float] = None # percentage across image
location_y: Optional[float] = None # percentage down image
screenshot_index: int = 0
screenshot_angle: str = "front"
timestamp: str = ""
def __post_init__(self):
if not self.timestamp:
self.timestamp = datetime.now(timezone.utc).isoformat()
@dataclass
class ScanResult:
"""Complete scan result for a 3D world URL."""
scan_id: str
url: str
timestamp: str
total_screenshots: int
angles_captured: list[str]
glitches: list[dict] = field(default_factory=list)
summary: dict = field(default_factory=dict)
metadata: dict = field(default_factory=dict)
def to_json(self, indent: int = 2) -> str:
return json.dumps(asdict(self), indent=indent)
def generate_scan_angles(num_angles: int) -> list[dict]:
"""Generate camera angle configurations for multi-angle scanning.
Returns a list of dicts with yaw/pitch/label for browser camera control.
"""
base_angles = [
{"yaw": 0, "pitch": 0, "label": "front"},
{"yaw": 90, "pitch": 0, "label": "right"},
{"yaw": 180, "pitch": 0, "label": "back"},
{"yaw": 270, "pitch": 0, "label": "left"},
{"yaw": 0, "pitch": -30, "label": "front_low"},
{"yaw": 45, "pitch": -15, "label": "front_right_low"},
{"yaw": 0, "pitch": 30, "label": "front_high"},
{"yaw": 45, "pitch": 0, "label": "front_right"},
]
if num_angles <= len(base_angles):
return base_angles[:num_angles]
return base_angles + [
{"yaw": i * (360 // num_angles), "pitch": 0, "label": f"angle_{i}"}
for i in range(len(base_angles), num_angles)
]
def capture_screenshots(url: str, angles: list[dict], output_dir: Path) -> list[Path]:
"""Capture screenshots of a 3D web world from multiple angles.
Uses browser_vision tool when available; falls back to placeholder generation
for testing and environments without browser access.
"""
output_dir.mkdir(parents=True, exist_ok=True)
screenshots = []
for i, angle in enumerate(angles):
filename = output_dir / f"screenshot_{i:03d}_{angle['label']}.png"
# Attempt browser-based capture via browser_vision
try:
result = _browser_capture(url, angle, filename)
if result:
screenshots.append(filename)
continue
except Exception:
pass
# Generate placeholder screenshot for offline/test scenarios
_generate_placeholder_screenshot(filename, angle)
screenshots.append(filename)
return screenshots
def _browser_capture(url: str, angle: dict, output_path: Path) -> bool:
"""Capture a screenshot via browser automation.
This is a stub that delegates to the browser_vision tool when run
in an environment that provides it. In CI or offline mode, returns False.
"""
# Check if browser_vision is available via environment
bv_script = os.environ.get("BROWSER_VISION_SCRIPT")
if bv_script and Path(bv_script).exists():
import subprocess
cmd = [
sys.executable, bv_script,
"--url", url,
"--screenshot", str(output_path),
"--rotate-yaw", str(angle["yaw"]),
"--rotate-pitch", str(angle["pitch"]),
]
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
return proc.returncode == 0 and output_path.exists()
return False
def _generate_placeholder_screenshot(path: Path, angle: dict):
"""Generate a minimal 1x1 PNG as a placeholder for testing."""
# Minimal valid PNG (1x1 transparent pixel)
png_data = (
b"\x89PNG\r\n\x1a\n"
b"\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01"
b"\x08\x06\x00\x00\x00\x1f\x15\xc4\x89"
b"\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01"
b"\r\n\xb4\x00\x00\x00\x00IEND\xaeB`\x82"
)
path.write_bytes(png_data)
def analyze_with_vision(
screenshot_paths: list[Path],
angles: list[dict],
patterns: list[GlitchPattern] | None = None,
) -> list[DetectedGlitch]:
"""Send screenshots to vision AI for glitch analysis.
In environments with a vision model available, sends each screenshot
with the composite detection prompt. Otherwise returns simulated results.
"""
if patterns is None:
patterns = MATRIX_GLITCH_PATTERNS
prompt = build_vision_prompt(patterns)
glitches = []
for i, (path, angle) in enumerate(zip(screenshot_paths, angles)):
# Attempt vision analysis
detected = _vision_analyze_image(path, prompt, i, angle["label"])
glitches.extend(detected)
return glitches
def _vision_analyze_image(
image_path: Path,
prompt: str,
screenshot_index: int,
angle_label: str,
) -> list[DetectedGlitch]:
"""Analyze a single screenshot with vision AI.
Uses the vision_analyze tool when available; returns empty list otherwise.
"""
# Check for vision API configuration
api_key = os.environ.get("VISION_API_KEY") or os.environ.get("OPENAI_API_KEY")
api_base = os.environ.get("VISION_API_BASE", "https://api.openai.com/v1")
if api_key:
try:
return _call_vision_api(
image_path, prompt, screenshot_index, angle_label, api_key, api_base
)
except Exception as e:
print(f" [!] Vision API error for {image_path.name}: {e}", file=sys.stderr)
# No vision backend available
return []
def _call_vision_api(
image_path: Path,
prompt: str,
screenshot_index: int,
angle_label: str,
api_key: str,
api_base: str,
) -> list[DetectedGlitch]:
"""Call a vision API (OpenAI-compatible) for image analysis."""
import urllib.request
import urllib.error
image_data = base64.b64encode(image_path.read_bytes()).decode()
payload = json.dumps({
"model": os.environ.get("VISION_MODEL", "gpt-4o"),
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{image_data}",
"detail": "high",
},
},
],
}
],
"max_tokens": 4096,
}).encode()
req = urllib.request.Request(
f"{api_base}/chat/completions",
data=payload,
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}",
},
)
with urllib.request.urlopen(req, timeout=60) as resp:
result = json.loads(resp.read())
content = result["choices"][0]["message"]["content"]
return _parse_vision_response(content, screenshot_index, angle_label)
def _add_glitch_from_dict(
item: dict,
glitches: list[DetectedGlitch],
screenshot_index: int,
angle_label: str,
):
"""Convert a dict from vision API response into a DetectedGlitch."""
cat = item.get("category", item.get("type", "unknown"))
conf = float(item.get("confidence", item.get("score", 0.5)))
glitch = DetectedGlitch(
id=str(uuid.uuid4())[:8],
category=cat,
name=item.get("name", item.get("label", cat)),
description=item.get("description", item.get("detail", "")),
severity=item.get("severity", _infer_severity(cat, conf)),
confidence=conf,
location_x=item.get("location_x", item.get("x")),
location_y=item.get("location_y", item.get("y")),
screenshot_index=screenshot_index,
screenshot_angle=angle_label,
)
glitches.append(glitch)
def _parse_vision_response(
text: str, screenshot_index: int, angle_label: str
) -> list[DetectedGlitch]:
"""Parse vision AI response into structured glitch detections."""
glitches = []
# Try to extract JSON from the response
json_blocks = []
in_json = False
json_buf = []
for line in text.split("\n"):
stripped = line.strip()
if stripped.startswith("```"):
if in_json and json_buf:
try:
json_blocks.append(json.loads("\n".join(json_buf)))
except json.JSONDecodeError:
pass
json_buf = []
in_json = not in_json
continue
if in_json:
json_buf.append(line)
# Flush any remaining buffer
if in_json and json_buf:
try:
json_blocks.append(json.loads("\n".join(json_buf)))
except json.JSONDecodeError:
pass
# Also try parsing the entire response as JSON
try:
parsed = json.loads(text)
if isinstance(parsed, list):
json_blocks.extend(parsed)
elif isinstance(parsed, dict):
if "glitches" in parsed:
json_blocks.extend(parsed["glitches"])
elif "detections" in parsed:
json_blocks.extend(parsed["detections"])
else:
json_blocks.append(parsed)
except json.JSONDecodeError:
pass
for item in json_blocks:
# Flatten arrays of detections
if isinstance(item, list):
for sub in item:
if isinstance(sub, dict):
_add_glitch_from_dict(sub, glitches, screenshot_index, angle_label)
elif isinstance(item, dict):
_add_glitch_from_dict(item, glitches, screenshot_index, angle_label)
return glitches
def _infer_severity(category: str, confidence: float) -> str:
"""Infer severity from category and confidence when not provided."""
critical_cats = {"missing_textures", "clipping"}
high_cats = {"floating_assets", "broken_normals"}
cat_lower = category.lower()
if any(c in cat_lower for c in critical_cats):
return "critical" if confidence > 0.7 else "high"
if any(c in cat_lower for c in high_cats):
return "high" if confidence > 0.7 else "medium"
return "medium" if confidence > 0.6 else "low"
def build_report(
url: str,
angles: list[dict],
screenshots: list[Path],
glitches: list[DetectedGlitch],
) -> ScanResult:
"""Build the final structured scan report."""
severity_counts = {}
category_counts = {}
for g in glitches:
severity_counts[g.severity] = severity_counts.get(g.severity, 0) + 1
category_counts[g.category] = category_counts.get(g.category, 0) + 1
report = ScanResult(
scan_id=str(uuid.uuid4()),
url=url,
timestamp=datetime.now(timezone.utc).isoformat(),
total_screenshots=len(screenshots),
angles_captured=[a["label"] for a in angles],
glitches=[asdict(g) for g in glitches],
summary={
"total_glitches": len(glitches),
"by_severity": severity_counts,
"by_category": category_counts,
"highest_severity": max(severity_counts.keys(), default="none"),
"clean_screenshots": sum(
1
for i in range(len(screenshots))
if not any(g.screenshot_index == i for g in glitches)
),
},
metadata={
"detector_version": "0.1.0",
"pattern_count": len(MATRIX_GLITCH_PATTERNS),
"reference": "timmy-config#491",
},
)
return report
def run_demo(output_path: Optional[Path] = None) -> ScanResult:
"""Run a demonstration scan with simulated detections."""
print("[*] Running Matrix glitch detection demo...")
url = "https://matrix.example.com/world/alpha"
angles = generate_scan_angles(4)
screenshots_dir = Path("/tmp/matrix_glitch_screenshots")
print(f"[*] Capturing {len(angles)} screenshots from: {url}")
screenshots = capture_screenshots(url, angles, screenshots_dir)
print(f"[*] Captured {len(screenshots)} screenshots")
# Simulate detections for demo
demo_glitches = [
DetectedGlitch(
id=str(uuid.uuid4())[:8],
category="floating_assets",
name="Floating Chair",
description="Office chair floating 0.3m above floor in sector 7",
severity="high",
confidence=0.87,
location_x=35.2,
location_y=62.1,
screenshot_index=0,
screenshot_angle="front",
),
DetectedGlitch(
id=str(uuid.uuid4())[:8],
category="z_fighting",
name="Wall Texture Flicker",
description="Z-fighting between wall panel and decorative overlay",
severity="medium",
confidence=0.72,
location_x=58.0,
location_y=40.5,
screenshot_index=1,
screenshot_angle="right",
),
DetectedGlitch(
id=str(uuid.uuid4())[:8],
category="missing_textures",
name="Placeholder Texture",
description="Bright magenta surface on door frame — missing asset reference",
severity="critical",
confidence=0.95,
location_x=72.3,
location_y=28.8,
screenshot_index=2,
screenshot_angle="back",
),
DetectedGlitch(
id=str(uuid.uuid4())[:8],
category="clipping",
name="Desk Through Wall",
description="Desk corner clipping through adjacent wall geometry",
severity="high",
confidence=0.81,
location_x=15.0,
location_y=55.0,
screenshot_index=3,
screenshot_angle="left",
),
]
print(f"[*] Detected {len(demo_glitches)} glitches")
report = build_report(url, angles, screenshots, demo_glitches)
if output_path:
output_path.write_text(report.to_json())
print(f"[*] Report saved to: {output_path}")
return report
def main():
parser = argparse.ArgumentParser(
description="Matrix 3D World Glitch Detector — scan for visual artifacts",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s https://matrix.example.com/world/alpha
%(prog)s https://matrix.example.com/world/alpha --angles 8 --output report.json
%(prog)s --demo
""",
)
parser.add_argument("url", nargs="?", help="URL of the 3D world to scan")
parser.add_argument(
"--angles", type=int, default=4, help="Number of camera angles to capture (default: 4)"
)
parser.add_argument("--output", "-o", type=str, help="Output file path for JSON report")
parser.add_argument("--demo", action="store_true", help="Run demo with simulated data")
parser.add_argument(
"--min-severity",
choices=["info", "low", "medium", "high", "critical"],
default="info",
help="Minimum severity to include in report",
)
parser.add_argument("--verbose", "-v", action="store_true", help="Verbose output")
args = parser.parse_args()
if args.demo:
output = Path(args.output) if args.output else Path("glitch_report_demo.json")
report = run_demo(output)
print(f"\n=== Scan Summary ===")
print(f"URL: {report.url}")
print(f"Screenshots: {report.total_screenshots}")
print(f"Glitches found: {report.summary['total_glitches']}")
print(f"By severity: {report.summary['by_severity']}")
return
if not args.url:
parser.error("URL required (or use --demo)")
scan_id = str(uuid.uuid4())[:8]
print(f"[*] Matrix Glitch Detector — Scan {scan_id}")
print(f"[*] Target: {args.url}")
# Generate camera angles
angles = generate_scan_angles(args.angles)
print(f"[*] Capturing {len(angles)} screenshots...")
# Capture screenshots
screenshots_dir = Path(f"/tmp/matrix_glitch_{scan_id}")
screenshots = capture_screenshots(args.url, angles, screenshots_dir)
print(f"[*] Captured {len(screenshots)} screenshots")
# Filter patterns by severity
min_sev = GlitchSeverity(args.min_severity)
patterns = get_patterns_by_severity(min_sev)
# Analyze with vision AI
print(f"[*] Analyzing with vision AI ({len(patterns)} patterns)...")
glitches = analyze_with_vision(screenshots, angles, patterns)
# Build and save report
report = build_report(args.url, angles, screenshots, glitches)
if args.output:
Path(args.output).write_text(report.to_json())
print(f"[*] Report saved: {args.output}")
else:
print(report.to_json())
print(f"\n[*] Done — {len(glitches)} glitches detected")
if __name__ == "__main__":
main()

View File

@@ -19,25 +19,25 @@ PASS=0
FAIL=0
WARN=0
check_kimi_model() {
check_anthropic_model() {
local model="$1"
local label="$2"
local api_key="${KIMI_API_KEY:-}"
local api_key="${ANTHROPIC_API_KEY:-}"
if [ -z "$api_key" ]; then
# Try loading from .env
api_key=$(grep '^KIMI_API_KEY=' "${HERMES_HOME:-$HOME/.hermes}/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d "'\"" || echo "")
api_key=$(grep '^ANTHROPIC_API_KEY=' "${HERMES_HOME:-$HOME/.hermes}/.env" 2>/dev/null | head -1 | cut -d= -f2- | tr -d "'\"" || echo "")
fi
if [ -z "$api_key" ]; then
log "SKIP [$label] $model -- no KIMI_API_KEY"
log "SKIP [$label] $model -- no ANTHROPIC_API_KEY"
return 0
fi
response=$(curl -sf --max-time 10 -X POST \
"https://api.kimi.com/coding/v1/chat/completions" \
"https://api.anthropic.com/v1/messages" \
-H "x-api-key: ${api_key}" \
-H "x-api-provider: kimi-coding" \
-H "anthropic-version: 2023-06-01" \
-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

@@ -1,20 +0,0 @@
#!/usr/bin/env bash
# muda-audit.sh — Weekly waste audit wrapper
# Runs scripts/muda_audit.py from the repo root.
# Designed for cron or Gitea Actions.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "$REPO_ROOT"
# Ensure python3 is available
if ! command -v python3 >/dev/null 2>&1; then
echo "ERROR: python3 not found" >&2
exit 1
fi
# Run the audit
python3 "${REPO_ROOT}/scripts/muda_audit.py" "$@"

View File

@@ -1,4 +1,3 @@
#!/usr/bin/env python3
"""
Full Nostr agent-to-agent communication demo - FINAL WORKING
"""

View File

@@ -1,155 +1,70 @@
#!/usr/bin/env bash
# ── Gitea Workflow Feed ────────────────────────────────────────────────
# Shows open PRs, review pressure, and issue queues across core repos.
# ── Gitea Feed Panel ───────────────────────────────────────────────────
# Shows open PRs, recent merges, and issue queue. Called by watch.
# ───────────────────────────────────────────────────────────────────────
set -euo pipefail
B='\033[1m' ; D='\033[2m' ; R='\033[0m'
G='\033[32m' ; Y='\033[33m' ; RD='\033[31m' ; C='\033[36m' ; M='\033[35m'
B='\033[1m'
D='\033[2m'
R='\033[0m'
C='\033[36m'
G='\033[32m'
Y='\033[33m'
TOKEN=$(cat ~/.hermes/gitea_token_vps 2>/dev/null)
API="http://143.198.27.163:3000/api/v1/repos/rockachopa/Timmy-time-dashboard"
resolve_gitea_url() {
if [ -n "${GITEA_URL:-}" ]; then
printf '%s\n' "${GITEA_URL%/}"
return 0
fi
if [ -f "$HOME/.hermes/gitea_api" ]; then
python3 - "$HOME/.hermes/gitea_api" <<'PY'
from pathlib import Path
import sys
raw = Path(sys.argv[1]).read_text().strip().rstrip("/")
print(raw[:-7] if raw.endswith("/api/v1") else raw)
PY
return 0
fi
if [ -f "$HOME/.config/gitea/base-url" ]; then
tr -d '[:space:]' < "$HOME/.config/gitea/base-url"
return 0
fi
echo "ERROR: set GITEA_URL or create ~/.hermes/gitea_api" >&2
return 1
}
resolve_ops_token() {
local token_file
for token_file in \
"$HOME/.config/gitea/timmy-token" \
"$HOME/.hermes/gitea_token_vps" \
"$HOME/.hermes/gitea_token_timmy"; do
if [ -f "$token_file" ]; then
tr -d '[:space:]' < "$token_file"
return 0
fi
done
return 1
}
GITEA_URL="$(resolve_gitea_url)"
CORE_REPOS="${CORE_REPOS:-Timmy_Foundation/the-nexus Timmy_Foundation/timmy-home Timmy_Foundation/timmy-config Timmy_Foundation/hermes-agent}"
TOKEN="$(resolve_ops_token || true)"
[ -z "$TOKEN" ] && echo "WARN: no approved Timmy Gitea token found; feed will use unauthenticated API calls" >&2
echo -e "${B}${C} ◈ GITEA WORKFLOW${R} ${D}$(date '+%H:%M:%S')${R}"
echo -e "${B}${C} ◈ GITEA${R} ${D}$(date '+%H:%M:%S')${R}"
echo -e "${D}────────────────────────────────────────${R}"
python3 - "$GITEA_URL" "$TOKEN" "$CORE_REPOS" <<'PY'
import json
import sys
import urllib.error
import urllib.request
# Open PRs
echo -e " ${B}Open PRs${R}"
curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/pulls?state=open&limit=10" 2>/dev/null | python3 -c "
import json,sys
try:
prs = json.loads(sys.stdin.read())
if not prs: print(' (none)')
for p in prs:
age_h = ''
print(f' #{p[\"number\"]:3d} {p[\"user\"][\"login\"]:8s} {p[\"title\"][:45]}')
except: print(' (error)')
" 2>/dev/null
base = sys.argv[1].rstrip("/")
token = sys.argv[2]
repos = sys.argv[3].split()
headers = {"Authorization": f"token {token}"} if token else {}
echo -e "${D}────────────────────────────────────────${R}"
# Recent merged (last 5)
echo -e " ${B}Recently Merged${R}"
curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/pulls?state=closed&sort=updated&limit=5" 2>/dev/null | python3 -c "
import json,sys
try:
prs = json.loads(sys.stdin.read())
merged = [p for p in prs if p.get('merged')]
if not merged: print(' (none)')
for p in merged[:5]:
t = p['merged_at'][:16].replace('T',' ')
print(f' ${G}${R} #{p[\"number\"]:3d} {p[\"title\"][:35]} ${D}{t}${R}')
except: print(' (error)')
" 2>/dev/null
def fetch(path):
req = urllib.request.Request(f"{base}{path}", headers=headers)
with urllib.request.urlopen(req, timeout=5) as resp:
return json.loads(resp.read().decode())
echo -e "${D}────────────────────────────────────────${R}"
# Issue queue (assigned to kimi)
echo -e " ${B}Kimi Queue${R}"
curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/issues?state=open&limit=50&type=issues" 2>/dev/null | python3 -c "
import json,sys
try:
all_issues = json.loads(sys.stdin.read())
issues = [i for i in all_issues if 'kimi' in [a.get('login','') for a in (i.get('assignees') or [])]]
if not issues: print(' (empty — assign more!)')
for i in issues[:8]:
print(f' #{i[\"number\"]:3d} {i[\"title\"][:50]}')
if len(issues) > 8: print(f' ... +{len(issues)-8} more')
except: print(' (error)')
" 2>/dev/null
def short_repo(repo):
return repo.split("/", 1)[1]
echo -e "${D}────────────────────────────────────────${R}"
issues = []
pulls = []
errors = []
for repo in repos:
try:
repo_pulls = fetch(f"/api/v1/repos/{repo}/pulls?state=open&limit=20")
for pr in repo_pulls:
pr["_repo"] = repo
pulls.append(pr)
repo_issues = fetch(f"/api/v1/repos/{repo}/issues?state=open&limit=50&type=issues")
for issue in repo_issues:
issue["_repo"] = repo
issues.append(issue)
except urllib.error.URLError as exc:
errors.append(f"{repo}: {exc.reason}")
except Exception as exc: # pragma: no cover - defensive panel path
errors.append(f"{repo}: {exc}")
print(" \033[1mOpen PRs\033[0m")
if not pulls:
print(" (none)")
else:
for pr in pulls[:8]:
print(
f" #{pr['number']:3d} {short_repo(pr['_repo']):12s} "
f"{pr['user']['login'][:12]:12s} {pr['title'][:40]}"
)
print("\033[2m────────────────────────────────────────\033[0m")
print(" \033[1mNeeds Timmy / Allegro Review\033[0m")
reviewers = []
for repo in repos:
try:
repo_items = fetch(f"/api/v1/repos/{repo}/issues?state=open&limit=50&type=pulls")
for item in repo_items:
assignees = [a.get("login", "") for a in (item.get("assignees") or [])]
if any(name in assignees for name in ("Timmy", "allegro")):
item["_repo"] = repo
reviewers.append(item)
except Exception:
continue
if not reviewers:
print(" (clear)")
else:
for item in reviewers[:8]:
names = ",".join(a.get("login", "") for a in (item.get("assignees") or []))
print(
f" #{item['number']:3d} {short_repo(item['_repo']):12s} "
f"{names[:18]:18s} {item['title'][:34]}"
)
print("\033[2m────────────────────────────────────────\033[0m")
print(" \033[1mIssue Queues\033[0m")
queue_agents = ["allegro", "codex-agent", "groq", "claude", "ezra", "perplexity", "KimiClaw"]
for agent in queue_agents:
assigned = [
issue
for issue in issues
if agent in [a.get("login", "") for a in (issue.get("assignees") or [])]
]
print(f" {agent:12s} {len(assigned):2d}")
unassigned = [issue for issue in issues if not issue.get("assignees")]
print("\033[2m────────────────────────────────────────\033[0m")
print(f" Unassigned issues: \033[33m{len(unassigned)}\033[0m")
if errors:
print("\033[2m────────────────────────────────────────\033[0m")
print(" \033[1mErrors\033[0m")
for err in errors[:4]:
print(f" {err}")
PY
# Unassigned issues
UNASSIGNED=$(curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/issues?state=open&limit=50&type=issues" 2>/dev/null | python3 -c "
import json,sys
try:
issues = json.loads(sys.stdin.read())
print(len([i for i in issues if not i.get('assignees')]))
except: print('?')
" 2>/dev/null)
echo -e " Unassigned issues: ${Y}$UNASSIGNED${R}"

View File

@@ -1,294 +1,235 @@
#!/usr/bin/env bash
# ── Workflow Control Helpers ──────────────────────────────────────────
# ── Dashboard Control Helpers ──────────────────────────────────────────
# Source this in the controls pane: source ~/.hermes/bin/ops-helpers.sh
# These helpers intentionally target the current Hermes + Gitea workflow
# and do not revive deprecated bash worker loops.
# ───────────────────────────────────────────────────────────────────────
resolve_gitea_url() {
if [ -n "${GITEA:-}" ]; then
printf '%s\n' "${GITEA%/}"
return 0
fi
if [ -f "$HOME/.hermes/gitea_api" ]; then
python3 - "$HOME/.hermes/gitea_api" <<'PY'
from pathlib import Path
import sys
raw = Path(sys.argv[1]).read_text().strip().rstrip("/")
print(raw[:-7] if raw.endswith("/api/v1") else raw)
PY
return 0
fi
if [ -f "$HOME/.config/gitea/base-url" ]; then
tr -d '[:space:]' < "$HOME/.config/gitea/base-url"
return 0
fi
echo "ERROR: set GITEA or create ~/.hermes/gitea_api" >&2
return 1
}
export GITEA="$(resolve_gitea_url)"
export OPS_DEFAULT_REPO="${OPS_DEFAULT_REPO:-Timmy_Foundation/timmy-home}"
export OPS_CORE_REPOS="${OPS_CORE_REPOS:-Timmy_Foundation/the-nexus Timmy_Foundation/timmy-home Timmy_Foundation/timmy-config Timmy_Foundation/hermes-agent}"
ops-token() {
local token_file
for token_file in \
"$HOME/.config/gitea/timmy-token" \
"$HOME/.hermes/gitea_token_vps" \
"$HOME/.hermes/gitea_token_timmy"; do
if [ -f "$token_file" ]; then
tr -d '[:space:]' < "$token_file"
return 0
fi
done
return 1
}
export TOKEN=*** ~/.hermes/gitea_token_vps 2>/dev/null)
export GITEA="http://143.198.27.163:3000"
export REPO_API="$GITEA/api/v1/repos/rockachopa/Timmy-time-dashboard"
ops-help() {
echo ""
echo -e "\033[1m\033[35m ◈ WORKFLOW CONTROLS\033[0m"
echo -e "\033[1m\033[35m ◈ CONTROLS\033[0m"
echo -e "\033[2m ──────────────────────────────────────\033[0m"
echo ""
echo -e " \033[1mReview\033[0m"
echo " ops-prs [repo] List open PRs across the core repos or one repo"
echo " ops-review-queue Show PRs waiting on Timmy or Allegro"
echo " ops-merge PR REPO Squash-merge a reviewed PR"
echo -e " \033[1mWake Up\033[0m"
echo " ops-wake-kimi Restart Kimi loop"
echo " ops-wake-claude Restart Claude loop"
echo " ops-wake-gemini Restart Gemini loop"
echo " ops-wake-gateway Restart gateway"
echo " ops-wake-all Restart everything"
echo ""
echo -e " \033[1mDispatch\033[0m"
echo " ops-assign ISSUE AGENT [repo] Assign an issue to an agent"
echo " ops-unassign ISSUE [repo] Remove all assignees from an issue"
echo " ops-queue AGENT [repo|all] Show an agent's queue"
echo " ops-unassigned [repo|all] Show unassigned issues"
echo -e " \033[1mManage\033[0m"
echo " ops-merge PR_NUM Squash-merge a PR"
echo " ops-assign ISSUE Assign issue to Kimi"
echo " ops-assign-claude ISSUE [REPO] Assign to Claude"
echo " ops-audit Run efficiency audit now"
echo " ops-prs List open PRs"
echo " ops-queue Show Kimi's queue"
echo " ops-claude-queue Show Claude's queue"
echo " ops-gemini-queue Show Gemini's queue"
echo ""
echo -e " \033[1mWorkflow Health\033[0m"
echo " ops-gitea-feed Render the Gitea workflow feed"
echo " ops-freshness Check Hermes session/export freshness"
echo -e " \033[1mEmergency\033[0m"
echo " ops-kill-kimi Stop Kimi loop"
echo " ops-kill-claude Stop Claude loop"
echo " ops-kill-gemini Stop Gemini loop"
echo " ops-kill-zombies Kill stuck git/pytest"
echo ""
echo -e " \033[1mShortcuts\033[0m"
echo " ops-assign-allegro ISSUE [repo]"
echo " ops-assign-codex ISSUE [repo]"
echo " ops-assign-groq ISSUE [repo]"
echo " ops-assign-claude ISSUE [repo]"
echo " ops-assign-ezra ISSUE [repo]"
echo -e " \033[1mOrchestrator\033[0m"
echo " ops-wake-timmy Start Timmy (Ollama)"
echo " ops-kill-timmy Stop Timmy"
echo ""
echo -e " \033[1mWatchdog\033[0m"
echo " ops-wake-watchdog Start loop watchdog"
echo " ops-kill-watchdog Stop loop watchdog"
echo ""
echo -e " \033[2m Type ops-help to see this again\033[0m"
echo ""
}
ops-python() {
local token
token=$(ops-token) || { echo "No Gitea token found"; return 1; }
OPS_TOKEN="$token" python3 - "$@"
ops-wake-kimi() {
pkill -f "kimi-loop.sh" 2>/dev/null
sleep 1
nohup bash ~/.hermes/bin/kimi-loop.sh >> ~/.hermes/logs/kimi-loop.log 2>&1 &
echo " Kimi loop started (PID $!)"
}
ops-prs() {
local target="${1:-all}"
ops-python "$GITEA" "$OPS_CORE_REPOS" "$target" <<'PY'
import json
import os
import sys
import urllib.request
base = sys.argv[1].rstrip("/")
repos = sys.argv[2].split()
target = sys.argv[3]
token = os.environ["OPS_TOKEN"]
headers = {"Authorization": f"token {token}"}
if target != "all":
repos = [target]
pulls = []
for repo in repos:
req = urllib.request.Request(
f"{base}/api/v1/repos/{repo}/pulls?state=open&limit=20",
headers=headers,
)
with urllib.request.urlopen(req, timeout=5) as resp:
for pr in json.loads(resp.read().decode()):
pr["_repo"] = repo
pulls.append(pr)
if not pulls:
print(" (none)")
else:
for pr in pulls:
print(f" #{pr['number']:4d} {pr['_repo'].split('/', 1)[1]:12s} {pr['user']['login'][:12]:12s} {pr['title'][:60]}")
PY
ops-wake-gateway() {
hermes gateway start 2>&1
}
ops-review-queue() {
ops-python "$GITEA" "$OPS_CORE_REPOS" <<'PY'
import json
import os
import sys
import urllib.request
base = sys.argv[1].rstrip("/")
repos = sys.argv[2].split()
token = os.environ["OPS_TOKEN"]
headers = {"Authorization": f"token {token}"}
items = []
for repo in repos:
req = urllib.request.Request(
f"{base}/api/v1/repos/{repo}/issues?state=open&limit=50&type=pulls",
headers=headers,
)
with urllib.request.urlopen(req, timeout=5) as resp:
for item in json.loads(resp.read().decode()):
assignees = [a.get("login", "") for a in (item.get("assignees") or [])]
if any(name in assignees for name in ("Timmy", "allegro")):
item["_repo"] = repo
items.append(item)
if not items:
print(" (clear)")
else:
for item in items:
names = ",".join(a.get("login", "") for a in (item.get("assignees") or []))
print(f" #{item['number']:4d} {item['_repo'].split('/', 1)[1]:12s} {names[:20]:20s} {item['title'][:56]}")
PY
ops-wake-claude() {
local workers="${1:-3}"
pkill -f "claude-loop.sh" 2>/dev/null
sleep 1
nohup bash ~/.hermes/bin/claude-loop.sh "$workers" >> ~/.hermes/logs/claude-loop.log 2>&1 &
echo " Claude loop started — $workers workers (PID $!)"
}
ops-assign() {
local issue="$1"
local agent="$2"
local repo="${3:-$OPS_DEFAULT_REPO}"
local token
[ -z "$issue" ] && { echo "Usage: ops-assign ISSUE_NUMBER AGENT [owner/repo]"; return 1; }
[ -z "$agent" ] && { echo "Usage: ops-assign ISSUE_NUMBER AGENT [owner/repo]"; return 1; }
token=$(ops-token) || { echo "No Gitea token found"; return 1; }
curl -s -X PATCH -H "Authorization: token $token" -H "Content-Type: application/json" \
"$GITEA/api/v1/repos/$repo/issues/$issue" -d "{\"assignees\":[\"$agent\"]}" | python3 -c "
ops-wake-gemini() {
pkill -f "gemini-loop.sh" 2>/dev/null
sleep 1
nohup bash ~/.hermes/bin/gemini-loop.sh >> ~/.hermes/logs/gemini-loop.log 2>&1 &
echo " Gemini loop started (PID $!)"
}
ops-wake-all() {
ops-wake-gateway
sleep 1
ops-wake-kimi
sleep 1
ops-wake-claude
sleep 1
ops-wake-gemini
echo " All services started"
}
ops-merge() {
local pr=$1
[ -z "$pr" ] && { echo "Usage: ops-merge PR_NUMBER"; return 1; }
curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
"$REPO_API/pulls/$pr/merge" -d '{"Do":"squash"}' | python3 -c "
import json,sys
d=json.loads(sys.stdin.read())
names=','.join(a.get('login','') for a in (d.get('assignees') or []))
print(f' ✓ #{d.get(\"number\", \"?\")} assigned to {names or \"(none)\"}')
if 'sha' in d: print(f' ✓ PR #{$pr} merged ({d[\"sha\"][:8]})')
else: print(f' {d.get(\"message\",\"unknown error\")}')
" 2>/dev/null
}
ops-unassign() {
local issue="$1"
local repo="${2:-$OPS_DEFAULT_REPO}"
local token
[ -z "$issue" ] && { echo "Usage: ops-unassign ISSUE_NUMBER [owner/repo]"; return 1; }
token=$(ops-token) || { echo "No Gitea token found"; return 1; }
curl -s -X PATCH -H "Authorization: token $token" -H "Content-Type: application/json" \
"$GITEA/api/v1/repos/$repo/issues/$issue" -d '{"assignees":[]}' | python3 -c "
ops-assign() {
local issue=$1
[ -z "$issue" ] && { echo "Usage: ops-assign ISSUE_NUMBER"; return 1; }
curl -s -X PATCH -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
"$REPO_API/issues/$issue" -d '{"assignees":["kimi"]}' | python3 -c "
import json,sys; d=json.loads(sys.stdin.read()); print(f' ✓ #{$issue} assigned to kimi')
" 2>/dev/null
}
ops-audit() {
bash ~/.hermes/bin/efficiency-audit.sh
}
ops-prs() {
curl -s -H "Authorization: token $TOKEN" "$REPO_API/pulls?state=open&limit=20" | python3 -c "
import json,sys
d=json.loads(sys.stdin.read())
print(f' ✓ #{d.get(\"number\", \"?\")} unassigned')
prs=json.loads(sys.stdin.read())
for p in prs: print(f' #{p[\"number\"]:4d} {p[\"user\"][\"login\"]:8s} {p[\"title\"][:60]}')
if not prs: print(' (none)')
" 2>/dev/null
}
ops-queue() {
local agent="$1"
local target="${2:-all}"
[ -z "$agent" ] && { echo "Usage: ops-queue AGENT [repo|all]"; return 1; }
ops-python "$GITEA" "$OPS_CORE_REPOS" "$agent" "$target" <<'PY'
import json
import os
import sys
import urllib.request
base = sys.argv[1].rstrip("/")
repos = sys.argv[2].split()
agent = sys.argv[3]
target = sys.argv[4]
token = os.environ["OPS_TOKEN"]
headers = {"Authorization": f"token {token}"}
if target != "all":
repos = [target]
rows = []
for repo in repos:
req = urllib.request.Request(
f"{base}/api/v1/repos/{repo}/issues?state=open&limit=50&type=issues",
headers=headers,
)
with urllib.request.urlopen(req, timeout=5) as resp:
for issue in json.loads(resp.read().decode()):
assignees = [a.get("login", "") for a in (issue.get("assignees") or [])]
if agent in assignees:
rows.append((repo, issue["number"], issue["title"]))
if not rows:
print(" (empty)")
else:
for repo, number, title in rows:
print(f" #{number:4d} {repo.split('/', 1)[1]:12s} {title[:60]}")
PY
}
ops-unassigned() {
local target="${1:-all}"
ops-python "$GITEA" "$OPS_CORE_REPOS" "$target" <<'PY'
import json
import os
import sys
import urllib.request
base = sys.argv[1].rstrip("/")
repos = sys.argv[2].split()
target = sys.argv[3]
token = os.environ["OPS_TOKEN"]
headers = {"Authorization": f"token {token}"}
if target != "all":
repos = [target]
rows = []
for repo in repos:
req = urllib.request.Request(
f"{base}/api/v1/repos/{repo}/issues?state=open&limit=50&type=issues",
headers=headers,
)
with urllib.request.urlopen(req, timeout=5) as resp:
for issue in json.loads(resp.read().decode()):
if not issue.get("assignees"):
rows.append((repo, issue["number"], issue["title"]))
if not rows:
print(" (none)")
else:
for repo, number, title in rows[:20]:
print(f" #{number:4d} {repo.split('/', 1)[1]:12s} {title[:60]}")
if len(rows) > 20:
print(f" ... +{len(rows) - 20} more")
PY
}
ops-merge() {
local pr="$1"
local repo="${2:-$OPS_DEFAULT_REPO}"
local token
[ -z "$pr" ] && { echo "Usage: ops-merge PR_NUMBER [owner/repo]"; return 1; }
token=$(ops-token) || { echo "No Gitea token found"; return 1; }
curl -s -X POST -H "Authorization: token $token" -H "Content-Type: application/json" \
"$GITEA/api/v1/repos/$repo/pulls/$pr/merge" -d '{"Do":"squash"}' | python3 -c "
curl -s -H "Authorization: token $TOKEN" "$REPO_API/issues?state=open&limit=50&type=issues" | python3 -c "
import json,sys
d=json.loads(sys.stdin.read())
if 'sha' in d:
print(f' ✓ PR merged ({d[\"sha\"][:8]})')
else:
print(f' ✗ {d.get(\"message\", \"unknown error\")}')
all_issues=json.loads(sys.stdin.read())
issues=[i for i in all_issues if 'kimi' in [a.get('login','') for a in (i.get('assignees') or [])]]
for i in issues: print(f' #{i[\"number\"]:4d} {i[\"title\"][:60]}')
if not issues: print(' (empty)')
" 2>/dev/null
}
ops-gitea-feed() {
bash "$HOME/.hermes/bin/ops-gitea.sh"
ops-kill-kimi() {
pkill -f "kimi-loop.sh" 2>/dev/null
pkill -f "kimi.*--print" 2>/dev/null
echo " Kimi stopped"
}
ops-freshness() {
bash "$HOME/.hermes/bin/pipeline-freshness.sh"
ops-kill-claude() {
pkill -f "claude-loop.sh" 2>/dev/null
pkill -f "claude.*--print.*--dangerously" 2>/dev/null
rm -rf ~/.hermes/logs/claude-locks/*.lock 2>/dev/null
echo '{}' > ~/.hermes/logs/claude-active.json 2>/dev/null
echo " Claude stopped (all workers)"
}
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-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}"; }
ops-kill-gemini() {
pkill -f "gemini-loop.sh" 2>/dev/null
pkill -f "gemini.*--print" 2>/dev/null
echo " Gemini stopped"
}
ops-assign-claude() {
local issue=$1
local repo="${2:-rockachopa/Timmy-time-dashboard}"
[ -z "$issue" ] && { echo "Usage: ops-assign-claude ISSUE_NUMBER [owner/repo]"; return 1; }
curl -s -X PATCH -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
"$GITEA/api/v1/repos/$repo/issues/$issue" -d '{"assignees":["claude"]}' | python3 -c "
import json,sys; d=json.loads(sys.stdin.read()); print(f' ✓ #{$issue} assigned to claude')
" 2>/dev/null
}
ops-claude-queue() {
python3 -c "
import json, urllib.request
token=*** ~/.hermes/claude_token 2>/dev/null)'
base = 'http://143.198.27.163:3000'
repos = ['rockachopa/Timmy-time-dashboard','rockachopa/alexanderwhitestone.com','replit/timmy-tower','replit/token-gated-economy','rockachopa/hermes-agent']
for repo in repos:
url = f'{base}/api/v1/repos/{repo}/issues?state=open&limit=50&type=issues'
try:
req = urllib.request.Request(url, headers={'Authorization': f'token {token}'})
resp = urllib.request.urlopen(req, timeout=5)
raw = json.loads(resp.read())
issues = [i for i in raw if 'claude' in [a.get('login','') for a in (i.get('assignees') or [])]]
for i in issues:
print(f' #{i[\"number\"]:4d} {repo.split(\"/\")[1]:20s} {i[\"title\"][:50]}')
except: continue
" 2>/dev/null || echo " (error)"
}
ops-assign-gemini() {
local issue=$1
local repo="${2:-rockachopa/Timmy-time-dashboard}"
[ -z "$issue" ] && { echo "Usage: ops-assign-gemini ISSUE_NUMBER [owner/repo]"; return 1; }
curl -s -X PATCH -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \
"$GITEA/api/v1/repos/$repo/issues/$issue" -d '{"assignees":["gemini"]}' | python3 -c "
import json,sys; d=json.loads(sys.stdin.read()); print(f' ✓ #{$issue} assigned to gemini')
" 2>/dev/null
}
ops-gemini-queue() {
curl -s -H "Authorization: token $TOKEN" "$REPO_API/issues?state=open&limit=50&type=issues" | python3 -c "
import json,sys
all_issues=json.loads(sys.stdin.read())
issues=[i for i in all_issues if 'gemini' in [a.get('login','') for a in (i.get('assignees') or [])]]
for i in issues: print(f' #{i[\"number\"]:4d} {i[\"title\"][:60]}')
if not issues: print(' (empty)')
" 2>/dev/null
}
ops-kill-zombies() {
local killed=0
for pid in $(ps aux | grep "pytest tests/" | grep -v grep | awk '{print $2}'); do
kill "$pid" 2>/dev/null && killed=$((killed+1))
done
for pid in $(ps aux | grep "git.*push\|git-remote-http" | grep -v grep | awk '{print $2}'); do
kill "$pid" 2>/dev/null && killed=$((killed+1))
done
echo " Killed $killed zombie processes"
}
ops-wake-timmy() {
pkill -f "timmy-orchestrator.sh" 2>/dev/null
rm -f ~/.hermes/logs/timmy-orchestrator.pid
sleep 1
nohup bash ~/.hermes/bin/timmy-orchestrator.sh >> ~/.hermes/logs/timmy-orchestrator.log 2>&1 &
echo " Timmy orchestrator started (PID $!)"
}
ops-kill-timmy() {
pkill -f "timmy-orchestrator.sh" 2>/dev/null
rm -f ~/.hermes/logs/timmy-orchestrator.pid
echo " Timmy stopped"
}
ops-wake-watchdog() {
pkill -f "loop-watchdog.sh" 2>/dev/null
sleep 1
nohup bash ~/.hermes/bin/loop-watchdog.sh >> ~/.hermes/logs/watchdog.log 2>&1 &
echo " Watchdog started (PID $!)"
}
ops-kill-watchdog() {
pkill -f "loop-watchdog.sh" 2>/dev/null
echo " Watchdog stopped"
}

View File

@@ -1,224 +1,300 @@
#!/usr/bin/env bash
# ── Workflow Ops Panel ─────────────────────────────────────────────────
# Current-state dashboard for review, dispatch, and freshness.
# This intentionally reflects the post-loop, Hermes-sidecar workflow.
# ── Consolidated Ops Panel ─────────────────────────────────────────────
# Everything in one view. Designed for a half-screen pane (~100x45).
# ───────────────────────────────────────────────────────────────────────
set -euo pipefail
B='\033[1m' ; D='\033[2m' ; R='\033[0m' ; U='\033[4m'
G='\033[32m' ; Y='\033[33m' ; RD='\033[31m' ; C='\033[36m' ; M='\033[35m' ; W='\033[37m'
OK="${G}${R}" ; WARN="${Y}${R}" ; FAIL="${RD}${R}" ; OFF="${D}${R}"
B='\033[1m'
D='\033[2m'
R='\033[0m'
U='\033[4m'
G='\033[32m'
Y='\033[33m'
RD='\033[31m'
M='\033[35m'
OK="${G}${R}"
WARN="${Y}${R}"
FAIL="${RD}${R}"
resolve_gitea_url() {
if [ -n "${GITEA_URL:-}" ]; then
printf '%s\n' "${GITEA_URL%/}"
return 0
fi
if [ -f "$HOME/.hermes/gitea_api" ]; then
python3 - "$HOME/.hermes/gitea_api" <<'PY'
from pathlib import Path
import sys
raw = Path(sys.argv[1]).read_text().strip().rstrip("/")
print(raw[:-7] if raw.endswith("/api/v1") else raw)
PY
return 0
fi
if [ -f "$HOME/.config/gitea/base-url" ]; then
tr -d '[:space:]' < "$HOME/.config/gitea/base-url"
return 0
fi
echo "ERROR: set GITEA_URL or create ~/.hermes/gitea_api" >&2
return 1
}
resolve_ops_token() {
local token_file
for token_file in \
"$HOME/.config/gitea/timmy-token" \
"$HOME/.hermes/gitea_token_vps" \
"$HOME/.hermes/gitea_token_timmy"; do
if [ -f "$token_file" ]; then
tr -d '[:space:]' < "$token_file"
return 0
fi
done
return 1
}
GITEA_URL="$(resolve_gitea_url)"
CORE_REPOS="${CORE_REPOS:-Timmy_Foundation/the-nexus Timmy_Foundation/timmy-home Timmy_Foundation/timmy-config Timmy_Foundation/hermes-agent}"
TOKEN="$(resolve_ops_token || true)"
[ -z "$TOKEN" ] && echo "WARN: no approved Timmy Gitea token found; panel will use unauthenticated API calls" >&2
TOKEN=$(cat ~/.hermes/gitea_token_vps 2>/dev/null)
API="http://143.198.27.163:3000/api/v1/repos/rockachopa/Timmy-time-dashboard"
# ── HEADER ─────────────────────────────────────────────────────────────
echo ""
echo -e " ${B}${M}WORKFLOW OPERATIONS${R} ${D}$(date '+%a %b %d %H:%M:%S')${R}"
echo -e " ${B}${M}HERMES OPERATIONS${R} ${D}$(date '+%a %b %d %H:%M:%S')${R}"
echo -e " ${D}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${R}"
echo ""
# ── SERVICES ───────────────────────────────────────────────────────────
echo -e " ${B}${U}SERVICES${R}"
echo ""
GW_PID=$(pgrep -f "hermes.*gateway.*run" 2>/dev/null | head -1 || true)
if [ -n "${GW_PID:-}" ]; then
echo -e " ${OK} Hermes Gateway ${D}pid $GW_PID${R}"
# Gateway
GW_PID=$(pgrep -f "hermes.*gateway.*run" 2>/dev/null | head -1)
[ -n "$GW_PID" ] && echo -e " ${OK} Gateway ${D}pid $GW_PID${R}" \
|| echo -e " ${FAIL} Gateway ${RD}DOWN — run: hermes gateway start${R}"
# Kimi Code loop
KIMI_PID=$(pgrep -f "kimi-loop.sh" 2>/dev/null | head -1)
[ -n "$KIMI_PID" ] && echo -e " ${OK} Kimi Loop ${D}pid $KIMI_PID${R}" \
|| echo -e " ${FAIL} Kimi Loop ${RD}DOWN — run: ops-wake-kimi${R}"
# Active Kimi Code worker
KIMI_WORK=$(pgrep -f "kimi.*--print" 2>/dev/null | head -1)
if [ -n "$KIMI_WORK" ]; then
echo -e " ${OK} Kimi Code ${D}pid $KIMI_WORK ${G}working${R}"
elif [ -n "$KIMI_PID" ]; then
echo -e " ${WARN} Kimi Code ${Y}between issues${R}"
else
echo -e " ${FAIL} Hermes Gateway ${RD}down${R}"
echo -e " ${OFF} Kimi Code ${D}not running${R}"
fi
if curl -s --max-time 3 "$GITEA_URL/api/v1/version" >/dev/null 2>&1; then
echo -e " ${OK} Gitea ${D}${GITEA_URL}${R}"
# Claude Code loop (parallel workers)
CLAUDE_PID=$(pgrep -f "claude-loop.sh" 2>/dev/null | head -1)
CLAUDE_WORKERS=$(pgrep -f "claude.*--print.*--dangerously" 2>/dev/null | wc -l | tr -d ' ')
if [ -n "$CLAUDE_PID" ]; then
echo -e " ${OK} Claude Loop ${D}pid $CLAUDE_PID ${G}${CLAUDE_WORKERS} workers active${R}"
else
echo -e " ${FAIL} Gitea ${RD}unreachable${R}"
echo -e " ${FAIL} Claude Loop ${RD}DOWN — run: ops-wake-claude${R}"
fi
if hermes cron list >/dev/null 2>&1; then
echo -e " ${OK} Hermes Cron ${D}reachable${R}"
# Gemini Code loop
GEMINI_PID=$(pgrep -f "gemini-loop.sh" 2>/dev/null | head -1)
GEMINI_WORK=$(pgrep -f "gemini.*--print" 2>/dev/null | head -1)
if [ -n "$GEMINI_PID" ]; then
if [ -n "$GEMINI_WORK" ]; then
echo -e " ${OK} Gemini Loop ${D}pid $GEMINI_PID ${G}working${R}"
else
echo -e " ${WARN} Gemini Loop ${D}pid $GEMINI_PID ${Y}between issues${R}"
fi
else
echo -e " ${WARN} Hermes Cron ${Y}not responding${R}"
echo -e " ${FAIL} Gemini Loop ${RD}DOWN — run: ops-wake-gemini${R}"
fi
FRESHNESS_OUTPUT=$("$HOME/.hermes/bin/pipeline-freshness.sh" 2>/dev/null || true)
FRESHNESS_STATUS=$(printf '%s\n' "$FRESHNESS_OUTPUT" | awk -F= '/^status=/{print $2}')
FRESHNESS_REASON=$(printf '%s\n' "$FRESHNESS_OUTPUT" | awk -F= '/^reason=/{print $2}')
if [ "$FRESHNESS_STATUS" = "ok" ]; then
echo -e " ${OK} Export Freshness ${D}${FRESHNESS_REASON:-within freshness window}${R}"
elif [ -n "$FRESHNESS_STATUS" ]; then
echo -e " ${WARN} Export Freshness ${Y}${FRESHNESS_REASON:-lagging}${R}"
# Timmy Orchestrator
TIMMY_PID=$(pgrep -f "timmy-orchestrator.sh" 2>/dev/null | head -1)
if [ -n "$TIMMY_PID" ]; then
TIMMY_LAST=$(tail -1 "$HOME/.hermes/logs/timmy-orchestrator.log" 2>/dev/null | sed 's/.*TIMMY: //')
echo -e " ${OK} Timmy (Ollama) ${D}pid $TIMMY_PID ${G}${TIMMY_LAST:0:30}${R}"
else
echo -e " ${WARN} Export Freshness ${Y}unknown${R}"
echo -e " ${FAIL} Timmy ${RD}DOWN — run: ops-wake-timmy${R}"
fi
# Gitea VPS
if curl -s --max-time 3 "http://143.198.27.163:3000/api/v1/version" >/dev/null 2>&1; then
echo -e " ${OK} Gitea VPS ${D}143.198.27.163:3000${R}"
else
echo -e " ${FAIL} Gitea VPS ${RD}unreachable${R}"
fi
# Matrix staging
HTTP=$(curl -s --max-time 3 -o /dev/null -w "%{http_code}" "http://143.198.27.163/")
[ "$HTTP" = "200" ] && echo -e " ${OK} Matrix Staging ${D}143.198.27.163${R}" \
|| echo -e " ${FAIL} Matrix Staging ${RD}HTTP $HTTP${R}"
# Dev cycle cron
CRON_LINE=$(hermes cron list 2>&1 | grep -B1 "consolidated-dev-cycle" | head -1 2>/dev/null)
if echo "$CRON_LINE" | grep -q "active"; then
NEXT=$(hermes cron list 2>&1 | grep -A4 "consolidated-dev-cycle" | grep "Next" | awk '{print $NF}' | cut -dT -f2 | cut -d. -f1)
echo -e " ${OK} Dev Cycle ${D}every 30m, next ${NEXT:-?}${R}"
else
echo -e " ${FAIL} Dev Cycle Cron ${RD}MISSING${R}"
fi
echo ""
python3 - "$GITEA_URL" "$TOKEN" "$CORE_REPOS" <<'PY'
# ── KIMI STATS ─────────────────────────────────────────────────────────
echo -e " ${B}${U}KIMI${R}"
echo ""
KIMI_LOG="$HOME/.hermes/logs/kimi-loop.log"
if [ -f "$KIMI_LOG" ]; then
COMPLETED=$(grep -c "SUCCESS:" "$KIMI_LOG" 2>/dev/null | tail -1 || echo 0)
FAILED=$(grep -c "FAILED:" "$KIMI_LOG" 2>/dev/null | tail -1 || echo 0)
LAST_ISSUE=$(grep "=== ISSUE" "$KIMI_LOG" | tail -1 | sed 's/.*=== //' | sed 's/ ===//')
LAST_TIME=$(grep "=== ISSUE\|SUCCESS\|FAILED" "$KIMI_LOG" | tail -1 | cut -d']' -f1 | tr -d '[')
RATE=""
if [ "$COMPLETED" -gt 0 ] && [ "$FAILED" -gt 0 ]; then
TOTAL=$((COMPLETED + FAILED))
PCT=$((COMPLETED * 100 / TOTAL))
RATE=" (${PCT}% success)"
fi
echo -e " Completed ${G}${B}$COMPLETED${R} Failed ${RD}$FAILED${R}${D}$RATE${R}"
echo -e " Current ${C}$LAST_ISSUE${R}"
echo -e " Last seen ${D}$LAST_TIME${R}"
fi
echo ""
# ── CLAUDE STATS ──────────────────────────────────────────────────
echo -e " ${B}${U}CLAUDE${R}"
echo ""
CLAUDE_LOG="$HOME/.hermes/logs/claude-loop.log"
if [ -f "$CLAUDE_LOG" ]; then
CL_COMPLETED=$(grep -c "SUCCESS" "$CLAUDE_LOG" 2>/dev/null | tail -1 || echo 0)
CL_FAILED=$(grep -c "FAILED" "$CLAUDE_LOG" 2>/dev/null | tail -1 || echo 0)
CL_RATE_LIM=$(grep -c "RATE LIMITED" "$CLAUDE_LOG" 2>/dev/null | tail -1 || echo 0)
CL_RATE=""
if [ "$CL_COMPLETED" -gt 0 ] || [ "$CL_FAILED" -gt 0 ]; then
CL_TOTAL=$((CL_COMPLETED + CL_FAILED))
[ "$CL_TOTAL" -gt 0 ] && CL_PCT=$((CL_COMPLETED * 100 / CL_TOTAL)) && CL_RATE=" (${CL_PCT}%)"
fi
echo -e " ${G}${B}$CL_COMPLETED${R} done ${RD}$CL_FAILED${R} fail ${Y}$CL_RATE_LIM${R} rate-limited${D}$CL_RATE${R}"
# Show active workers
ACTIVE="$HOME/.hermes/logs/claude-active.json"
if [ -f "$ACTIVE" ]; then
python3 -c "
import json
import sys
import urllib.error
import urllib.request
from datetime import datetime, timedelta, timezone
base = sys.argv[1].rstrip("/")
token = sys.argv[2]
repos = sys.argv[3].split()
headers = {"Authorization": f"token {token}"} if token else {}
def fetch(path):
req = urllib.request.Request(f"{base}{path}", headers=headers)
with urllib.request.urlopen(req, timeout=5) as resp:
return json.loads(resp.read().decode())
def short(repo):
return repo.split("/", 1)[1]
issues = []
pulls = []
review_queue = []
errors = []
for repo in repos:
try:
repo_pulls = fetch(f"/api/v1/repos/{repo}/pulls?state=open&limit=20")
for pr in repo_pulls:
pr["_repo"] = repo
pulls.append(pr)
repo_issues = fetch(f"/api/v1/repos/{repo}/issues?state=open&limit=50&type=issues")
for issue in repo_issues:
issue["_repo"] = repo
issues.append(issue)
repo_pull_issues = fetch(f"/api/v1/repos/{repo}/issues?state=open&limit=50&type=pulls")
for item in repo_pull_issues:
assignees = [a.get("login", "") for a in (item.get("assignees") or [])]
if any(name in assignees for name in ("Timmy", "allegro")):
item["_repo"] = repo
review_queue.append(item)
except urllib.error.URLError as exc:
errors.append(f"{repo}: {exc.reason}")
except Exception as exc: # pragma: no cover - defensive panel path
errors.append(f"{repo}: {exc}")
print(" \033[1m\033[4mREVIEW QUEUE\033[0m\n")
if not review_queue:
print(" \033[2m(clear)\033[0m\n")
else:
for item in review_queue[:8]:
names = ",".join(a.get("login", "") for a in (item.get("assignees") or []))
print(f" #{item['number']:<4d} {short(item['_repo']):12s} {names[:20]:20s} {item['title'][:44]}")
print()
print(" \033[1m\033[4mOPEN PRS\033[0m\n")
if not pulls:
print(" \033[2m(none open)\033[0m\n")
else:
for pr in pulls[:8]:
print(f" #{pr['number']:<4d} {short(pr['_repo']):12s} {pr['user']['login'][:12]:12s} {pr['title'][:48]}")
print()
print(" \033[1m\033[4mDISPATCH QUEUES\033[0m\n")
queue_agents = [
("allegro", "dispatch"),
("codex-agent", "cleanup"),
("groq", "fast ship"),
("claude", "refactor"),
("ezra", "archive"),
("perplexity", "research"),
("KimiClaw", "digest"),
]
for agent, label in queue_agents:
assigned = [
issue
for issue in issues
if agent in [a.get("login", "") for a in (issue.get("assignees") or [])]
]
print(f" {agent:12s} {len(assigned):2d} \033[2m{label}\033[0m")
print()
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"):
count = sum(
1
for issue in issues
if agent in [a.get("login", "") for a in (issue.get("assignees") or [])]
)
if count > 3:
overloaded.append((agent, count))
print(" \033[1m\033[4mWARNINGS\033[0m\n")
warns = []
if len(unassigned) > 10:
warns.append(f"{len(unassigned)} unassigned issues across core repos")
if stale_prs:
warns.append(f"{len(stale_prs)} open PRs look stale and may need a review nudge")
for agent, count in overloaded:
warns.append(f"{agent} has {count} assigned issues; rebalance dispatch")
if warns:
for warn in warns:
print(f" \033[33m⚠ {warn}\033[0m")
else:
print(" \033[2m(no major workflow warnings)\033[0m")
if errors:
print("\n \033[1m\033[4mFETCH ERRORS\033[0m\n")
for err in errors[:4]:
print(f" \033[31m{err}\033[0m")
PY
try:
with open('$ACTIVE') as f: active = json.load(f)
for wid, info in sorted(active.items()):
iss = info.get('issue','')
repo = info.get('repo','').split('/')[-1] if info.get('repo') else ''
st = info.get('status','')
if st == 'working':
print(f' \033[36mW{wid}\033[0m \033[33m#{iss}\033[0m \033[2m{repo}\033[0m')
elif st == 'idle':
print(f' \033[2mW{wid} idle\033[0m')
except: pass
" 2>/dev/null
fi
else
echo -e " ${D}(no log yet — start with ops-wake-claude)${R}"
fi
echo ""
# ── GEMINI STATS ─────────────────────────────────────────────────────
echo -e " ${B}${U}GEMINI${R}"
echo ""
GEMINI_LOG="$HOME/.hermes/logs/gemini-loop.log"
if [ -f "$GEMINI_LOG" ]; then
GM_COMPLETED=$(grep -c "SUCCESS:" "$GEMINI_LOG" 2>/dev/null | tail -1 || echo 0)
GM_FAILED=$(grep -c "FAILED:" "$GEMINI_LOG" 2>/dev/null | tail -1 || echo 0)
GM_RATE=""
if [ "$GM_COMPLETED" -gt 0 ] || [ "$GM_FAILED" -gt 0 ]; then
GM_TOTAL=$((GM_COMPLETED + GM_FAILED))
[ "$GM_TOTAL" -gt 0 ] && GM_PCT=$((GM_COMPLETED * 100 / GM_TOTAL)) && GM_RATE=" (${GM_PCT}%)"
fi
GM_LAST=$(grep "=== ISSUE" "$GEMINI_LOG" | tail -1 | sed 's/.*=== //' | sed 's/ ===//')
echo -e " ${G}${B}$GM_COMPLETED${R} done ${RD}$GM_FAILED${R} fail${D}$GM_RATE${R}"
[ -n "$GM_LAST" ] && echo -e " Current ${C}$GM_LAST${R}"
else
echo -e " ${D}(no log yet — start with ops-wake-gemini)${R}"
fi
echo ""
# ── OPEN PRS ───────────────────────────────────────────────────────────
echo -e " ${B}${U}PULL REQUESTS${R}"
echo ""
curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/pulls?state=open&limit=8" 2>/dev/null | python3 -c "
import json,sys
try:
prs = json.loads(sys.stdin.read())
if not prs: print(' \033[2m(none open)\033[0m')
for p in prs[:6]:
n = p['number']
t = p['title'][:55]
u = p['user']['login']
print(f' \033[33m#{n:<4d}\033[0m \033[2m{u:8s}\033[0m {t}')
if len(prs) > 6: print(f' \033[2m... +{len(prs)-6} more\033[0m')
except: print(' \033[31m(error fetching)\033[0m')
" 2>/dev/null
echo ""
# ── RECENTLY MERGED ────────────────────────────────────────────────────
echo -e " ${B}${U}RECENTLY MERGED${R}"
echo ""
curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/pulls?state=closed&sort=updated&limit=5" 2>/dev/null | python3 -c "
import json,sys
try:
prs = json.loads(sys.stdin.read())
merged = [p for p in prs if p.get('merged')][:5]
if not merged: print(' \033[2m(none recent)\033[0m')
for p in merged:
n = p['number']
t = p['title'][:50]
when = p['merged_at'][11:16]
print(f' \033[32m✓ #{n:<4d}\033[0m {t} \033[2m{when}\033[0m')
except: print(' \033[31m(error)\033[0m')
" 2>/dev/null
echo ""
# ── KIMI QUEUE ─────────────────────────────────────────────────────────
echo -e " ${B}${U}KIMI QUEUE${R}"
echo ""
curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/issues?state=open&limit=50&type=issues" 2>/dev/null | python3 -c "
import json,sys
try:
all_issues = json.loads(sys.stdin.read())
issues = [i for i in all_issues if 'kimi' in [a.get('login','') for a in (i.get('assignees') or [])]]
if not issues: print(' \033[33m⚠ Queue empty — assign more issues to kimi\033[0m')
for i in issues[:6]:
n = i['number']
t = i['title'][:55]
print(f' #{n:<4d} {t}')
if len(issues) > 6: print(f' \033[2m... +{len(issues)-6} more\033[0m')
except: print(' \033[31m(error)\033[0m')
" 2>/dev/null
echo ""
# ── CLAUDE QUEUE ──────────────────────────────────────────────────
echo -e " ${B}${U}CLAUDE QUEUE${R}"
echo ""
# Claude works across multiple repos
python3 -c "
import json, sys, urllib.request
token = '$(cat ~/.hermes/claude_token 2>/dev/null)'
base = 'http://143.198.27.163:3000'
repos = ['rockachopa/Timmy-time-dashboard','rockachopa/alexanderwhitestone.com','replit/timmy-tower','replit/token-gated-economy','rockachopa/hermes-agent']
all_issues = []
for repo in repos:
url = f'{base}/api/v1/repos/{repo}/issues?state=open&limit=50&type=issues'
try:
req = urllib.request.Request(url, headers={'Authorization': f'token {token}'})
resp = urllib.request.urlopen(req, timeout=5)
raw = json.loads(resp.read())
issues = [i for i in raw if 'claude' in [a.get('login','') for a in (i.get('assignees') or [])]]
for i in issues:
i['_repo'] = repo.split('/')[1]
all_issues.extend(issues)
except: continue
if not all_issues:
print(' \033[33m\u26a0 Queue empty \u2014 assign issues to claude\033[0m')
else:
for i in all_issues[:6]:
n = i['number']
t = i['title'][:45]
r = i['_repo'][:12]
print(f' #{n:<4d} \033[2m{r:12s}\033[0m {t}')
if len(all_issues) > 6:
print(f' \033[2m... +{len(all_issues)-6} more\033[0m')
" 2>/dev/null
echo ""
# ── GEMINI QUEUE ─────────────────────────────────────────────────────
echo -e " ${B}${U}GEMINI QUEUE${R}"
echo ""
curl -s --max-time 5 -H "Authorization: token $TOKEN" "$API/issues?state=open&limit=50&type=issues" 2>/dev/null | python3 -c "
import json,sys
try:
all_issues = json.loads(sys.stdin.read())
issues = [i for i in all_issues if 'gemini' in [a.get('login','') for a in (i.get('assignees') or [])]]
if not issues: print(' \033[33m⚠ Queue empty — assign issues to gemini\033[0m')
for i in issues[:6]:
n = i['number']
t = i['title'][:55]
print(f' #{n:<4d} {t}')
if len(issues) > 6: print(f' \033[2m... +{len(issues)-6} more\033[0m')
except: print(' \033[31m(error)\033[0m')
" 2>/dev/null
echo ""
# ── WARNINGS ───────────────────────────────────────────────────────────
HERMES_PROCS=$(ps aux | grep -E "hermes.*python" | grep -v grep | wc -l | tr -d ' ')
STUCK_GIT=$(ps aux | grep "git.*push\|git-remote-http" | grep -v grep | wc -l | tr -d ' ')
ORPHAN_PY=$(ps aux | grep "pytest tests/" | grep -v grep | wc -l | tr -d ' ')
UNASSIGNED=$(curl -s --max-time 3 -H "Authorization: token $TOKEN" "$API/issues?state=open&limit=50&type=issues" 2>/dev/null | python3 -c "import json,sys; issues=json.loads(sys.stdin.read()); print(len([i for i in issues if not i.get('assignees')]))" 2>/dev/null)
WARNS=""
[ "$STUCK_GIT" -gt 0 ] && WARNS+=" ${RD}$STUCK_GIT stuck git processes${R}\n"
[ "$ORPHAN_PY" -gt 0 ] && WARNS+=" ${Y}$ORPHAN_PY orphaned pytest runs${R}\n"
[ "${UNASSIGNED:-0}" -gt 10 ] && WARNS+=" ${Y}$UNASSIGNED unassigned issues — feed the queue${R}\n"
if [ -n "$WARNS" ]; then
echo -e " ${B}${U}WARNINGS${R}"
echo ""
echo -e "$WARNS"
fi
echo -e " ${D}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${R}"
echo -e " ${D}repos: $(printf '%s' "$CORE_REPOS" | wc -w | tr -d ' ') refresh via watch or rerun script${R}"
echo -e " ${D}hermes sessions: $HERMES_PROCS unassigned: ${UNASSIGNED:-?} ↻ 20s${R}"

View File

@@ -1,514 +0,0 @@
#!/usr/bin/env bash
# pane-watchdog.sh — Detect stuck/dead tmux panes and auto-restart them
#
# Tracks output hash per pane across cycles. If a pane's captured output
# hasn't changed for STUCK_CYCLES consecutive checks, the pane is STUCK.
# Dead panes (PID gone) are also detected.
#
# On STUCK/DEAD:
# 1. Kill the pane
# 2. Attempt restart with --resume (session ID from manifest)
# 3. Fallback: fresh prompt with last known task from logs
#
# State file: ~/.hermes/pane-state.json
# Log: ~/.hermes/logs/pane-watchdog.log
#
# Usage:
# pane-watchdog.sh # One-shot check all sessions
# pane-watchdog.sh --daemon # Run every CHECK_INTERVAL seconds
# pane-watchdog.sh --status # Print current pane state
# pane-watchdog.sh --session NAME # Check only one session
#
# Issue: timmy-config #515
set -uo pipefail
export PATH="/opt/homebrew/bin:$HOME/.local/bin:$HOME/.hermes/bin:/usr/local/bin:$PATH"
# === CONFIG ===
STATE_FILE="${PANE_STATE_FILE:-$HOME/.hermes/pane-state.json}"
LOG_FILE="${PANE_WATCHDOG_LOG:-$HOME/.hermes/logs/pane-watchdog.log}"
CHECK_INTERVAL="${PANE_CHECK_INTERVAL:-120}" # seconds between cycles
STUCK_CYCLES=2 # unchanged cycles before STUCK
MAX_RESTART_ATTEMPTS=3 # per pane per hour
RESTART_COOLDOWN=3600 # seconds between escalation alerts
CAPTURE_LINES=40 # lines of output to hash
# Sessions to monitor (all if empty)
MONITOR_SESSIONS="${PANE_WATCHDOG_SESSIONS:-}"
mkdir -p "$(dirname "$STATE_FILE")" "$(dirname "$LOG_FILE")"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}
# === HELPERS ===
# Capture last N lines of pane output and hash them
capture_pane_hash() {
local target="$1"
local output
output=$(tmux capture-pane -t "$target" -p -S "-${CAPTURE_LINES}" 2>/dev/null || echo "DEAD")
echo -n "$output" | shasum -a 256 | cut -d' ' -f1
}
# Check if pane PID is alive
pane_pid_alive() {
local target="$1"
local pid
pid=$(tmux list-panes -t "$target" -F '#{pane_pid}' 2>/dev/null | head -1 || echo "")
if [ -z "$pid" ]; then
return 1 # pane doesn't exist
fi
kill -0 "$pid" 2>/dev/null
}
# Get pane start command
pane_start_command() {
local target="$1"
tmux list-panes -t "$target" -F '#{pane_start_command}' 2>/dev/null | head -1 || echo "unknown"
}
# Get the pane's current running command (child process)
pane_current_command() {
local target="$1"
tmux list-panes -t "$target" -F '#{pane_current_command}' 2>/dev/null || echo "unknown"
}
# Only restart panes running hermes/agent commands (not zsh, python3 repls, etc.)
is_restartable() {
local cmd="$1"
case "$cmd" in
hermes|*hermes*|*agent*|*timmy*|*kimi*|*claude-loop*|*gemini-loop*)
return 0
;;
*)
return 1
;;
esac
}
# Get session ID from hermes manifest if available
get_hermes_session_id() {
local session_name="$1"
local manifest="$HOME/.hermes/sessions/${session_name}/manifest.json"
if [ -f "$manifest" ]; then
python3 -c "
import json, sys
try:
m = json.load(open('$manifest'))
print(m.get('session_id', m.get('id', '')))
except: pass
" 2>/dev/null || echo ""
else
echo ""
fi
}
# Get last task from pane logs
get_last_task() {
local session_name="$1"
local log_dir="$HOME/.hermes/logs"
# Find the most recent log for this session
local log_file
log_file=$(find "$log_dir" -name "*${session_name}*" -type f -mtime -1 2>/dev/null | sort -r | head -1)
if [ -n "$log_file" ] && [ -f "$log_file" ]; then
# Extract last user prompt or task description
grep -i "task:\|prompt:\|issue\|working on" "$log_file" 2>/dev/null | tail -1 | sed 's/.*[:>] *//' | head -c 200
fi
}
# Restart a pane with a fresh shell/command
restart_pane() {
local target="$1"
local session_name="${target%%:*}"
local session_id last_task cmd
log "RESTART: Attempting to restart $target"
# Kill existing pane
tmux kill-pane -t "$target" 2>/dev/null || true
sleep 1
# Try --resume with session ID
session_id=$(get_hermes_session_id "$session_name")
if [ -n "$session_id" ]; then
log "RESTART: Trying --resume with session $session_id"
tmux split-window -t "$session_name" -d \
"hermes chat --resume '$session_id' 2>&1 | tee -a '$HOME/.hermes/logs/${session_name}-restart.log'"
sleep 2
if pane_pid_alive "${session_name}:1" 2>/dev/null; then
log "RESTART: Success with --resume"
return 0
fi
fi
# Fallback: fresh prompt
last_task=$(get_last_task "$session_name")
if [ -n "$last_task" ]; then
log "RESTART: Fallback — fresh prompt with task: $last_task"
tmux split-window -t "$session_name" -d \
"echo 'Watchdog restart — last task: $last_task' && hermes chat 2>&1 | tee -a '$HOME/.hermes/logs/${session_name}-restart.log'"
else
log "RESTART: Fallback — fresh hermes chat"
tmux split-window -t "$session_name" -d \
"hermes chat 2>&1 | tee -a '$HOME/.hermes/logs/${session_name}-restart.log'"
fi
sleep 2
if pane_pid_alive "${session_name}:1" 2>/dev/null; then
log "RESTART: Fallback restart succeeded"
return 0
else
log "RESTART: FAILED to restart $target"
return 1
fi
}
# === STATE MANAGEMENT ===
read_state() {
if [ -f "$STATE_FILE" ]; then
cat "$STATE_FILE"
else
echo "{}"
fi
}
write_state() {
echo "$1" > "$STATE_FILE"
}
# Update state for a single pane and return JSON status
update_pane_state() {
local target="$1"
local hash="$2"
local is_alive="$3"
local now
now=$(date +%s)
python3 - "$STATE_FILE" "$target" "$hash" "$is_alive" "$now" "$STUCK_CYCLES" <<'PYEOF'
import json, sys, time
state_file = sys.argv[1]
target = sys.argv[2]
new_hash = sys.argv[3]
is_alive = sys.argv[4] == "true"
now = int(sys.argv[5])
stuck_cycles = int(sys.argv[6])
try:
with open(state_file) as f:
state = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
state = {}
pane = state.get(target, {
"hash": "",
"same_count": 0,
"status": "UNKNOWN",
"last_change": 0,
"last_check": 0,
"restart_attempts": 0,
"last_restart": 0,
"current_command": "",
})
if not is_alive:
pane["status"] = "DEAD"
pane["same_count"] = 0
elif new_hash == pane.get("hash", ""):
pane["same_count"] = pane.get("same_count", 0) + 1
if pane["same_count"] >= stuck_cycles:
pane["status"] = "STUCK"
else:
pane["status"] = "STALE" if pane["same_count"] > 0 else "OK"
else:
pane["hash"] = new_hash
pane["same_count"] = 0
pane["status"] = "OK"
pane["last_change"] = now
pane["last_check"] = now
state[target] = pane
with open(state_file, "w") as f:
json.dump(state, f, indent=2)
print(json.dumps(pane))
PYEOF
}
# Reset restart attempt counter if cooldown expired
maybe_reset_restarts() {
local target="$1"
local now
now=$(date +%s)
python3 - "$STATE_FILE" "$target" "$now" "$RESTART_COOLDOWN" <<'PYEOF'
import json, sys
state_file = sys.argv[1]
target = sys.argv[2]
now = int(sys.argv[3])
cooldown = int(sys.argv[4])
with open(state_file) as f:
state = json.load(f)
pane = state.get(target, {})
last_restart = pane.get("last_restart", 0)
if now - last_restart > cooldown:
pane["restart_attempts"] = 0
state[target] = pane
with open(state_file, "w") as f:
json.dump(state, f, indent=2)
print(pane.get("restart_attempts", 0))
PYEOF
}
increment_restart_attempt() {
local target="$1"
local now
now=$(date +%s)
python3 - "$STATE_FILE" "$target" "$now" <<'PYEOF'
import json, sys
state_file = sys.argv[1]
target = sys.argv[2]
now = int(sys.argv[3])
with open(state_file) as f:
state = json.load(f)
pane = state.get(target, {})
pane["restart_attempts"] = pane.get("restart_attempts", 0) + 1
pane["last_restart"] = now
pane["status"] = "RESTARTING"
state[target] = pane
with open(state_file, "w") as f:
json.dump(state, f, indent=2)
print(pane["restart_attempts"])
PYEOF
}
# === CORE CHECK ===
check_pane() {
local target="$1"
local hash is_alive status current_cmd
# Capture state
hash=$(capture_pane_hash "$target")
if pane_pid_alive "$target"; then
is_alive="true"
else
is_alive="false"
fi
# Get current command for the pane
current_cmd=$(pane_current_command "$target")
# Update state and get result
local result
result=$(update_pane_state "$target" "$hash" "$is_alive")
status=$(echo "$result" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('status','UNKNOWN'))" 2>/dev/null || echo "UNKNOWN")
case "$status" in
OK)
# Healthy, do nothing
;;
DEAD)
log "DETECTED: $target is DEAD (PID gone) cmd=$current_cmd"
if is_restartable "$current_cmd"; then
handle_stuck "$target"
else
log "SKIP: $target not a hermes pane (cmd=$current_cmd), not restarting"
fi
;;
STUCK)
log "DETECTED: $target is STUCK (output unchanged for ${STUCK_CYCLES} cycles) cmd=$current_cmd"
if is_restartable "$current_cmd"; then
handle_stuck "$target"
else
log "SKIP: $target not a hermes pane (cmd=$current_cmd), not restarting"
fi
;;
STALE)
# Output unchanged but within threshold — just log
local count
count=$(echo "$result" | python3 -c "import json,sys; print(json.loads(sys.stdin.read()).get('same_count',0))" 2>/dev/null || echo "?")
log "STALE: $target unchanged for $count cycle(s)"
;;
esac
}
handle_stuck() {
local target="$1"
local session_name="${target%%:*}"
local attempts
# Check restart budget
attempts=$(maybe_reset_restarts "$target")
if [ "$attempts" -ge "$MAX_RESTART_ATTEMPTS" ]; then
log "ESCALATION: $target stuck ${attempts}x — manual intervention needed"
echo "ALERT: $target stuck after $attempts restart attempts" >&2
return 1
fi
attempts=$(increment_restart_attempt "$target")
log "ACTION: Restarting $target (attempt $attempts/$MAX_RESTART_ATTEMPTS)"
if restart_pane "$target"; then
log "OK: $target restarted successfully"
else
log "FAIL: $target restart failed (attempt $attempts)"
fi
}
check_all_sessions() {
local sessions
if [ -n "$MONITOR_SESSIONS" ]; then
IFS=',' read -ra sessions <<< "$MONITOR_SESSIONS"
else
sessions=()
while IFS= read -r line; do
[ -n "$line" ] && sessions+=("$line")
done < <(tmux list-sessions -F '#{session_name}' 2>/dev/null || true)
fi
local total=0 stuck=0 dead=0 ok=0
for session in "${sessions[@]}"; do
[ -z "$session" ] && continue
# Get pane targets
local panes
panes=$(tmux list-panes -t "$session" -F "${session}:#{window_index}.#{pane_index}" 2>/dev/null || true)
for target in $panes; do
check_pane "$target"
total=$((total + 1))
done
done
log "CHECK: Processed $total panes"
}
# === STATUS DISPLAY ===
show_status() {
if [ ! -f "$STATE_FILE" ]; then
echo "No pane state file found at $STATE_FILE"
echo "Run pane-watchdog.sh once to initialize."
exit 0
fi
python3 - "$STATE_FILE" <<'PYEOF'
import json, sys, time
state_file = sys.argv[1]
try:
with open(state_file) as f:
state = json.load(f)
except (FileNotFoundError, json.JSONDecodeError):
print("No state data yet.")
sys.exit(0)
if not state:
print("No panes tracked.")
sys.exit(0)
now = int(time.time())
print(f"{'PANE':<35} {'STATUS':<12} {'STALE':<6} {'LAST CHANGE':<15} {'RESTARTS'}")
print("-" * 90)
for target in sorted(state.keys()):
p = state[target]
status = p.get("status", "?")
same = p.get("same_count", 0)
last_change = p.get("last_change", 0)
restarts = p.get("restart_attempts", 0)
if last_change:
ago = now - last_change
if ago < 60:
change_str = f"{ago}s ago"
elif ago < 3600:
change_str = f"{ago//60}m ago"
else:
change_str = f"{ago//3600}h ago"
else:
change_str = "never"
# Color code
if status == "OK":
icon = "✓"
elif status == "STUCK":
icon = "✖"
elif status == "DEAD":
icon = "☠"
elif status == "STALE":
icon = "⏳"
else:
icon = "?"
print(f" {icon} {target:<32} {status:<12} {same:<6} {change_str:<15} {restarts}")
PYEOF
}
# === DAEMON MODE ===
run_daemon() {
log "DAEMON: Starting (interval=${CHECK_INTERVAL}s, stuck_threshold=${STUCK_CYCLES})"
echo "Pane watchdog started. Checking every ${CHECK_INTERVAL}s. Ctrl+C to stop."
echo "Log: $LOG_FILE"
echo "State: $STATE_FILE"
echo ""
while true; do
check_all_sessions
sleep "$CHECK_INTERVAL"
done
}
# === MAIN ===
case "${1:-}" in
--daemon)
run_daemon
;;
--status)
show_status
;;
--session)
if [ -z "${2:-}" ]; then
echo "Usage: pane-watchdog.sh --session SESSION_NAME"
exit 1
fi
MONITOR_SESSIONS="$2"
check_all_sessions
;;
--help|-h)
echo "pane-watchdog.sh — Detect stuck/dead tmux panes and auto-restart"
echo ""
echo "Usage:"
echo " pane-watchdog.sh # One-shot check"
echo " pane-watchdog.sh --daemon # Continuous monitoring"
echo " pane-watchdog.sh --status # Show pane state"
echo " pane-watchdog.sh --session S # Check one session"
echo ""
echo "Config (env vars):"
echo " PANE_CHECK_INTERVAL Seconds between checks (default: 120)"
echo " PANE_WATCHDOG_SESSIONS Comma-separated session names"
echo " PANE_STATE_FILE State file path"
echo " STUCK_CYCLES Unchanged cycles before STUCK (default: 2)"
;;
*)
check_all_sessions
;;
esac

View File

@@ -1,191 +0,0 @@
#!/usr/bin/env python3
"""pr-checklist.py -- Automated PR quality gate for Gitea CI.
Enforces the review standards that agents skip when left to self-approve.
Runs in CI on every pull_request event. Exits non-zero on any failure.
Checks:
1. PR has >0 file changes (no empty PRs)
2. PR branch is not behind base branch
3. PR does not bundle >3 unrelated issues
4. Changed .py files pass syntax check (python -c import)
5. Changed .sh files are executable
6. PR body references an issue number
7. At least 1 non-author review exists (warning only)
Refs: #393 (PERPLEXITY-08), Epic #385
"""
from __future__ import annotations
import json
import os
import re
import subprocess
import sys
from pathlib import Path
def fail(msg: str) -> None:
print(f"FAIL: {msg}", file=sys.stderr)
def warn(msg: str) -> None:
print(f"WARN: {msg}", file=sys.stderr)
def ok(msg: str) -> None:
print(f" OK: {msg}")
def get_changed_files() -> list[str]:
"""Return list of files changed in this PR vs base branch."""
base = os.environ.get("GITHUB_BASE_REF", "main")
try:
result = subprocess.run(
["git", "diff", "--name-only", f"origin/{base}...HEAD"],
capture_output=True, text=True, check=True,
)
return [f for f in result.stdout.strip().splitlines() if f]
except subprocess.CalledProcessError:
# Fallback: diff against HEAD~1
result = subprocess.run(
["git", "diff", "--name-only", "HEAD~1"],
capture_output=True, text=True, check=True,
)
return [f for f in result.stdout.strip().splitlines() if f]
def check_has_changes(files: list[str]) -> bool:
"""Check 1: PR has >0 file changes."""
if not files:
fail("PR has 0 file changes. Empty PRs are not allowed.")
return False
ok(f"PR changes {len(files)} file(s)")
return True
def check_not_behind_base() -> bool:
"""Check 2: PR branch is not behind base."""
base = os.environ.get("GITHUB_BASE_REF", "main")
try:
result = subprocess.run(
["git", "rev-list", "--count", f"HEAD..origin/{base}"],
capture_output=True, text=True, check=True,
)
behind = int(result.stdout.strip())
if behind > 0:
fail(f"Branch is {behind} commit(s) behind {base}. Rebase or merge.")
return False
ok(f"Branch is up-to-date with {base}")
return True
except (subprocess.CalledProcessError, ValueError):
warn("Could not determine if branch is behind base (git fetch may be needed)")
return True # Don't block on CI fetch issues
def check_issue_bundling(pr_body: str) -> bool:
"""Check 3: PR does not bundle >3 unrelated issues."""
issue_refs = set(re.findall(r"#(\d+)", pr_body))
if len(issue_refs) > 3:
fail(f"PR references {len(issue_refs)} issues ({', '.join(sorted(issue_refs))}). "
"Max 3 per PR to prevent bundling. Split into separate PRs.")
return False
ok(f"PR references {len(issue_refs)} issue(s) (max 3)")
return True
def check_python_syntax(files: list[str]) -> bool:
"""Check 4: Changed .py files have valid syntax."""
py_files = [f for f in files if f.endswith(".py") and Path(f).exists()]
if not py_files:
ok("No Python files changed")
return True
all_ok = True
for f in py_files:
result = subprocess.run(
[sys.executable, "-c", f"import ast; ast.parse(open('{f}').read())"],
capture_output=True, text=True,
)
if result.returncode != 0:
fail(f"Syntax error in {f}: {result.stderr.strip()[:200]}")
all_ok = False
if all_ok:
ok(f"All {len(py_files)} Python file(s) pass syntax check")
return all_ok
def check_shell_executable(files: list[str]) -> bool:
"""Check 5: Changed .sh files are executable."""
sh_files = [f for f in files if f.endswith(".sh") and Path(f).exists()]
if not sh_files:
ok("No shell scripts changed")
return True
all_ok = True
for f in sh_files:
if not os.access(f, os.X_OK):
fail(f"{f} is not executable. Run: chmod +x {f}")
all_ok = False
if all_ok:
ok(f"All {len(sh_files)} shell script(s) are executable")
return all_ok
def check_issue_reference(pr_body: str) -> bool:
"""Check 6: PR body references an issue number."""
if re.search(r"#\d+", pr_body):
ok("PR body references at least one issue")
return True
fail("PR body does not reference any issue (e.g. #123). "
"Every PR must trace to an issue.")
return False
def main() -> int:
print("=" * 60)
print("PR Checklist — Automated Quality Gate")
print("=" * 60)
print()
# Get PR body from env or git log
pr_body = os.environ.get("PR_BODY", "")
if not pr_body:
try:
result = subprocess.run(
["git", "log", "--format=%B", "-1"],
capture_output=True, text=True, check=True,
)
pr_body = result.stdout
except subprocess.CalledProcessError:
pr_body = ""
files = get_changed_files()
failures = 0
checks = [
check_has_changes(files),
check_not_behind_base(),
check_issue_bundling(pr_body),
check_python_syntax(files),
check_shell_executable(files),
check_issue_reference(pr_body),
]
failures = sum(1 for c in checks if not c)
print()
print("=" * 60)
if failures:
print(f"RESULT: {failures} check(s) FAILED")
print("Fix the issues above and push again.")
return 1
else:
print("RESULT: All checks passed")
return 0
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,4 +1,3 @@
#!/usr/bin/env python3
"""
Soul Eval Gate — The Conscience of the Training Pipeline

362
bin/timmy-dashboard Normal file → Executable file
View File

@@ -1,19 +1,20 @@
#!/usr/bin/env python3
"""Timmy workflow dashboard.
"""Timmy Model Dashboard — where are my models, what are they doing.
Shows current workflow state from the active local surfaces instead of the
archived dashboard/loop era, while preserving useful local/session metrics.
Usage:
timmy-dashboard # one-shot
timmy-dashboard --watch # live refresh every 30s
timmy-dashboard --hours=48 # look back 48h
"""
from __future__ import annotations
import json
import os
import sqlite3
import subprocess
import sys
import time
import urllib.request
from datetime import datetime, timedelta, timezone
from datetime import datetime, timezone, timedelta
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
@@ -25,97 +26,37 @@ from metrics_helpers import summarize_local_metrics, summarize_session_rows
HERMES_HOME = Path.home() / ".hermes"
TIMMY_HOME = Path.home() / ".timmy"
METRICS_DIR = TIMMY_HOME / "metrics"
CORE_REPOS = [
"Timmy_Foundation/the-nexus",
"Timmy_Foundation/timmy-home",
"Timmy_Foundation/timmy-config",
"Timmy_Foundation/hermes-agent",
]
def resolve_gitea_url() -> str:
env = os.environ.get("GITEA_URL")
if env:
return env.rstrip("/")
api_hint = HERMES_HOME / "gitea_api"
if api_hint.exists():
raw = api_hint.read_text().strip().rstrip("/")
return raw[:-7] if raw.endswith("/api/v1") else raw
base_url = Path.home() / ".config" / "gitea" / "base-url"
if base_url.exists():
return base_url.read_text().strip().rstrip("/")
raise FileNotFoundError("Set GITEA_URL or create ~/.hermes/gitea_api")
# ── Data Sources ──────────────────────────────────────────────────────
GITEA_URL = resolve_gitea_url()
def read_token() -> str | None:
for path in [
Path.home() / ".config" / "gitea" / "timmy-token",
Path.home() / ".hermes" / "gitea_token_vps",
Path.home() / ".hermes" / "gitea_token_timmy",
]:
if path.exists():
return path.read_text().strip()
return None
def gitea_get(path: str, token: str | None) -> list | dict:
headers = {"Authorization": f"token {token}"} if token else {}
req = urllib.request.Request(f"{GITEA_URL}/api/v1{path}", headers=headers)
with urllib.request.urlopen(req, timeout=5) as resp:
return json.loads(resp.read().decode())
def get_model_health() -> dict:
path = HERMES_HOME / "model_health.json"
if not path.exists():
return {}
def get_ollama_models():
try:
return json.loads(path.read_text())
req = urllib.request.Request("http://localhost:11434/api/tags")
with urllib.request.urlopen(req, timeout=5) as resp:
return json.loads(resp.read()).get("models", [])
except Exception:
return {}
return []
def get_last_tick() -> dict:
path = TIMMY_HOME / "heartbeat" / "last_tick.json"
if not path.exists():
return {}
def get_loaded_models():
try:
return json.loads(path.read_text())
req = urllib.request.Request("http://localhost:11434/api/ps")
with urllib.request.urlopen(req, timeout=5) as resp:
return json.loads(resp.read()).get("models", [])
except Exception:
return {}
return []
def get_archive_checkpoint() -> dict:
path = TIMMY_HOME / "twitter-archive" / "checkpoint.json"
if not path.exists():
return {}
def get_huey_pid():
try:
return json.loads(path.read_text())
r = subprocess.run(["pgrep", "-f", "huey_consumer"],
capture_output=True, text=True, timeout=5)
return r.stdout.strip().split("\n")[0] if r.returncode == 0 else None
except Exception:
return {}
return None
def get_local_metrics(hours: int = 24) -> list[dict]:
records = []
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
if not METRICS_DIR.exists():
return records
for path in sorted(METRICS_DIR.glob("local_*.jsonl")):
for line in path.read_text().splitlines():
if not line.strip():
continue
try:
record = json.loads(line)
ts = datetime.fromisoformat(record["timestamp"])
if ts >= cutoff:
records.append(record)
except Exception:
continue
return records
def get_hermes_sessions() -> list[dict]:
def get_hermes_sessions():
sessions_file = HERMES_HOME / "sessions" / "sessions.json"
if not sessions_file.exists():
return []
@@ -126,7 +67,7 @@ def get_hermes_sessions() -> list[dict]:
return []
def get_session_rows(hours: int = 24):
def get_session_rows(hours=24):
state_db = HERMES_HOME / "state.db"
if not state_db.exists():
return []
@@ -150,14 +91,14 @@ def get_session_rows(hours: int = 24):
return []
def get_heartbeat_ticks(date_str: str | None = None) -> list[dict]:
def get_heartbeat_ticks(date_str=None):
if not date_str:
date_str = datetime.now().strftime("%Y%m%d")
tick_file = TIMMY_HOME / "heartbeat" / f"ticks_{date_str}.jsonl"
if not tick_file.exists():
return []
ticks = []
for line in tick_file.read_text().splitlines():
for line in tick_file.read_text().strip().split("\n"):
if not line.strip():
continue
try:
@@ -167,33 +108,42 @@ def get_heartbeat_ticks(date_str: str | None = None) -> list[dict]:
return ticks
def get_review_and_issue_state(token: str | None) -> dict:
state = {"prs": [], "review_queue": [], "unassigned": 0}
for repo in CORE_REPOS:
try:
prs = gitea_get(f"/repos/{repo}/pulls?state=open&limit=20", token)
for pr in prs:
pr["_repo"] = repo
state["prs"].append(pr)
except Exception:
continue
try:
issue_prs = gitea_get(f"/repos/{repo}/issues?state=open&limit=50&type=pulls", token)
for item in issue_prs:
assignees = [a.get("login", "") for a in (item.get("assignees") or [])]
if any(name in assignees for name in ("Timmy", "allegro")):
item["_repo"] = repo
state["review_queue"].append(item)
except Exception:
continue
try:
issues = gitea_get(f"/repos/{repo}/issues?state=open&limit=50&type=issues", token)
state["unassigned"] += sum(1 for issue in issues if not issue.get("assignees"))
except Exception:
continue
return state
def get_local_metrics(hours=24):
"""Read local inference metrics from jsonl files."""
records = []
cutoff = datetime.now(timezone.utc) - timedelta(hours=hours)
if not METRICS_DIR.exists():
return records
for f in sorted(METRICS_DIR.glob("local_*.jsonl")):
for line in f.read_text().strip().split("\n"):
if not line.strip():
continue
try:
r = json.loads(line)
ts = datetime.fromisoformat(r["timestamp"])
if ts >= cutoff:
records.append(r)
except Exception:
continue
return records
def get_cron_jobs():
"""Get Hermes cron job status."""
try:
r = subprocess.run(
["hermes", "cron", "list", "--json"],
capture_output=True, text=True, timeout=10
)
if r.returncode == 0:
return json.loads(r.stdout).get("jobs", [])
except Exception:
pass
return []
# ── Rendering ─────────────────────────────────────────────────────────
DIM = "\033[2m"
BOLD = "\033[1m"
GREEN = "\033[32m"
@@ -204,133 +154,119 @@ RST = "\033[0m"
CLR = "\033[2J\033[H"
def render(hours: int = 24) -> None:
token = read_token()
metrics = get_local_metrics(hours)
local_summary = summarize_local_metrics(metrics)
def render(hours=24):
models = get_ollama_models()
loaded = get_loaded_models()
huey_pid = get_huey_pid()
ticks = get_heartbeat_ticks()
health = get_model_health()
last_tick = get_last_tick()
checkpoint = get_archive_checkpoint()
metrics = get_local_metrics(hours)
sessions = get_hermes_sessions()
session_rows = get_session_rows(hours)
local_summary = summarize_local_metrics(metrics)
session_summary = summarize_session_rows(session_rows)
gitea = get_review_and_issue_state(token)
loaded_names = {m.get("name", "") for m in loaded}
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(CLR, end="")
print(f"{BOLD}{'=' * 72}")
print(" TIMMY WORKFLOW DASHBOARD")
print(f" {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"{'=' * 72}{RST}")
print(f"{BOLD}{'=' * 70}")
print(f" TIMMY MODEL DASHBOARD")
print(f" {now} | Huey: {GREEN}PID {huey_pid}{RST if huey_pid else f'{RED}DOWN{RST}'}")
print(f"{'=' * 70}{RST}")
print(f"\n {BOLD}HEARTBEAT{RST}")
print(f" {DIM}{'-' * 58}{RST}")
if last_tick:
sev = last_tick.get("decision", {}).get("severity", "?")
tick_id = last_tick.get("tick_id", "?")
model_decisions = sum(
1
for tick in ticks
if isinstance(tick.get("decision"), dict)
and tick["decision"].get("severity") != "fallback"
)
print(f" last tick: {tick_id}")
print(f" severity: {sev}")
print(f" ticks today: {len(ticks)} | model decisions: {model_decisions}")
# ── LOCAL MODELS ──
print(f"\n {BOLD}LOCAL MODELS (Ollama){RST}")
print(f" {DIM}{'-' * 55}{RST}")
if models:
for m in models:
name = m.get("name", "?")
size_gb = m.get("size", 0) / 1e9
if name in loaded_names:
status = f"{GREEN}IN VRAM{RST}"
else:
status = f"{DIM}on disk{RST}"
print(f" {name:35s} {size_gb:5.1f}GB {status}")
else:
print(f" {DIM}(no heartbeat data){RST}")
print(f" {RED}(Ollama not responding){RST}")
print(f"\n {BOLD}MODEL HEALTH{RST}")
print(f" {DIM}{'-' * 58}{RST}")
if health:
provider = GREEN if health.get("api_responding") else RED
inference = GREEN if health.get("inference_ok") else YELLOW
print(f" provider: {provider}{health.get('api_responding')}{RST}")
print(f" inference: {inference}{health.get('inference_ok')}{RST}")
print(f" models: {', '.join(health.get('models_loaded', [])[:4]) or '(none reported)'}")
else:
print(f" {DIM}(no model_health.json){RST}")
print(f"\n {BOLD}ARCHIVE PIPELINE{RST}")
print(f" {DIM}{'-' * 58}{RST}")
if checkpoint:
print(f" batches completed: {checkpoint.get('batches_completed', '?')}")
print(f" next offset: {checkpoint.get('next_offset', '?')}")
print(f" phase: {checkpoint.get('phase', '?')}")
else:
print(f" {DIM}(no archive checkpoint yet){RST}")
print(f"\n {BOLD}LOCAL METRICS ({len(metrics)} calls, last {hours}h){RST}")
print(f" {DIM}{'-' * 58}{RST}")
# ── LOCAL INFERENCE ACTIVITY ──
print(f"\n {BOLD}LOCAL INFERENCE ({len(metrics)} calls, last {hours}h){RST}")
print(f" {DIM}{'-' * 55}{RST}")
if metrics:
print(
f" Tokens: {local_summary['input_tokens']} in | "
f"{local_summary['output_tokens']} out | "
f"{local_summary['total_tokens']} total"
)
if local_summary.get("avg_latency_s") is not None:
print(f" Tokens: {local_summary['input_tokens']} in | {local_summary['output_tokens']} out | {local_summary['total_tokens']} total")
if local_summary.get('avg_latency_s') is not None:
print(f" Avg latency: {local_summary['avg_latency_s']:.2f}s")
if local_summary.get("avg_tokens_per_second") is not None:
if local_summary.get('avg_tokens_per_second') is not None:
print(f" Avg throughput: {GREEN}{local_summary['avg_tokens_per_second']:.2f} tok/s{RST}")
for caller, stats in sorted(local_summary["by_caller"].items()):
err = f" {RED}err:{stats['failed_calls']}{RST}" if stats["failed_calls"] else ""
print(
f" {caller:24s} calls={stats['calls']:3d} "
f"tok={stats['total_tokens']:5d} {GREEN}ok:{stats['successful_calls']}{RST}{err}"
)
else:
print(f" {DIM}(no local metrics yet){RST}")
for caller, stats in sorted(local_summary['by_caller'].items()):
err = f" {RED}err:{stats['failed_calls']}{RST}" if stats['failed_calls'] else ""
print(f" {caller:25s} calls:{stats['calls']:4d} tokens:{stats['total_tokens']:5d} {GREEN}ok:{stats['successful_calls']}{RST}{err}")
print(f"\n {BOLD}SESSION LOAD{RST}")
print(f" {DIM}{'-' * 58}{RST}")
local_sessions = [s for s in sessions if "localhost" in str(s.get("base_url", ""))]
print(f"\n {DIM}Models used:{RST}")
for model, stats in sorted(local_summary['by_model'].items(), key=lambda x: -x[1]['calls']):
print(f" {model:30s} {stats['calls']} calls {stats['total_tokens']} tok")
else:
print(f" {DIM}(no local calls recorded yet){RST}")
# ── HEARTBEAT STATUS ──
print(f"\n {BOLD}HEARTBEAT ({len(ticks)} ticks today){RST}")
print(f" {DIM}{'-' * 55}{RST}")
if ticks:
last = ticks[-1]
decision = last.get("decision", last.get("actions", {}))
if isinstance(decision, dict):
severity = decision.get("severity", "unknown")
reasoning = decision.get("reasoning", "")
sev_color = GREEN if severity == "ok" else YELLOW if severity == "warning" else RED
print(f" Last tick: {last.get('tick_id', '?')}")
print(f" Severity: {sev_color}{severity}{RST}")
if reasoning:
print(f" Reasoning: {reasoning[:65]}")
else:
print(f" Last tick: {last.get('tick_id', '?')}")
actions = last.get("actions", [])
print(f" Actions: {actions if actions else 'none'}")
model_decisions = sum(1 for t in ticks
if isinstance(t.get("decision"), dict)
and t["decision"].get("severity") != "fallback")
fallback = len(ticks) - model_decisions
print(f" {CYAN}Model: {model_decisions}{RST} | {DIM}Fallback: {fallback}{RST}")
else:
print(f" {DIM}(no ticks today){RST}")
# ── HERMES SESSIONS / SOVEREIGNTY LOAD ──
local_sessions = [s for s in sessions if "localhost:11434" in str(s.get("base_url", ""))]
cloud_sessions = [s for s in sessions if s not in local_sessions]
print(
f" Session cache: {len(sessions)} total | "
f"{GREEN}{len(local_sessions)} local{RST} | "
f"{YELLOW}{len(cloud_sessions)} remote{RST}"
)
print(f"\n {BOLD}HERMES SESSIONS / SOVEREIGNTY LOAD{RST}")
print(f" {DIM}{'-' * 55}{RST}")
print(f" Session cache: {len(sessions)} total | {GREEN}{len(local_sessions)} local{RST} | {YELLOW}{len(cloud_sessions)} cloud{RST}")
if session_rows:
print(
f" Session DB: {session_summary['total_sessions']} total | "
f"{GREEN}{session_summary['local_sessions']} local{RST} | "
f"{YELLOW}{session_summary['cloud_sessions']} remote{RST}"
)
print(
f" Token est: {GREEN}{session_summary['local_est_tokens']} local{RST} | "
f"{YELLOW}{session_summary['cloud_est_tokens']} remote{RST}"
)
print(f" Est remote cost: ${session_summary['cloud_est_cost_usd']:.4f}")
print(f" Session DB: {session_summary['total_sessions']} total | {GREEN}{session_summary['local_sessions']} local{RST} | {YELLOW}{session_summary['cloud_sessions']} cloud{RST}")
print(f" Token est: {GREEN}{session_summary['local_est_tokens']} local{RST} | {YELLOW}{session_summary['cloud_est_tokens']} cloud{RST}")
print(f" Est cloud cost: ${session_summary['cloud_est_cost_usd']:.4f}")
else:
print(f" {DIM}(no session-db stats available){RST}")
print(f"\n {BOLD}REVIEW QUEUE{RST}")
print(f" {DIM}{'-' * 58}{RST}")
if gitea["review_queue"]:
for item in gitea["review_queue"][:8]:
repo = item["_repo"].split("/", 1)[1]
print(f" {repo:12s} #{item['number']:<4d} {item['title'][:42]}")
else:
print(f" {DIM}(clear){RST}")
# ── ACTIVE LOOPS ──
print(f"\n {BOLD}ACTIVE LOOPS{RST}")
print(f" {DIM}{'-' * 55}{RST}")
print(f" {CYAN}heartbeat_tick{RST} 10m hermes4:14b DECIDE phase")
print(f" {DIM}model_health{RST} 5m (local check) Ollama ping")
print(f" {DIM}gemini_worker{RST} 20m gemini-2.5-pro aider")
print(f" {DIM}grok_worker{RST} 20m grok-3-fast opencode")
print(f" {DIM}cross_review{RST} 30m gemini+grok PR review")
print(f"\n {BOLD}OPEN PRS / UNASSIGNED{RST}")
print(f" {DIM}{'-' * 58}{RST}")
print(f" open PRs: {len(gitea['prs'])}")
print(f" unassigned issues: {gitea['unassigned']}")
for pr in gitea["prs"][:6]:
repo = pr["_repo"].split("/", 1)[1]
print(f" PR {repo:10s} #{pr['number']:<4d} {pr['title'][:40]}")
print(f"\n{BOLD}{'=' * 72}{RST}")
print(f"\n{BOLD}{'=' * 70}{RST}")
print(f" {DIM}Refresh: timmy-dashboard --watch | History: --hours=N{RST}")
if __name__ == "__main__":
watch = "--watch" in sys.argv
hours = 24
for arg in sys.argv[1:]:
if arg.startswith("--hours="):
hours = int(arg.split("=", 1)[1])
for a in sys.argv[1:]:
if a.startswith("--hours="):
hours = int(a.split("=")[1])
if watch:
try:

View File

@@ -3,12 +3,12 @@
# Uses Hermes CLI plus workforce-manager to triage and review.
# Timmy is the brain. Other agents are the hands.
set -uo pipefail\n\nSCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
set -uo pipefail
LOG_DIR="$HOME/.hermes/logs"
LOG="$LOG_DIR/timmy-orchestrator.log"
PIDFILE="$LOG_DIR/timmy-orchestrator.pid"
GITEA_URL="${GITEA_URL:-https://forge.alexanderwhitestone.com}"
GITEA_URL="http://143.198.27.163:3000"
GITEA_TOKEN=$(cat "$HOME/.hermes/gitea_token_vps" 2>/dev/null) # Timmy token, NOT rockachopa
CYCLE_INTERVAL=300
HERMES_TIMEOUT=180
@@ -40,7 +40,6 @@ gather_state() {
> "$state_dir/unassigned.txt"
> "$state_dir/open_prs.txt"
> "$state_dir/agent_status.txt"
> "$state_dir/uncommitted_work.txt"
for repo in $REPOS; do
local short=$(echo "$repo" | cut -d/ -f2)
@@ -65,30 +64,8 @@ for p in json.load(sys.stdin):
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"
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"
tail -1 "/tmp/kimi-heartbeat.log" 2>/dev/null | xargs -I{} echo "Kimi last event: {}" >> "$state_dir/agent_status.txt"
# Scan worktrees for uncommitted work
for wt_dir in "$HOME/worktrees"/*/; do
[ -d "$wt_dir" ] || continue
[ -d "$wt_dir/.git" ] || continue
local dirty
dirty=$(cd "$wt_dir" && git status --porcelain 2>/dev/null | wc -l | tr -d " ")
if [ "${dirty:-0}" -gt 0 ]; then
local branch
branch=$(cd "$wt_dir" && git branch --show-current 2>/dev/null || echo "?")
local age=""
local last_commit
last_commit=$(cd "$wt_dir" && git log -1 --format=%ct 2>/dev/null || echo 0)
local now=$(date +%s)
local stale_mins=$(( (now - last_commit) / 60 ))
echo "DIR=$wt_dir BRANCH=$branch DIRTY=$dirty STALE=${stale_mins}m" >> "$state_dir/uncommitted_work.txt"
fi
done
tail -50 "$LOG_DIR/claude-loop.log" 2>/dev/null | grep -c "SUCCESS" | xargs -I{} echo "Recent successes: {}" >> "$state_dir/agent_status.txt"
tail -50 "$LOG_DIR/claude-loop.log" 2>/dev/null | grep -c "FAILED" | xargs -I{} echo "Recent failures: {}" >> "$state_dir/agent_status.txt"
echo "$state_dir"
}
@@ -100,25 +77,6 @@ run_triage() {
log "Cycle: $unassigned_count unassigned, $pr_count open PRs"
# Check for uncommitted work — nag if stale
local uncommitted_count
uncommitted_count=$(wc -l < "$state_dir/uncommitted_work.txt" 2>/dev/null | tr -d " " || echo 0)
if [ "${uncommitted_count:-0}" -gt 0 ]; then
log "WARNING: $uncommitted_count worktree(s) with uncommitted work"
while IFS= read -r line; do
log " UNCOMMITTED: $line"
# Auto-commit stale work (>60 min without commit)
local stale=$(echo "$line" | sed 's/.*STALE=\([0-9]*\)m.*/\1/')
local wt_dir=$(echo "$line" | sed 's/.*DIR=\([^ ]*\) .*/\1/')
if [ "${stale:-0}" -gt 60 ]; then
log " AUTO-COMMITTING stale work in $wt_dir (${stale}m stale)"
(cd "$wt_dir" && git add -A && git commit -m "WIP: orchestrator auto-commit — ${stale}m stale work
Preserved by timmy-orchestrator to prevent loss." 2>/dev/null && git push 2>/dev/null) && log " COMMITTED: $wt_dir" || log " COMMIT FAILED: $wt_dir"
fi
done < "$state_dir/uncommitted_work.txt"
fi
# If nothing to do, skip the LLM call
if [ "$unassigned_count" -eq 0 ] && [ "$pr_count" -eq 0 ]; then
log "Nothing to triage"
@@ -206,14 +164,7 @@ HEADER
fi
echo "" >> "$prompt_file"
cat >> "$prompt_file" <<'FOOTER'
INSTRUCTIONS: For EACH PR above, do ONE of the following RIGHT NOW using your terminal tool:
- Run the merge curl command if the diff looks good
- Run the close curl command if it is a duplicate or garbage
- Run the comment curl command only if there is a clear bug
IMPORTANT: Actually run the curl commands. Do not just describe what you would do. Finish means the PR world-state changed.
FOOTER
echo "Review each PR above. Execute curl commands for your decisions. Be brief." >> "$prompt_file"
local prompt_text
prompt_text=$(cat "$prompt_file")
@@ -236,12 +187,6 @@ FOOTER
log "=== Timmy Orchestrator Started (PID $$) ==="
log "Cycle: ${CYCLE_INTERVAL}s | Auto-assign: ${AUTO_ASSIGN_UNASSIGNED} | Inference surface: Hermes CLI"
# Start auto-commit-guard daemon for work preservation
if ! pgrep -f "auto-commit-guard.sh" >/dev/null 2>&1; then
nohup bash "$SCRIPT_DIR/auto-commit-guard.sh" 120 >> "$LOG_DIR/auto-commit-guard.log" 2>&1 &
log "Started auto-commit-guard daemon (PID $!)"
fi
WORKFORCE_CYCLE=0
while true; do

View File

@@ -1,182 +1,284 @@
#!/usr/bin/env bash
# ── Timmy Status Sidebar ───────────────────────────────────────────────
# Compact current-state view for the local Hermes + Timmy workflow.
# ── Timmy Loop Status Panel ────────────────────────────────────────────
# Compact, info-dense sidebar for the tmux development loop.
# Refreshes every 10s. Designed for ~40-col wide pane.
# ───────────────────────────────────────────────────────────────────────
set -euo pipefail
STATE="$HOME/Timmy-Time-dashboard/.loop/state.json"
REPO="$HOME/Timmy-Time-dashboard"
TOKEN=$(cat ~/.hermes/gitea_token 2>/dev/null)
API="http://143.198.27.163:3000/api/v1/repos/rockachopa/Timmy-time-dashboard"
resolve_gitea_url() {
if [ -n "${GITEA_URL:-}" ]; then
printf '%s\n' "${GITEA_URL%/}"
return 0
fi
if [ -f "$HOME/.hermes/gitea_api" ]; then
python3 - "$HOME/.hermes/gitea_api" <<'PY'
from pathlib import Path
import sys
# ── Colors ──
B='\033[1m' # bold
D='\033[2m' # dim
R='\033[0m' # reset
G='\033[32m' # green
Y='\033[33m' # yellow
RD='\033[31m' # red
C='\033[36m' # cyan
M='\033[35m' # magenta
W='\033[37m' # white
BG='\033[42;30m' # green bg
BY='\033[43;30m' # yellow bg
BR='\033[41;37m' # red bg
raw = Path(sys.argv[1]).read_text().strip().rstrip("/")
print(raw[:-7] if raw.endswith("/api/v1") else raw)
PY
return 0
fi
if [ -f "$HOME/.config/gitea/base-url" ]; then
tr -d '[:space:]' < "$HOME/.config/gitea/base-url"
return 0
fi
echo "ERROR: set GITEA_URL or create ~/.hermes/gitea_api" >&2
return 1
}
# How wide is our pane?
COLS=$(tput cols 2>/dev/null || echo 40)
resolve_ops_token() {
local token_file
for token_file in \
"$HOME/.config/gitea/timmy-token" \
"$HOME/.hermes/gitea_token_vps" \
"$HOME/.hermes/gitea_token_timmy"; do
if [ -f "$token_file" ]; then
tr -d '[:space:]' < "$token_file"
return 0
fi
done
return 1
}
GITEA_URL="$(resolve_gitea_url)"
CORE_REPOS="${CORE_REPOS:-Timmy_Foundation/the-nexus Timmy_Foundation/timmy-home Timmy_Foundation/timmy-config Timmy_Foundation/hermes-agent}"
TOKEN="$(resolve_ops_token || true)"
[ -z "$TOKEN" ] && echo "WARN: no approved Timmy Gitea token found; status sidebar will use unauthenticated API calls" >&2
B='\033[1m'
D='\033[2m'
R='\033[0m'
G='\033[32m'
Y='\033[33m'
RD='\033[31m'
C='\033[36m'
COLS=$(tput cols 2>/dev/null || echo 48)
hr() { printf "${D}"; printf '─%.0s' $(seq 1 "$COLS"); printf "${R}\n"; }
while true; do
clear
echo -e "${B}${C} TIMMY STATUS${R} ${D}$(date '+%H:%M:%S')${R}"
# ── Header ──
echo -e "${B}${C} ⚙ TIMMY DEV LOOP${R} ${D}$(date '+%H:%M:%S')${R}"
hr
python3 - "$HOME/.timmy" "$HOME/.hermes" <<'PY'
# ── Loop State ──
if [ -f "$STATE" ]; then
eval "$(python3 -c "
import json, sys
with open('$STATE') as f: s = json.load(f)
print(f'CYCLE={s.get(\"cycle\",\"?\")}')" 2>/dev/null)"
STATUS=$(python3 -c "import json; print(json.load(open('$STATE'))['status'])" 2>/dev/null || echo "?")
LAST_OK=$(python3 -c "
import json
import sys
from pathlib import Path
timmy = Path(sys.argv[1])
hermes = Path(sys.argv[2])
last_tick = timmy / "heartbeat" / "last_tick.json"
model_health = hermes / "model_health.json"
checkpoint = timmy / "twitter-archive" / "checkpoint.json"
if last_tick.exists():
try:
tick = json.loads(last_tick.read_text())
sev = tick.get("decision", {}).get("severity", "?")
tick_id = tick.get("tick_id", "?")
print(f" heartbeat {tick_id} severity={sev}")
except Exception:
print(" heartbeat unreadable")
else:
print(" heartbeat missing")
if model_health.exists():
try:
health = json.loads(model_health.read_text())
provider_ok = health.get("api_responding")
inference_ok = health.get("inference_ok")
models = len(health.get("models_loaded", []) or [])
print(f" model api={provider_ok} inference={inference_ok} models={models}")
except Exception:
print(" model unreadable")
else:
print(" model missing")
if checkpoint.exists():
try:
cp = json.loads(checkpoint.read_text())
print(f" archive batches={cp.get('batches_completed', '?')} next={cp.get('next_offset', '?')} phase={cp.get('phase', '?')}")
except Exception:
print(" archive unreadable")
else:
print(" archive missing")
PY
hr
echo -e " ${B}freshness${R}"
~/.hermes/bin/pipeline-freshness.sh 2>/dev/null | sed 's/^/ /' || echo -e " ${Y}unknown${R}"
hr
echo -e " ${B}review queue${R}"
python3 - "$GITEA_URL" "$TOKEN" "$CORE_REPOS" <<'PY'
from datetime import datetime, timezone
s = json.load(open('$STATE'))
t = s.get('last_completed','')
if t:
dt = datetime.fromisoformat(t.replace('Z','+00:00'))
delta = datetime.now(timezone.utc) - dt
mins = int(delta.total_seconds() / 60)
if mins < 60: print(f'{mins}m ago')
else: print(f'{mins//60}h {mins%60}m ago')
else: print('never')
" 2>/dev/null || echo "?")
CLOSED=$(python3 -c "import json; print(len(json.load(open('$STATE')).get('issues_closed',[])))" 2>/dev/null || echo 0)
CREATED=$(python3 -c "import json; print(len(json.load(open('$STATE')).get('issues_created',[])))" 2>/dev/null || echo 0)
ERRS=$(python3 -c "import json; print(len(json.load(open('$STATE')).get('errors',[])))" 2>/dev/null || echo 0)
LAST_ISSUE=$(python3 -c "import json; print(json.load(open('$STATE')).get('last_issue','—'))" 2>/dev/null || echo "—")
LAST_PR=$(python3 -c "import json; print(json.load(open('$STATE')).get('last_pr','—'))" 2>/dev/null || echo "—")
TESTS=$(python3 -c "
import json
import sys
import urllib.request
s = json.load(open('$STATE'))
t = s.get('test_results',{})
if t:
print(f\"{t.get('passed',0)} pass, {t.get('failed',0)} fail, {t.get('coverage','?')} cov\")
else:
print('no data')
" 2>/dev/null || echo "no data")
base = sys.argv[1].rstrip("/")
token = sys.argv[2]
repos = sys.argv[3].split()
headers = {"Authorization": f"token {token}"} if token else {}
# Status badge
case "$STATUS" in
working) BADGE="${BY} WORKING ${R}" ;;
idle) BADGE="${BG} IDLE ${R}" ;;
error) BADGE="${BR} ERROR ${R}" ;;
*) BADGE="${D} $STATUS ${R}" ;;
esac
count = 0
for repo in repos:
try:
req = urllib.request.Request(f"{base}/api/v1/repos/{repo}/issues?state=open&limit=50&type=pulls", headers=headers)
with urllib.request.urlopen(req, timeout=5) as resp:
items = json.loads(resp.read().decode())
for item in items:
assignees = [a.get("login", "") for a in (item.get("assignees") or [])]
if any(name in assignees for name in ("Timmy", "allegro")):
print(f" {repo.split('/',1)[1]:12s} #{item['number']:<4d} {item['title'][:28]}")
count += 1
if count >= 6:
raise SystemExit
except SystemExit:
break
except Exception:
continue
if count == 0:
print(" (clear)")
PY
echo -e " ${B}Status${R} $BADGE ${D}cycle${R} ${B}$CYCLE${R}"
echo -e " ${B}Last OK${R} ${G}$LAST_OK${R} ${D}issue${R} #$LAST_ISSUE ${D}PR${R} #$LAST_PR"
echo -e " ${G}${R} $CLOSED closed ${C}+${R} $CREATED created ${RD}${R} $ERRS errs"
echo -e " ${D}Tests:${R} $TESTS"
else
echo -e " ${RD}No state file${R}"
fi
hr
echo -e " ${B}unassigned${R}"
python3 - "$GITEA_URL" "$TOKEN" "$CORE_REPOS" <<'PY'
# ── Ollama Status ──
echo -e " ${B}${M}◆ OLLAMA${R}"
OLLAMA_PS=$(curl -s http://localhost:11434/api/ps 2>/dev/null)
if [ -n "$OLLAMA_PS" ] && echo "$OLLAMA_PS" | python3 -c "import sys,json; json.load(sys.stdin)" &>/dev/null; then
python3 -c "
import json, sys
data = json.loads('''$OLLAMA_PS''')
models = data.get('models', [])
if not models:
print(' \033[2m(no models loaded)\033[0m')
for m in models:
name = m.get('name','?')
vram = m.get('size_vram', 0) / 1e9
exp = m.get('expires_at','')
print(f' \033[32m●\033[0m {name} \033[2m{vram:.1f}GB VRAM\033[0m')
" 2>/dev/null
else
echo -e " ${RD}● offline${R}"
fi
# ── Timmy Health ──
TIMMY_HEALTH=$(curl -s --max-time 2 http://localhost:8000/health 2>/dev/null)
if [ -n "$TIMMY_HEALTH" ]; then
python3 -c "
import json
import sys
import urllib.request
base = sys.argv[1].rstrip("/")
token = sys.argv[2]
repos = sys.argv[3].split()
headers = {"Authorization": f"token {token}"} if token else {}
count = 0
for repo in repos:
try:
req = urllib.request.Request(f"{base}/api/v1/repos/{repo}/issues?state=open&limit=50&type=issues", headers=headers)
with urllib.request.urlopen(req, timeout=5) as resp:
items = json.loads(resp.read().decode())
for item in items:
if not item.get("assignees"):
print(f" {repo.split('/',1)[1]:12s} #{item['number']:<4d} {item['title'][:28]}")
count += 1
if count >= 6:
raise SystemExit
except SystemExit:
break
except Exception:
continue
if count == 0:
print(" (none)")
PY
h = json.loads('''$TIMMY_HEALTH''')
status = h.get('status','?')
ollama = h.get('services',{}).get('ollama','?')
model = h.get('llm_model','?')
agent_st = list(h.get('agents',{}).values())[0].get('status','?') if h.get('agents') else '?'
up = int(h.get('uptime_seconds',0))
hrs, rem = divmod(up, 3600)
mins = rem // 60
print(f' \033[1m\033[35m◆ TIMMY DASHBOARD\033[0m')
print(f' \033[32m●\033[0m {status} model={model}')
print(f' \033[2magent={agent_st} ollama={ollama} up={hrs}h{mins}m\033[0m')
" 2>/dev/null
else
echo -e " ${B}${M}◆ TIMMY DASHBOARD${R}"
echo -e " ${RD}● unreachable${R}"
fi
hr
sleep 10
# ── Open Issues ──
echo -e " ${B}${Y}▶ OPEN ISSUES${R}"
if [ -n "$TOKEN" ]; then
curl -s "${API}/issues?state=open&limit=10&sort=created&direction=desc" \
-H "Authorization: token $TOKEN" 2>/dev/null | \
python3 -c "
import json, sys
try:
issues = json.load(sys.stdin)
if not issues:
print(' \033[2m(none)\033[0m')
for i in issues[:10]:
num = i['number']
title = i['title'][:36]
labels = ','.join(l['name'][:8] for l in i.get('labels',[]))
lbl = f' \033[2m[{labels}]\033[0m' if labels else ''
print(f' \033[33m#{num:<4d}\033[0m {title}{lbl}')
if len(issues) > 10:
print(f' \033[2m... +{len(issues)-10} more\033[0m')
except: print(' \033[2m(fetch failed)\033[0m')
" 2>/dev/null
else
echo -e " ${RD}(no token)${R}"
fi
# ── Open PRs ──
echo -e " ${B}${G}▶ OPEN PRs${R}"
if [ -n "$TOKEN" ]; then
curl -s "${API}/pulls?state=open&limit=5" \
-H "Authorization: token $TOKEN" 2>/dev/null | \
python3 -c "
import json, sys
try:
prs = json.load(sys.stdin)
if not prs:
print(' \033[2m(none)\033[0m')
for p in prs[:5]:
num = p['number']
title = p['title'][:36]
print(f' \033[32mPR #{num:<4d}\033[0m {title}')
except: print(' \033[2m(fetch failed)\033[0m')
" 2>/dev/null
else
echo -e " ${RD}(no token)${R}"
fi
hr
# ── Git Log ──
echo -e " ${B}${D}▶ RECENT COMMITS${R}"
cd "$REPO" 2>/dev/null && git log --oneline --no-decorate -6 2>/dev/null | while read line; do
HASH=$(echo "$line" | cut -c1-7)
MSG=$(echo "$line" | cut -c9- | cut -c1-32)
echo -e " ${C}${HASH}${R} ${D}${MSG}${R}"
done
hr
# ── Claims ──
CLAIMS_FILE="$REPO/.loop/claims.json"
if [ -f "$CLAIMS_FILE" ]; then
CLAIMS=$(python3 -c "
import json
with open('$CLAIMS_FILE') as f: c = json.load(f)
active = [(k,v) for k,v in c.items() if v.get('status') == 'active']
if active:
for k,v in active:
print(f' \033[33m⚡\033[0m #{k} claimed by {v.get(\"agent\",\"?\")[:12]}')
else:
print(' \033[2m(none active)\033[0m')
" 2>/dev/null)
if [ -n "$CLAIMS" ]; then
echo -e " ${B}${Y}▶ CLAIMED${R}"
echo "$CLAIMS"
fi
fi
# ── System ──
echo -e " ${B}${D}▶ SYSTEM${R}"
# Disk
DISK=$(df -h / 2>/dev/null | tail -1 | awk '{print $4 " free / " $2}')
echo -e " ${D}Disk:${R} $DISK"
# Memory (macOS)
if command -v memory_pressure &>/dev/null; then
MEM_PRESS=$(memory_pressure 2>/dev/null | grep "System-wide" | head -1 | sed 's/.*: //')
echo -e " ${D}Mem:${R} $MEM_PRESS"
elif [ -f /proc/meminfo ]; then
MEM=$(awk '/MemAvailable/{printf "%.1fGB free", $2/1048576}' /proc/meminfo 2>/dev/null)
echo -e " ${D}Mem:${R} $MEM"
fi
# CPU load
LOAD=$(uptime | sed 's/.*averages: //' | cut -d',' -f1 | xargs)
echo -e " ${D}Load:${R} $LOAD"
hr
# ── Notes from last cycle ──
if [ -f "$STATE" ]; then
NOTES=$(python3 -c "
import json
s = json.load(open('$STATE'))
n = s.get('notes','')
if n:
lines = n[:150]
if len(n) > 150: lines += '...'
print(lines)
" 2>/dev/null)
if [ -n "$NOTES" ]; then
echo -e " ${B}${D}▶ LAST CYCLE NOTE${R}"
echo -e " ${D}${NOTES}${R}"
hr
fi
# Timmy observations
TIMMY_OBS=$(python3 -c "
import json
s = json.load(open('$STATE'))
obs = s.get('timmy_observations','')
if obs:
lines = obs[:120]
if len(obs) > 120: lines += '...'
print(lines)
" 2>/dev/null)
if [ -n "$TIMMY_OBS" ]; then
echo -e " ${B}${M}▶ TIMMY SAYS${R}"
echo -e " ${D}${TIMMY_OBS}${R}"
hr
fi
fi
# ── Watchdog: restart loop if it died ──────────────────────────────
LOOP_LOCK="/tmp/timmy-loop.lock"
if [ -f "$LOOP_LOCK" ]; then
LOOP_PID=$(cat "$LOOP_LOCK" 2>/dev/null)
if ! kill -0 "$LOOP_PID" 2>/dev/null; then
echo -e " ${BR} ⚠ LOOP DIED — RESTARTING ${R}"
rm -f "$LOOP_LOCK"
tmux send-keys -t "dev:2.1" "bash ~/.hermes/bin/timmy-loop.sh" Enter 2>/dev/null
fi
else
# No lock file at all — loop never started or was killed
if ! pgrep -f "timmy-loop.sh" >/dev/null 2>&1; then
echo -e " ${BR} ⚠ LOOP NOT RUNNING — STARTING ${R}"
tmux send-keys -t "dev:2.1" "bash ~/.hermes/bin/timmy-loop.sh" Enter 2>/dev/null
fi
fi
echo -e " ${D}↻ 8s${R}"
sleep 8
done

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
{
"updated_at": "2026-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

@@ -1,91 +0,0 @@
# Code Claw delegation
Purpose:
- give the team a clean way to hand issues to `claw-code`
- let Code Claw work from Gitea instead of ad hoc local prompts
- keep queue state visible through labels and comments
## What it is
Code Claw is a separate local runtime from Hermes.
Current lane:
- runtime: local patched `~/code-claw`
- backend: OpenRouter
- model: `qwen/qwen3.6-plus:free`
- Gitea identity: `claw-code`
- dispatch style: assign in Gitea, heartbeat picks it up every 15 minutes
## Trigger methods
Either of these is enough:
- assign the issue to `claw-code`
- add label `assigned-claw-code`
## Label lifecycle
- `assigned-claw-code` — queued
- `claw-code-in-progress` — picked up by heartbeat
- `claw-code-done` — Code Claw completed a pass
## Repo coverage
Currently wired:
- `Timmy_Foundation/timmy-home`
- `Timmy_Foundation/timmy-config`
- `Timmy_Foundation/the-nexus`
- `Timmy_Foundation/hermes-agent`
## Operational flow
1. Team assigns issue to `claw-code` or adds `assigned-claw-code`
2. launchd heartbeat runs every 15 minutes
3. Timmy posts a pickup comment
4. worker clones the target repo
5. worker creates branch `claw-code/issue-<num>`
6. worker runs Code Claw against the issue context
7. if work exists, worker pushes and opens a PR
8. issue is marked `claw-code-done`
9. completion comment links branch + PR
## Logs and files
Local files:
- heartbeat script: `~/.timmy/uniwizard/codeclaw_qwen_heartbeat.py`
- worker script: `~/.timmy/uniwizard/codeclaw_qwen_worker.py`
- launchd job: `~/Library/LaunchAgents/ai.timmy.codeclaw-qwen-heartbeat.plist`
Logs:
- heartbeat log: `/tmp/codeclaw-qwen-heartbeat.log`
- worker log: `/tmp/codeclaw-qwen-worker-<issue>.log`
## Best-fit work
Use Code Claw for:
- small code/config/doc issues
- repo hygiene
- isolated bugfixes
- narrow CI and `.gitignore` work
- quick issue-driven patches where a PR is the desired output
Do not use it first for:
- giant epics
- broad architecture KT
- local game embodiment tasks
- complex multi-repo archaeology
## Proof of life
Smoke-tested on:
- `Timmy_Foundation/timmy-config#232`
Observed:
- pickup comment posted
- branch `claw-code/issue-232` created
- PR opened by `claw-code`
## Notes
- Exact PR matching matters. Do not trust broad Gitea PR queries without post-filtering by branch.
- This lane is intentionally simple and issue-driven.
- Treat it like a specialized intern: useful, fast, and bounded.

View File

@@ -1,16 +1,19 @@
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
@@ -25,71 +28,81 @@ 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
enabled: false
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: ''
smart_model_routing:
enabled: false
max_simple_chars: 160
max_simple_words: 28
cheap_model: {}
max_simple_chars: 200
max_simple_words: 35
cheap_model:
provider: ''
model: ''
base_url: ''
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
@@ -101,7 +114,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 +150,7 @@ delegation:
provider: ''
base_url: ''
api_key: ''
max_iterations: 50
prefill_messages_file: ''
honcho: {}
timezone: ''
@@ -160,66 +174,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

@@ -81,7 +81,33 @@
"last_error": null,
"deliver": "local",
"origin": null,
"state": "scheduled",
"state": "scheduled"
},
{
"id": "5e9d952871bc",
"name": "Agent Status Check",
"prompt": "Check which tmux panes are idle vs working, report utilization",
"schedule": {
"kind": "interval",
"minutes": 10,
"display": "every 10m"
},
"schedule_display": "every 10m",
"repeat": {
"times": null,
"completed": 8
},
"enabled": false,
"created_at": "2026-03-24T11:28:46.409727-04:00",
"next_run_at": "2026-03-24T15:45:58.108921-04:00",
"last_run_at": "2026-03-24T15:35:58.108921-04:00",
"last_status": "ok",
"last_error": null,
"deliver": "local",
"origin": null,
"state": "paused",
"paused_at": "2026-03-24T16:23:03.869047-04:00",
"paused_reason": "Dashboard repo frozen - loops redirected to the-nexus",
"skills": [],
"skill": null
},
@@ -106,97 +132,8 @@
"last_status": null,
"last_error": null,
"deliver": "local",
"origin": null,
"skills": [],
"skill": null
},
{
"id": "muda-audit-weekly",
"name": "Muda Audit",
"prompt": "Run the Muda Audit script at /root/wizards/ezra/workspace/timmy-config/fleet/muda-audit.sh. The script measures the 7 wastes across the fleet and posts a report to Telegram. Report whether it succeeded or failed.",
"schedule": {
"kind": "cron",
"expr": "0 21 * * 0",
"display": "0 21 * * 0"
},
"schedule_display": "0 21 * * 0",
"repeat": {
"times": null,
"completed": 0
},
"enabled": true,
"created_at": "2026-04-07T15:00:00+00:00",
"next_run_at": null,
"last_run_at": null,
"last_status": null,
"last_error": null,
"deliver": "local",
"origin": null,
"state": "scheduled",
"paused_at": null,
"paused_reason": null,
"skills": [],
"skill": null
},
{
"id": "kaizen-retro-349",
"name": "Kaizen Retro",
"prompt": "Run the automated burn-cycle retrospective. Execute: cd /root/wizards/ezra/workspace/timmy-config && ./bin/kaizen-retro.sh",
"model": "hermes3:latest",
"provider": "ollama",
"base_url": "http://localhost:11434/v1",
"schedule": {
"kind": "interval",
"minutes": 1440,
"display": "every 1440m"
},
"schedule_display": "daily at 07:30",
"repeat": {
"times": null,
"completed": 0
},
"enabled": true,
"created_at": "2026-04-07T15:30:00.000000Z",
"next_run_at": "2026-04-08T07:30:00.000000Z",
"last_run_at": null,
"last_status": null,
"last_error": null,
"deliver": "local",
"origin": null,
"state": "scheduled",
"paused_at": null,
"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
"origin": null
}
],
"updated_at": "2026-04-13T02:00:00+00:00"
}
"updated_at": "2026-03-24T16:23:03.869797-04:00"
}

View File

@@ -1,2 +0,0 @@
# Muda Audit — run every Sunday at 21:00
0 21 * * 0 cd /root/wizards/ezra/workspace/timmy-config && bash fleet/muda-audit.sh >> /tmp/muda-audit.log 2>&1

View File

@@ -1,9 +0,0 @@
- name: Nightly Pipeline Scheduler
schedule: '*/30 18-23,0-8 * * *' # Every 30 min, off-peak hours only
tasks:
- name: Check and start pipelines
shell: "bash scripts/nightly-pipeline-scheduler.sh"
env:
PIPELINE_TOKEN_LIMIT: "500000"
PIPELINE_PEAK_START: "9"
PIPELINE_PEAK_END: "18"

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,24 +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">
<plist version="1.0">
<dict>
<key>Label</key>
<string>ai.timmy.auto-commit-guard</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/apayne/.hermes/bin/auto-commit-guard.sh</string>
<string>120</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/apayne/.hermes/logs/auto-commit-guard.stdout.log</string>
<key>StandardErrorPath</key>
<string>/Users/apayne/.hermes/logs/auto-commit-guard.stderr.log</string>
<key>WorkingDirectory</key>
<string>/Users/apayne</string>
</dict>
</plist>

View File

@@ -1,58 +0,0 @@
# Caddy configuration for Conduit Matrix homeserver
# Location: /etc/caddy/conf.d/matrix.conf (imported by main Caddyfile)
# Reference: docs/matrix-fleet-comms/README.md
matrix.timmy.foundation {
# Reverse proxy to Conduit
reverse_proxy localhost:8448 {
# Headers for WebSocket upgrade (client sync)
header_up Host {host}
header_up X-Real-IP {remote}
header_up X-Forwarded-For {remote}
header_up X-Forwarded-Proto {scheme}
}
# Security headers
header {
X-Frame-Options DENY
X-Content-Type-Options nosniff
X-XSS-Protection "1; mode=block"
Referrer-Policy strict-origin-when-cross-origin
Permissions-Policy "geolocation=(), microphone=(), camera=()"
}
# Enable compression
encode gzip zstd
# Let's Encrypt automatic TLS
tls {
# Email for renewal notifications
# Uncomment and set: email admin@timmy.foundation
}
# Logging
log {
output file /var/log/caddy/matrix-access.log {
roll_size 100mb
roll_keep 5
}
}
}
# Well-known delegation for Matrix federation
# Allows other servers to discover our homeserver
timmy.foundation {
handle /.well-known/matrix/server {
header Content-Type application/json
respond `{"m.server": "matrix.timmy.foundation:443"}`
}
handle /.well-known/matrix/client {
header Content-Type application/json
header Access-Control-Allow-Origin *
respond `{"m.homeserver": {"base_url": "https://matrix.timmy.foundation"}}`
}
# Redirect root to Element Web or documentation
redir / https://matrix.timmy.foundation permanent
}

View File

@@ -1,37 +0,0 @@
[Unit]
Description=Conduit Matrix Homeserver
After=network.target
[Service]
Type=simple
User=conduit
Group=conduit
WorkingDirectory=/opt/conduit
ExecStart=/opt/conduit/conduit
# Restart on failure
Restart=on-failure
RestartSec=5
# Resource limits
LimitNOFILE=65536
# Security hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/conduit/data /opt/conduit/logs
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=true
LockPersonality=true
# Environment
Environment="RUST_LOG=info"
Environment="CONDUIT_CONFIG=/opt/conduit/conduit.toml"
[Install]
WantedBy=multi-user.target

View File

@@ -1,81 +0,0 @@
# Conduit Homeserver Configuration
# Location: /opt/conduit/conduit.toml
# Reference: docs/matrix-fleet-comms/README.md
[global]
# The server_name is the canonical name of your homeserver.
# It must match the domain in your MXIDs (e.g., @user:timmy.foundation)
server_name = "timmy.foundation"
# Database path - SQLite for simplicity, PostgreSQL available if needed
database_path = "/opt/conduit/data/conduit.db"
# Port to listen on
port = 8448
# Maximum request size (20MB for file uploads)
max_request_size = 20000000
# Allow guests to register (false = closed registration)
allow_registration = false
# Allow guests to join rooms without registering
allow_guest_registration = false
# Require authentication for profile requests
authenticate_profile_requests = true
[registration]
# Closed registration - admin creates accounts manually
enabled = false
[federation]
# Enable federation to communicate with other Matrix homeservers
enabled = true
# Servers to block from federation
# disabled_servers = ["bad.actor.com", "spammer.org"]
disabled_servers = []
# Enable server discovery via .well-known
well_known = true
[media]
# Maximum upload size per file (50MB)
max_file_size = 50000000
# Maximum total media cache size (100MB)
max_media_size = 100000000
# Directory for media storage
media_path = "/opt/conduit/data/media"
[retention]
# Enable message retention policies
enabled = true
# Default retention for rooms without explicit policy
default_room_retention = "30d"
# Minimum allowed retention period
min_retention = "1d"
# Maximum allowed retention period (null = no limit)
max_retention = null
[logging]
# Log level: error, warn, info, debug, trace
level = "info"
# Log to file
log_file = "/opt/conduit/logs/conduit.log"
[security]
# Require transaction IDs for idempotent requests
require_transaction_ids = true
# IP range blacklist for incoming federation
# ip_range_blacklist = ["10.0.0.0/8", "172.16.0.0/12"]
# Allow incoming federation from these IP ranges only (empty = allow all)
# ip_range_whitelist = []

View File

@@ -1,121 +0,0 @@
#!/bin/bash
# Conduit Matrix Homeserver Installation Script
# Location: Run this on target VPS after cloning timmy-config
# Reference: docs/matrix-fleet-comms/README.md
set -euo pipefail
# Configuration
CONDUIT_VERSION="0.8.0" # Check https://gitlab.com/famedly/conduit/-/releases
CONDUIT_DIR="/opt/conduit"
DATA_DIR="$CONDUIT_DIR/data"
LOGS_DIR="$CONDUIT_DIR/logs"
SCRIPTS_DIR="$CONDUIT_DIR/scripts"
CONDUIT_USER="conduit"
echo "========================================"
echo "Conduit Matrix Homeserver Installer"
echo "Target: $CONDUIT_DIR"
echo "Version: $CONDUIT_VERSION"
echo "========================================"
echo
# Check root
if [ "$EUID" -ne 0 ]; then
echo "Error: Please run as root"
exit 1
fi
# Create conduit user
echo "[1/8] Creating conduit user..."
if ! id "$CONDUIT_USER" &>/dev/null; then
useradd -r -s /bin/false -d "$CONDUIT_DIR" "$CONDUIT_USER"
echo " Created user: $CONDUIT_USER"
else
echo " User exists: $CONDUIT_USER"
fi
# Create directories
echo "[2/8] Creating directories..."
mkdir -p "$CONDUIT_DIR" "$DATA_DIR" "$LOGS_DIR" "$SCRIPTS_DIR"
chown -R "$CONDUIT_USER:$CONDUIT_USER" "$CONDUIT_DIR"
# Download Conduit
echo "[3/8] Downloading Conduit v${CONDUIT_VERSION}..."
ARCH=$(uname -m)
case "$ARCH" in
x86_64)
CONDUIT_ARCH="x86_64-unknown-linux-gnu"
;;
aarch64)
CONDUIT_ARCH="aarch64-unknown-linux-gnu"
;;
*)
echo "Error: Unsupported architecture: $ARCH"
exit 1
;;
esac
CONDUIT_URL="https://gitlab.com/famedly/conduit/-/releases/download/v${CONDUIT_VERSION}/conduit-${CONDUIT_ARCH}"
curl -L -o "$CONDUIT_DIR/conduit" "$CONDUIT_URL"
chmod +x "$CONDUIT_DIR/conduit"
chown "$CONDUIT_USER:$CONDUIT_USER" "$CONDUIT_DIR/conduit"
echo " Downloaded: $CONDUIT_DIR/conduit"
# Install configuration
echo "[4/8] Installing configuration..."
if [ -f "conduit.toml" ]; then
cp conduit.toml "$CONDUIT_DIR/conduit.toml"
chown "$CONDUIT_USER:$CONDUIT_USER" "$CONDUIT_DIR/conduit.toml"
echo " Installed: $CONDUIT_DIR/conduit.toml"
else
echo " Warning: conduit.toml not found in current directory"
fi
# Install systemd service
echo "[5/8] Installing systemd service..."
if [ -f "conduit.service" ]; then
cp conduit.service /etc/systemd/system/conduit.service
systemctl daemon-reload
echo " Installed: /etc/systemd/system/conduit.service"
else
echo " Warning: conduit.service not found in current directory"
fi
# Install scripts
echo "[6/8] Installing operational scripts..."
if [ -d "scripts" ]; then
cp scripts/*.sh "$SCRIPTS_DIR/"
chmod +x "$SCRIPTS_DIR"/*.sh
chown -R "$CONDUIT_USER:$CONDUIT_USER" "$SCRIPTS_DIR"
echo " Installed scripts to $SCRIPTS_DIR"
fi
# Create backup directory
echo "[7/8] Creating backup directory..."
mkdir -p /backups/conduit
chown "$CONDUIT_USER:$CONDUIT_USER" /backups/conduit
# Setup cron for backups
echo "[8/8] Setting up backup cron job..."
if [ -f "$SCRIPTS_DIR/backup.sh" ]; then
(crontab -l 2>/dev/null || true; echo "0 3 * * * $SCRIPTS_DIR/backup.sh >> $LOGS_DIR/backup.log 2>&1") | crontab -
echo " Backup cron job added (3 AM daily)"
fi
echo
echo "========================================"
echo "Installation Complete!"
echo "========================================"
echo
echo "Next steps:"
echo " 1. Configure DNS: matrix.timmy.foundation -> $(hostname -I | awk '{print $1}')"
echo " 2. Configure Caddy: cp Caddyfile /etc/caddy/conf.d/matrix.conf"
echo " 3. Start Conduit: systemctl start conduit"
echo " 4. Check health: $SCRIPTS_DIR/health.sh"
echo " 5. Create admin account (see README.md)"
echo
echo "Logs: $LOGS_DIR/"
echo "Data: $DATA_DIR/"
echo "Config: $CONDUIT_DIR/conduit.toml"

View File

@@ -1,82 +0,0 @@
#!/bin/bash
# Conduit Matrix Homeserver Backup Script
# Location: /opt/conduit/scripts/backup.sh
# Reference: docs/matrix-fleet-comms/README.md
# Run via cron: 0 3 * * * /opt/conduit/scripts/backup.sh
set -euo pipefail
# Configuration
BACKUP_BASE_DIR="/backups/conduit"
DATA_DIR="/opt/conduit/data"
CONFIG_FILE="/opt/conduit/conduit.toml"
RETENTION_DAYS=7
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="$BACKUP_BASE_DIR/$TIMESTAMP"
# Ensure backup directory exists
mkdir -p "$BACKUP_DIR"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
log "Starting Conduit backup..."
# Check if Conduit is running
if systemctl is-active --quiet conduit; then
log "Stopping Conduit for consistent backup..."
systemctl stop conduit
RESTART_NEEDED=true
else
log "Conduit already stopped"
RESTART_NEEDED=false
fi
# Backup database
if [ -f "$DATA_DIR/conduit.db" ]; then
log "Backing up database..."
cp "$DATA_DIR/conduit.db" "$BACKUP_DIR/"
sqlite3 "$BACKUP_DIR/conduit.db" "VACUUM;"
else
log "WARNING: Database not found at $DATA_DIR/conduit.db"
fi
# Backup configuration
if [ -f "$CONFIG_FILE" ]; then
log "Backing up configuration..."
cp "$CONFIG_FILE" "$BACKUP_DIR/"
fi
# Backup media (if exists)
if [ -d "$DATA_DIR/media" ]; then
log "Backing up media files..."
cp -r "$DATA_DIR/media" "$BACKUP_DIR/"
fi
# Restart Conduit if it was running
if [ "$RESTART_NEEDED" = true ]; then
log "Restarting Conduit..."
systemctl start conduit
fi
# Create compressed archive
log "Creating compressed archive..."
cd "$BACKUP_BASE_DIR"
tar czf "$TIMESTAMP.tar.gz" -C "$BACKUP_DIR" .
rm -rf "$BACKUP_DIR"
ARCHIVE_SIZE=$(du -h "$BACKUP_BASE_DIR/$TIMESTAMP.tar.gz" | cut -f1)
log "Backup complete: $TIMESTAMP.tar.gz ($ARCHIVE_SIZE)"
# Upload to S3 (uncomment and configure when ready)
# if command -v aws &> /dev/null; then
# log "Uploading to S3..."
# aws s3 cp "$BACKUP_BASE_DIR/$TIMESTAMP.tar.gz" s3://timmy-backups/conduit/
# fi
# Cleanup old backups
log "Cleaning up backups older than $RETENTION_DAYS days..."
find "$BACKUP_BASE_DIR" -name "*.tar.gz" -mtime +$RETENTION_DAYS -delete
log "Backup process complete"

View File

@@ -1,142 +0,0 @@
#!/bin/bash
# Conduit Matrix Homeserver Health Check
# Location: /opt/conduit/scripts/health.sh
# Reference: docs/matrix-fleet-comms/README.md
set -euo pipefail
HOMESERVER_URL="https://matrix.timmy.foundation"
ADMIN_EMAIL="admin@timmy.foundation"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() {
echo -e "${GREEN}[INFO]${NC} $*"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $*"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $*"
}
# Check if Conduit process is running
check_process() {
if systemctl is-active --quiet conduit; then
log_info "Conduit service is running"
return 0
else
log_error "Conduit service is not running"
return 1
fi
}
# Check Matrix client-server API
check_client_api() {
local response
response=$(curl -s -o /dev/null -w "%{http_code}" "$HOMESERVER_URL/_matrix/client/versions" 2>/dev/null || echo "000")
if [ "$response" = "200" ]; then
log_info "Client-server API is responding (HTTP 200)"
return 0
else
log_error "Client-server API returned HTTP $response"
return 1
fi
}
# Check Matrix versions endpoint
check_versions() {
local versions
versions=$(curl -s "$HOMESERVER_URL/_matrix/client/versions" 2>/dev/null | jq -r '.versions | join(", ")' 2>/dev/null || echo "unknown")
if [ "$versions" != "unknown" ]; then
log_info "Supported Matrix versions: $versions"
return 0
else
log_warn "Could not determine Matrix versions"
return 1
fi
}
# Check federation (self-test)
check_federation() {
local response
response=$(curl -s -o /dev/null -w "%{http_code}" "https://federationtester.matrix.org/api/report?server_name=timmy.foundation" 2>/dev/null || echo "000")
if [ "$response" = "200" ]; then
log_info "Federation tester can reach server"
return 0
else
log_warn "Federation tester returned HTTP $response (may be DNS propagation)"
return 1
fi
}
# Check disk space
check_disk_space() {
local usage
usage=$(df /opt/conduit/data | tail -1 | awk '{print $5}' | sed 's/%//')
if [ "$usage" -lt 80 ]; then
log_info "Disk usage: ${usage}% (healthy)"
return 0
elif [ "$usage" -lt 90 ]; then
log_warn "Disk usage: ${usage}% (consider cleanup)"
return 1
else
log_error "Disk usage: ${usage}% (critical!)"
return 1
fi
}
# Check database size
check_database() {
local db_path="/opt/conduit/data/conduit.db"
if [ -f "$db_path" ]; then
local size
size=$(du -h "$db_path" | cut -f1)
log_info "Database size: $size"
return 0
else
log_warn "Database file not found at $db_path"
return 1
fi
}
# Main health check
main() {
echo "========================================"
echo "Conduit Matrix Homeserver Health Check"
echo "Server: $HOMESERVER_URL"
echo "Time: $(date)"
echo "========================================"
echo
local exit_code=0
check_process || exit_code=1
check_client_api || exit_code=1
check_versions || true # Non-critical
check_federation || true # Non-critical during initial setup
check_disk_space || exit_code=1
check_database || true # Non-critical
echo
if [ $exit_code -eq 0 ]; then
log_info "All critical checks passed ✓"
else
log_error "Some critical checks failed ✗"
fi
return $exit_code
}
main "$@"

View File

@@ -1,21 +0,0 @@
# Gitea Accessibility Fix - R4: Time Elements
WCAG 1.3.1: Relative timestamps lack machine-readable fallbacks.
## Fix
Wrap relative timestamps in `<time datetime="...">` elements.
## Files
- `custom/templates/custom/time_relative.tmpl` - Reusable `<time>` helper
- `custom/templates/repo/list_a11y.tmpl` - Explore/Repos list override
## Deploy
```bash
cp -r custom/templates/* /path/to/gitea/custom/templates/
systemctl restart gitea
```
Closes #554

View File

@@ -1,27 +0,0 @@
{{/*
Gitea a11y fix: R4 <time> elements for relative timestamps
Deploy to: custom/templates/custom/time_relative.tmpl
*/}}
{{define "custom/time_relative"}}
{{if and .Time .Relative}}
<time datetime="{{.Time.Format "2006-01-02T15:04:05Z07:00"}}" title="{{.Time.Format "Jan 02, 2006 15:04"}}">
{{.Relative}}
</time>
{{else if .Relative}}
<span>{{.Relative}}</span>
{{end}}
{{end}}
{{define "custom/time_from_unix"}}
{{if .Relative}}
<time datetime="" data-unix="{{.Unix}}" title="">{{.Relative}}</time>
<script>
(function() {
var el = document.currentScript.previousElementSibling;
var unix = parseInt(el.getAttribute('data-unix'));
if (unix) { el.setAttribute('datetime', new Date(unix * 1000).toISOString()); el.setAttribute('title', new Date(unix * 1000).toLocaleString()); }
})();
</script>
{{end}}
{{end}}

View File

@@ -1,27 +0,0 @@
{{/*
Gitea a11y fix: R4 <time> elements for relative timestamps on repo list
Deploy to: custom/templates/repo/list_a11y.tmpl
*/}}
{{/* Star count link with aria-label */}}
<a class="repo-card-star" href="{{.RepoLink}}/stars" aria-label="{{.NumStars}} stars" title="{{.NumStars}} stars">
<svg class="octicon octicon-star" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
<path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/>
</svg>
<span>{{.NumStars}}</span>
</a>
{{/* Fork count link with aria-label */}}
<a class="repo-card-fork" href="{{.RepoLink}}/forks" aria-label="{{.NumForks}} forks" title="{{.NumForks}} forks">
<svg class="octicon octicon-repo-forked" viewBox="0 0 16 16" width="16" height="16" aria-hidden="true">
<path d="M5 5.372v.878c0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75v-.878a2.25 2.25 0 111.5 0v.878a2.25 2.25 0 01-2.25 2.25h-1.5v2.128a2.251 2.251 0 11-1.5 0V8.5h-1.5A2.25 2.25 0 013.5 6.25v-.878a2.25 2.25 0 111.5 0zM5 3.25a.75.75 0 10-1.5 0 .75.75 0 001.5 0zm6.75.75a.75.75 0 100-1.5.75.75 0 000 1.5zm-3 8.75a.75.75 0 10-1.5 0 .75.75 0 001.5 0z"/>
</svg>
<span>{{.NumForks}}</span>
</a>
{{/* Relative timestamp with <time> element for a11y */}}
{{if .UpdatedUnix}}
<time datetime="{{.UpdatedUnix | TimeSinceISO}}" title="{{.UpdatedUnix | DateFmtLong}}" class="text-light">
{{.UpdatedUnix | TimeSince}}
</time>
{{end}}

View File

@@ -1,30 +0,0 @@
matrix.example.com {
handle /.well-known/matrix/server {
header Content-Type application/json
respond `{"m.server": "matrix.example.com:443"}`
}
handle /.well-known/matrix/client {
header Content-Type application/json
respond `{"m.homeserver": {"base_url": "https://matrix.example.com"}}`
}
handle_path /_matrix/* {
reverse_proxy localhost:6167
}
handle {
reverse_proxy localhost:8080
}
log {
output file /var/log/caddy/matrix.log {
roll_size 10MB
roll_keep 10
}
}
}
matrix-federation.example.com:8448 {
reverse_proxy localhost:6167
}

View File

@@ -1,38 +0,0 @@
# Matrix/Conduit Host Prerequisites
## Target Host Specification
| Resource | Minimum | Fleet Scale |
|----------|---------|-------------|
| CPU | 2 cores | 4+ cores |
| RAM | 2 GB | 8 GB |
| Storage | 20 GB SSD | 100+ GB SSD |
## DNS Requirements
| Type | Host | Value |
|------|------|-------|
| A/AAAA | matrix.example.com | Server IP |
| SRV | _matrix._tcp | 10 5 8448 matrix.example.com |
## Ports
| Port | Purpose | Access |
|------|---------|--------|
| 443 | Client-Server API | Public |
| 8448 | Server-Server (federation) | Public |
| 6167 | Conduit internal | Localhost only |
## Software
```bash
curl -fsSL https://get.docker.com | sh
sudo apt install caddy
```
## Checklist
- [ ] Valid domain with DNS control
- [ ] Docker host with 4GB RAM
- [ ] Caddy reverse proxy configured
- [ ] Backup destination configured

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