Compare commits

...

43 Commits

Author SHA1 Message Date
24887b615f feat: add AST-backed Python ingestion to knowledge base
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 9s
PR Checklist / pr-checklist (pull_request) Failing after 1m18s
Smoke Test / smoke (pull_request) Failing after 6s
Validate Config / YAML Lint (pull_request) Failing after 6s
Validate Config / JSON Validate (pull_request) Successful in 7s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 6s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Shell Script Lint (pull_request) Failing after 14s
Validate Config / Cron Syntax Check (pull_request) Successful in 5s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 5s
Validate Config / Playbook Schema Validation (pull_request) Successful in 7s
Architecture Lint / Lint Repository (pull_request) Failing after 6s
2026-04-13 04:09:57 +00:00
1e43776be1 feat: add AST-backed Python ingestion to knowledge base 2026-04-13 04:09:54 +00:00
e53fdd0f49 feat: overnight R&D automation — Deep Dive + tightening + DPO export (#503)
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 8s
Smoke Test / smoke (push) Failing after 7s
Validate Config / YAML Lint (push) Failing after 6s
Validate Config / JSON Validate (push) Successful in 6s
Validate Config / Python Syntax & Import Check (push) Failing after 7s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 13s
Validate Config / Cron Syntax Check (push) Successful in 5s
Validate Config / Deploy Script Dry Run (push) Successful in 5s
Validate Config / Playbook Schema Validation (push) Successful in 7s
Architecture Lint / Lint Repository (push) Failing after 7s
2026-04-13 02:10:16 +00:00
aeefe5027d purge: remove Anthropic from timmy-config (14 files) (#502)
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 10s
Smoke Test / smoke (push) Failing after 8s
Validate Config / YAML Lint (push) Failing after 6s
Validate Config / JSON Validate (push) Successful in 6s
Validate Config / Python Syntax & Import Check (push) Failing after 7s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 14s
Validate Config / Cron Syntax Check (push) Successful in 5s
Validate Config / Deploy Script Dry Run (push) Successful in 4s
Validate Config / Playbook Schema Validation (push) Successful in 8s
Architecture Lint / Lint Repository (push) Failing after 8s
2026-04-13 02:02:06 +00:00
989bc29c96 Merge pull request 'feat: Anthropic ban enforcement scanner' (#501) from perplexity/anthropic-ban-scanner into main
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 9s
Smoke Test / smoke (push) Failing after 7s
Validate Config / YAML Lint (push) Failing after 8s
Validate Config / JSON Validate (push) Successful in 7s
Validate Config / Python Syntax & Import Check (push) Failing after 9s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 15s
Validate Config / Cron Syntax Check (push) Successful in 5s
Validate Config / Deploy Script Dry Run (push) Successful in 6s
Validate Config / Playbook Schema Validation (push) Successful in 9s
Architecture Lint / Lint Repository (push) Failing after 7s
2026-04-13 01:36:10 +00:00
d923b9e38a feat: add Anthropic ban enforcement scanner
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 10s
PR Checklist / pr-checklist (pull_request) Failing after 1m14s
Smoke Test / smoke (pull_request) Failing after 7s
Validate Config / YAML Lint (pull_request) Failing after 8s
Validate Config / JSON Validate (pull_request) Successful in 7s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 8s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Shell Script Lint (pull_request) Failing after 17s
Validate Config / Cron Syntax Check (pull_request) Successful in 6s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 5s
Validate Config / Playbook Schema Validation (pull_request) Successful in 8s
Architecture Lint / Lint Repository (pull_request) Failing after 7s
2026-04-13 01:34:35 +00:00
22c4bb57fe Merge pull request '[INFRA] Merge Conflict Detector — catch sibling PR collisions' (#500) from perplexity/conflict-detector into main
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 9s
Smoke Test / smoke (push) Failing after 7s
Validate Config / YAML Lint (push) Failing after 5s
Validate Config / JSON Validate (push) Successful in 6s
Validate Config / Python Syntax & Import Check (push) Failing after 7s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 14s
Validate Config / Cron Syntax Check (push) Successful in 5s
Validate Config / Playbook Schema Validation (push) Successful in 8s
Validate Config / Deploy Script Dry Run (push) Successful in 5s
Architecture Lint / Lint Repository (push) Failing after 7s
2026-04-13 00:26:38 +00:00
55fc678dc3 Add merge conflict detector for sibling PRs
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 10s
PR Checklist / pr-checklist (pull_request) Failing after 1m12s
Smoke Test / smoke (pull_request) Failing after 8s
Validate Config / YAML Lint (pull_request) Failing after 9s
Validate Config / JSON Validate (pull_request) Successful in 9s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 10s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Shell Script Lint (pull_request) Failing after 14s
Validate Config / Cron Syntax Check (pull_request) Successful in 5s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 6s
Validate Config / Playbook Schema Validation (pull_request) Successful in 9s
Architecture Lint / Lint Repository (pull_request) Failing after 7s
2026-04-13 00:26:28 +00:00
77a95d0ca1 Merge pull request '[Multimodal] Implement Visual Smoke Test for The Nexus (#490)' (#498) from feat/nexus-visual-smoke-test-v2 into main
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 11s
Smoke Test / smoke (push) Failing after 8s
Validate Config / YAML Lint (push) Failing after 7s
Validate Config / JSON Validate (push) Successful in 7s
Validate Config / Python Syntax & Import Check (push) Failing after 9s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 16s
Validate Config / Cron Syntax Check (push) Successful in 7s
Validate Config / Deploy Script Dry Run (push) Successful in 5s
Validate Config / Playbook Schema Validation (push) Successful in 10s
Architecture Lint / Lint Repository (push) Failing after 8s
2026-04-13 00:02:51 +00:00
9677785d8a Merge pull request 'fix(ci): Enforce lint failures + add pytest job (fixes #485)' (#488) from burn/20260412-0809-audit-fix into main
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 11s
Smoke Test / smoke (push) Failing after 7s
Validate Config / YAML Lint (push) Failing after 8s
Validate Config / JSON Validate (push) Successful in 8s
Validate Config / Python Syntax & Import Check (push) Failing after 11s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 17s
Validate Config / Cron Syntax Check (push) Successful in 6s
Validate Config / Deploy Script Dry Run (push) Successful in 6s
Validate Config / Playbook Schema Validation (push) Successful in 8s
Architecture Lint / Lint Repository (push) Has been cancelled
2026-04-13 00:00:57 +00:00
a5ac4cc675 Merge pull request 'fix: restore self-healing runtime checks' (#489) from timmy/issue-435-self-healing into main
Some checks failed
Architecture Lint / Lint Repository (push) Has been cancelled
Architecture Lint / Linter Tests (push) Has been cancelled
Smoke Test / smoke (push) Has been cancelled
Validate Config / JSON Validate (push) Has been cancelled
Validate Config / Python Syntax & Import Check (push) Has been cancelled
Validate Config / Shell Script Lint (push) Has been cancelled
Validate Config / Cron Syntax Check (push) Has been cancelled
Validate Config / Deploy Script Dry Run (push) Has been cancelled
Validate Config / Playbook Schema Validation (push) Has been cancelled
Validate Config / YAML Lint (push) Has started running
2026-04-13 00:00:49 +00:00
d801c5bc78 Merge pull request 'feat: add fleet dashboard script' (#497) from burn/20260412-1217-dashboard into main
Some checks failed
Architecture Lint / Linter Tests (push) Has been cancelled
Architecture Lint / Lint Repository (push) Has been cancelled
Smoke Test / smoke (push) Has been cancelled
Validate Config / YAML Lint (push) Has been cancelled
Validate Config / JSON Validate (push) Has been cancelled
Validate Config / Python Syntax & Import Check (push) Has been cancelled
Validate Config / Shell Script Lint (push) Has been cancelled
Validate Config / Cron Syntax Check (push) Has been cancelled
Validate Config / Deploy Script Dry Run (push) Has been cancelled
Validate Config / Playbook Schema Validation (push) Has been cancelled
2026-04-13 00:00:44 +00:00
90dbd8212c Merge pull request '[Multimodal] Sovereign Toolsuite Implementation (#491-#496)' (#499) from feat/multimodal-toolsuite into main
Some checks failed
Architecture Lint / Linter Tests (push) Has been cancelled
Architecture Lint / Lint Repository (push) Has been cancelled
Smoke Test / smoke (push) Has been cancelled
Validate Config / JSON Validate (push) Has been cancelled
Validate Config / Python Syntax & Import Check (push) Has been cancelled
Validate Config / Shell Script Lint (push) Has been cancelled
Validate Config / Cron Syntax Check (push) Has been cancelled
Validate Config / Deploy Script Dry Run (push) Has been cancelled
Validate Config / Playbook Schema Validation (push) Has been cancelled
Validate Config / YAML Lint (push) Has started running
2026-04-13 00:00:38 +00:00
a1d1359deb feat: implement scripts/captcha_bypass_handler.py (#491-496)
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 8s
PR Checklist / pr-checklist (pull_request) Successful in 1m17s
Smoke Test / smoke (pull_request) Failing after 7s
Validate Config / YAML Lint (pull_request) Failing after 7s
Validate Config / JSON Validate (pull_request) Successful in 7s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 9s
Validate Config / Shell Script Lint (pull_request) Successful in 15s
Validate Config / Cron Syntax Check (pull_request) Successful in 6s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 5s
Validate Config / Playbook Schema Validation (pull_request) Successful in 8s
Architecture Lint / Lint Repository (pull_request) Failing after 6s
2026-04-12 23:26:00 +00:00
a91d7e5f4f feat: implement scripts/visual_pr_reviewer.py (#491-496) 2026-04-12 23:25:58 +00:00
92415ce18c feat: implement scripts/tower_visual_mapper.py (#491-496) 2026-04-12 23:25:57 +00:00
3040938c46 feat: implement scripts/diagram_meaning_extractor.py (#491-496) 2026-04-12 23:25:56 +00:00
99af3526ce feat: implement scripts/foundation_accessibility_audit.py (#491-496) 2026-04-12 23:25:54 +00:00
af3ba9d594 feat: implement scripts/matrix_glitch_detect.py (#491-496) 2026-04-12 23:25:53 +00:00
7813871296 feat: implement visual smoke test for The Nexus (#490)
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 8s
PR Checklist / pr-checklist (pull_request) Successful in 1m20s
Smoke Test / smoke (pull_request) Failing after 6s
Validate Config / YAML Lint (pull_request) Failing after 6s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 8s
Validate Config / Cron Syntax Check (pull_request) Successful in 5s
Validate Config / JSON Validate (pull_request) Successful in 6s
Validate Config / Shell Script Lint (pull_request) Successful in 15s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 4s
Validate Config / Playbook Schema Validation (pull_request) Successful in 8s
Architecture Lint / Lint Repository (pull_request) Failing after 7s
2026-04-12 23:24:02 +00:00
de83f1fda8 testing write access
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 9s
Smoke Test / smoke (push) Failing after 6s
Validate Config / YAML Lint (push) Failing after 6s
Validate Config / JSON Validate (push) Successful in 5s
Validate Config / Python Syntax & Import Check (push) Failing after 7s
Validate Config / Shell Script Lint (push) Successful in 14s
Validate Config / Cron Syntax Check (push) Successful in 5s
Validate Config / Deploy Script Dry Run (push) Successful in 4s
Validate Config / Playbook Schema Validation (push) Successful in 7s
Architecture Lint / Lint Repository (push) Failing after 7s
2026-04-12 23:23:26 +00:00
Alexander Whitestone
6863d9c0c5 feat: add fleet dashboard script (scripts/fleet-dashboard.py)
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 9s
PR Checklist / pr-checklist (pull_request) Failing after 1m16s
Smoke Test / smoke (pull_request) Failing after 6s
Validate Config / YAML Lint (pull_request) Failing after 6s
Validate Config / JSON Validate (pull_request) Successful in 6s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 7s
Validate Config / Shell Script Lint (pull_request) Successful in 14s
Validate Config / Cron Syntax Check (pull_request) Successful in 5s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 5s
Validate Config / Playbook Schema Validation (pull_request) Successful in 8s
Architecture Lint / Lint Repository (pull_request) Failing after 8s
One-page terminal dashboard for the Timmy Foundation fleet:
- Gitea: open PRs, issues, recent merges per repo
- VPS health: SSH reachability, service status, disk usage for ezra/allegro/bezalel
- Cron jobs: schedule, state, last run status from cron/jobs.json

Usage: python3 scripts/fleet-dashboard.py
       python3 scripts/fleet-dashboard.py --json

Uses existing gitea_client.py patterns for Gitea API access.
No external dependencies -- stdlib only.
2026-04-12 12:19:35 -04:00
Alexander Whitestone
b49a0abf39 fix: restore self-healing runtime checks
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 9s
PR Checklist / pr-checklist (pull_request) Failing after 1m17s
Smoke Test / smoke (pull_request) Failing after 7s
Validate Config / YAML Lint (pull_request) Failing after 6s
Validate Config / JSON Validate (pull_request) Successful in 6s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 8s
Validate Config / Shell Script Lint (pull_request) Successful in 15s
Validate Config / Cron Syntax Check (pull_request) Successful in 5s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 5s
Validate Config / Playbook Schema Validation (pull_request) Successful in 8s
Architecture Lint / Lint Repository (pull_request) Failing after 8s
2026-04-12 10:53:55 -04:00
Alexander Whitestone
72de3eebdf fix(ci): enforce lint failures and add pytest job to validate-config
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 9s
PR Checklist / pr-checklist (pull_request) Successful in 1m21s
Smoke Test / smoke (pull_request) Failing after 7s
Validate Config / YAML Lint (pull_request) Failing after 6s
Validate Config / JSON Validate (pull_request) Successful in 6s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 6s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 8s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Shell Script Lint (pull_request) Failing after 16s
Validate Config / Cron Syntax Check (pull_request) Successful in 5s
Validate Config / Playbook Schema Validation (pull_request) Successful in 7s
Architecture Lint / Lint Repository (pull_request) Failing after 8s
Refs #485 - Expand Gitea CI/CD pipeline maturity

Changes:
- Remove '|| true' from shellcheck step so shell lint errors block merges
- Remove '|| true' from flake8 step so Python lint errors block merges
- Expand flake8 scope to include scripts/, bin/, tests/
- Exclude .git/ from shellcheck file discovery
- Add python-test job that runs pytest on the test suite after syntax check passes
2026-04-12 08:11:47 -04:00
f9388f6875 Merge pull request '[PURGE] Remove OpenClaw from stack — Hermes maxi directive' (#487) from purge/openclaw into main
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 8s
Smoke Test / smoke (push) Failing after 5s
Validate Config / YAML Lint (push) Failing after 6s
Validate Config / JSON Validate (push) Successful in 6s
Validate Config / Python Syntax & Import Check (push) Failing after 12s
Validate Config / Shell Script Lint (push) Successful in 13s
Validate Config / Cron Syntax Check (push) Successful in 4s
Validate Config / Deploy Script Dry Run (push) Successful in 4s
Validate Config / Playbook Schema Validation (push) Successful in 7s
Architecture Lint / Lint Repository (push) Failing after 7s
MUDA Weekly Waste Audit / muda-audit (push) Failing after 30s
2026-04-12 05:52:06 +00:00
09aa06d65f Remove OpenClaw from fleet health checks
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 8s
PR Checklist / pr-checklist (pull_request) Failing after 1m11s
Smoke Test / smoke (pull_request) Failing after 6s
Validate Config / YAML Lint (pull_request) Failing after 6s
Validate Config / JSON Validate (pull_request) Successful in 6s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 7s
Validate Config / Shell Script Lint (pull_request) Successful in 14s
Validate Config / Cron Syntax Check (pull_request) Successful in 7s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 9s
Validate Config / Playbook Schema Validation (pull_request) Successful in 8s
Architecture Lint / Lint Repository (pull_request) Failing after 7s
2026-04-12 05:51:45 +00:00
8dc8bc4774 Replace OpenClaw with generic single-agent framing in Son of Timmy 2026-04-12 05:51:43 +00:00
fcf112cb1e Remove OpenClaw gateway from fleet topology 2026-04-12 05:51:41 +00:00
ce36d3813b Remove OpenClaw from fleet capacity inventory 2026-04-12 05:51:40 +00:00
d4876c0fa5 Remove OpenClaw gateway from automation inventory 2026-04-12 05:51:38 +00:00
8070536d57 Remove OpenClaw references from Allegro wizard house doc 2026-04-12 05:51:37 +00:00
438191c72e Remove OpenClaw reference from Code Claw delegation doc 2026-04-12 05:51:36 +00:00
21e4039ec9 Merge pull request 'feat(scripts): enforce verified SSH trust for Gemini suite (#434)' (#474) from timmy/issue-434-ssh-trust into main
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 8s
Smoke Test / smoke (push) Failing after 7s
Validate Config / YAML Lint (push) Failing after 6s
Validate Config / JSON Validate (push) Successful in 6s
Validate Config / Python Syntax & Import Check (push) Failing after 8s
Validate Config / Shell Script Lint (push) Successful in 15s
Validate Config / Cron Syntax Check (push) Successful in 4s
Validate Config / Deploy Script Dry Run (push) Successful in 5s
Validate Config / Playbook Schema Validation (push) Successful in 8s
Architecture Lint / Lint Repository (push) Failing after 7s
2026-04-11 20:23:26 +00:00
Alexander Whitestone
19aa0830f4 Harden Gemini scripts with verified SSH trust
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 8s
Validate Config / YAML Lint (pull_request) Failing after 5s
Validate Config / JSON Validate (pull_request) Successful in 6s
PR Checklist / pr-checklist (pull_request) Failing after 1m11s
Smoke Test / smoke (pull_request) Failing after 7s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 9s
Validate Config / Shell Script Lint (pull_request) Successful in 15s
Validate Config / Cron Syntax Check (pull_request) Successful in 5s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 5s
Validate Config / Playbook Schema Validation (pull_request) Successful in 7s
Architecture Lint / Lint Repository (pull_request) Failing after 6s
2026-04-11 15:13:15 -04:00
f2edb6a9b3 merge: feat(scripts): add GOFAI symbolic forward-chaining rule engine (#470)
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 8s
Smoke Test / smoke (push) Failing after 6s
Validate Config / YAML Lint (push) Failing after 6s
Validate Config / JSON Validate (push) Successful in 6s
Validate Config / Python Syntax & Import Check (push) Failing after 8s
Validate Config / Shell Script Lint (push) Successful in 13s
Validate Config / Cron Syntax Check (push) Successful in 6s
Validate Config / Deploy Script Dry Run (push) Successful in 7s
Validate Config / Playbook Schema Validation (push) Successful in 9s
Architecture Lint / Lint Repository (push) Failing after 7s
Auto-merged by PR triage bot
2026-04-11 02:08:02 +00:00
fc817c6a84 merge: feat(scripts): add GOFAI symbolic knowledge base (#471)
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 9s
Architecture Lint / Lint Repository (push) Has been cancelled
Smoke Test / smoke (push) Has been cancelled
Validate Config / YAML Lint (push) Has been cancelled
Validate Config / JSON Validate (push) Has been cancelled
Validate Config / Python Syntax & Import Check (push) Has been cancelled
Validate Config / Shell Script Lint (push) Has been cancelled
Validate Config / Cron Syntax Check (push) Has been cancelled
Validate Config / Deploy Script Dry Run (push) Has been cancelled
Validate Config / Playbook Schema Validation (push) Has been cancelled
Auto-merged by PR triage bot
2026-04-11 02:07:46 +00:00
a620bd19b3 merge: feat(scripts): add GOFAI STRIPS goal-directed planner (#472)
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 10s
Smoke Test / smoke (push) Failing after 6s
Architecture Lint / Lint Repository (push) Has been cancelled
Validate Config / JSON Validate (push) Has been cancelled
Validate Config / YAML Lint (push) Has been cancelled
Validate Config / Python Syntax & Import Check (push) Has been cancelled
Validate Config / Shell Script Lint (push) Has been cancelled
Validate Config / Cron Syntax Check (push) Has been cancelled
Validate Config / Deploy Script Dry Run (push) Has been cancelled
Validate Config / Playbook Schema Validation (push) Has been cancelled
Auto-merged by PR triage bot
2026-04-11 02:07:15 +00:00
0c98bce77f merge: feat(scripts): add GOFAI temporal reasoning engine (#473)
Some checks failed
Architecture Lint / Lint Repository (push) Has been cancelled
Architecture Lint / Linter Tests (push) Has been cancelled
Smoke Test / smoke (push) Has been cancelled
Validate Config / YAML Lint (push) Has been cancelled
Validate Config / JSON Validate (push) Has been cancelled
Validate Config / Python Syntax & Import Check (push) Has been cancelled
Validate Config / Shell Script Lint (push) Has been cancelled
Validate Config / Cron Syntax Check (push) Has been cancelled
Validate Config / Deploy Script Dry Run (push) Has been cancelled
Validate Config / Playbook Schema Validation (push) Has been cancelled
Auto-merged by PR triage bot
2026-04-11 02:07:04 +00:00
c01e7f7d7f merge: test(scripts): lock self_healing safe CLI behavior (#469)
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 11s
Architecture Lint / Lint Repository (push) Has been cancelled
Smoke Test / smoke (push) Failing after 10s
Validate Config / YAML Lint (push) Has been cancelled
Validate Config / JSON Validate (push) Has been cancelled
Validate Config / Python Syntax & Import Check (push) Has been cancelled
Validate Config / Shell Script Lint (push) Has been cancelled
Validate Config / Cron Syntax Check (push) Has been cancelled
Validate Config / Deploy Script Dry Run (push) Has been cancelled
Validate Config / Playbook Schema Validation (push) Has been cancelled
Auto-merged by PR triage bot
2026-04-11 02:06:42 +00:00
20bc0aa41a feat(scripts): add GOFAI temporal reasoning engine
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 8s
PR Checklist / pr-checklist (pull_request) Failing after 1m12s
Smoke Test / smoke (pull_request) Failing after 7s
Validate Config / YAML Lint (pull_request) Failing after 6s
Validate Config / JSON Validate (pull_request) Successful in 5s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 7s
Validate Config / Shell Script Lint (pull_request) Successful in 14s
Validate Config / Cron Syntax Check (pull_request) Successful in 4s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 4s
Validate Config / Playbook Schema Validation (pull_request) Successful in 7s
Architecture Lint / Lint Repository (pull_request) Failing after 6s
2026-04-11 01:40:24 +00:00
b6c0620c83 feat(scripts): add GOFAI STRIPS goal-directed planner
Some checks failed
Validate Config / Deploy Script Dry Run (pull_request) Successful in 5s
Validate Config / Playbook Schema Validation (pull_request) Successful in 8s
Architecture Lint / Lint Repository (pull_request) Failing after 6s
Architecture Lint / Linter Tests (pull_request) Successful in 7s
PR Checklist / pr-checklist (pull_request) Failing after 1m9s
Smoke Test / smoke (pull_request) Failing after 7s
Validate Config / YAML Lint (pull_request) Failing after 6s
Validate Config / JSON Validate (pull_request) Successful in 6s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 8s
Validate Config / Shell Script Lint (pull_request) Successful in 14s
Validate Config / Cron Syntax Check (pull_request) Successful in 5s
2026-04-11 01:36:03 +00:00
17de7f5df1 feat(scripts): add symbolic forward-chaining rule engine
Some checks failed
Validate Config / Playbook Schema Validation (pull_request) Successful in 7s
Architecture Lint / Linter Tests (pull_request) Successful in 10s
Architecture Lint / Lint Repository (pull_request) Failing after 7s
PR Checklist / pr-checklist (pull_request) Failing after 1m15s
Validate Config / JSON Validate (pull_request) Successful in 5s
Smoke Test / smoke (pull_request) Failing after 6s
Validate Config / YAML Lint (pull_request) Failing after 5s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 6s
Validate Config / Shell Script Lint (pull_request) Successful in 14s
Validate Config / Cron Syntax Check (pull_request) Successful in 6s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 6s
2026-04-11 01:25:34 +00:00
Alexander Whitestone
06031d923f test(scripts): lock self_healing safe CLI behavior (#435)
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 8s
PR Checklist / pr-checklist (pull_request) Failing after 1m18s
Architecture Lint / Lint Repository (pull_request) Failing after 8s
2026-04-10 21:11:47 -04:00
49 changed files with 2742 additions and 306 deletions

View File

@@ -59,7 +59,21 @@ jobs:
- name: Flake8 critical errors only
run: |
flake8 --select=E9,F63,F7,F82 --show-source --statistics \
scripts/ allegro/ cron/ || true
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
@@ -70,7 +84,7 @@ jobs:
run: sudo apt-get install -y shellcheck
- name: Lint shell scripts
run: |
find . -name '*.sh' -print0 | xargs -0 -r shellcheck --severity=error || true
find . -name '*.sh' -not -path './.git/*' -print0 | xargs -0 -r shellcheck --severity=error
cron-validate:
name: Cron Syntax Check

View File

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

120
bin/conflict_detector.py Normal file
View File

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

@@ -7,7 +7,7 @@
7|common sense fallbacks automatically.
8|
9|Fallback chain:
10|1. Primary model (Anthropic) down -> switch config to local-llama.cpp
10|1. Primary model (Kimi) down -> switch config to local-llama.cpp
11|2. Gitea unreachable -> cache issues locally, retry on recovery
12|3. VPS agents down -> alert + lazarus protocol
13|4. Local llama.cpp down -> try Ollama, then alert-only mode
@@ -61,16 +61,16 @@
61|
62|# ─── HEALTH CHECKS ───
63|
64|def check_anthropic():
65| """Can we reach Anthropic API?"""
66| key = os.environ.get("ANTHROPIC_API_KEY", "")
64|def check_kimi():
65| """Can we reach Kimi Coding API?"""
66| key = os.environ.get("KIMI_API_KEY", "")
67| if not key:
68| # Check multiple .env locations
69| for env_path in [HERMES_HOME / ".env", Path.home() / ".hermes" / ".env"]:
70| if env_path.exists():
71| for line in open(env_path):
72| line = line.strip()
73| if line.startswith("ANTHROPIC_API_KEY=***
73| if line.startswith("KIMI_API_KEY=***
74| key = line.split("=", 1)[1].strip().strip('"').strip("'")
75| break
76| if key:
@@ -79,10 +79,10 @@
79| return False, "no API key"
80| code, out, err = run(
81| f'curl -s -o /dev/null -w "%{{http_code}}" -H "x-api-key: {key}" '
82| f'-H "anthropic-version: 2023-06-01" '
83| f'https://api.anthropic.com/v1/messages -X POST '
82| f'-H "x-api-provider: kimi-coding" '
83| f'https://api.kimi.com/coding/v1/models -X POST '
84| f'-H "content-type: application/json" '
85| f'-d \'{{"model":"claude-haiku-4-5-20251001","max_tokens":1,"messages":[{{"role":"user","content":"ping"}}]}}\' ',
85| f'-d \'{{"model":"kimi-k2.5","max_tokens":1,"messages":[{{"role":"user","content":"ping"}}]}}\' ',
86| timeout=15
87| )
88| if code == 0 and out in ("200", "429"):
@@ -128,7 +128,7 @@
128|# ─── FALLBACK ACTIONS ───
129|
130|def fallback_to_local_model(cfg):
131| """Switch primary model from Anthropic to local llama.cpp"""
131| """Switch primary model from Kimi to local llama.cpp"""
132| if not BACKUP_CONFIG.exists():
133| shutil.copy2(CONFIG_PATH, BACKUP_CONFIG)
134|
@@ -176,8 +176,8 @@
176| }
177|
178| # Check all systems
179| anthropic_ok, anthropic_msg = check_anthropic()
180| results["checks"]["anthropic"] = {"ok": anthropic_ok, "msg": anthropic_msg}
179| kimi_ok, kimi_msg = check_kimi()
180| results["checks"]["kimi-coding"] = {"ok": kimi_ok, "msg": kimi_msg}
181|
182| llama_ok, llama_msg = check_local_llama()
183| results["checks"]["local_llama"] = {"ok": llama_ok, "msg": llama_msg}
@@ -198,21 +198,21 @@
198| vps_ok, vps_msg = check_vps(ip, name)
199| results["checks"][f"vps_{name.lower()}"] = {"ok": vps_ok, "msg": vps_msg}
200|
201| current_provider = cfg.get("model", {}).get("provider", "anthropic")
201| current_provider = cfg.get("model", {}).get("provider", "kimi-coding")
202|
203| # ─── FALLBACK LOGIC ───
204|
205| # Case 1: Primary (Anthropic) down, local available
206| if not anthropic_ok and current_provider == "anthropic":
205| # Case 1: Primary (Kimi) down, local available
206| if not kimi_ok and current_provider == "kimi-coding":
207| if llama_ok:
208| msg = fallback_to_local_model(cfg)
209| results["actions"].append(msg)
210| state["active_fallbacks"].append("anthropic->local-llama")
210| state["active_fallbacks"].append("kimi->local-llama")
211| results["status"] = "degraded_local"
212| elif ollama_ok:
213| msg = fallback_to_ollama(cfg)
214| results["actions"].append(msg)
215| state["active_fallbacks"].append("anthropic->ollama")
215| state["active_fallbacks"].append("kimi->ollama")
216| results["status"] = "degraded_ollama"
217| else:
218| msg = enter_safe_mode(state)
@@ -220,15 +220,15 @@
220| results["status"] = "safe_mode"
221|
222| # Case 2: Already on fallback, check if primary recovered
223| elif anthropic_ok and "anthropic->local-llama" in state.get("active_fallbacks", []):
223| elif kimi_ok and "kimi->local-llama" in state.get("active_fallbacks", []):
224| msg = restore_config()
225| results["actions"].append(msg)
226| state["active_fallbacks"].remove("anthropic->local-llama")
226| state["active_fallbacks"].remove("kimi->local-llama")
227| results["status"] = "recovered"
228| elif anthropic_ok and "anthropic->ollama" in state.get("active_fallbacks", []):
228| elif kimi_ok and "kimi->ollama" in state.get("active_fallbacks", []):
229| msg = restore_config()
230| results["actions"].append(msg)
231| state["active_fallbacks"].remove("anthropic->ollama")
231| state["active_fallbacks"].remove("kimi->ollama")
232| results["status"] = "recovered"
233|
234| # Case 3: Gitea down — just flag it, work locally

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

390
scripts/fleet-dashboard.py Executable file
View File

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

View File

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

View File

@@ -0,0 +1,12 @@
import json
from hermes_tools import browser_navigate, browser_vision
def audit_accessibility():
browser_navigate(url="https://timmyfoundation.org")
analysis = browser_vision(
question="Perform an accessibility audit. Check for: 1) Color contrast, 2) Font legibility, 3) Missing alt text for images. Provide a report with FAIL/PASS."
)
return {"status": "PASS" if "PASS" in analysis.upper() else "FAIL", "analysis": analysis}
if __name__ == '__main__':
print(json.dumps(audit_accessibility(), indent=2))

View File

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

View File

@@ -0,0 +1,12 @@
import json
from hermes_tools import browser_navigate, browser_vision
def detect_glitches():
browser_navigate(url="https://matrix.alexanderwhitestone.com")
analysis = browser_vision(
question="Scan the 3D world for visual artifacts, floating assets, or z-fighting. List all coordinates/descriptions of glitches found. Provide a PASS/FAIL."
)
return {"status": "PASS" if "PASS" in analysis.upper() else "FAIL", "analysis": analysis}
if __name__ == '__main__':
print(json.dumps(detect_glitches(), indent=2))

View File

@@ -0,0 +1,20 @@
import json
from hermes_tools import browser_navigate, browser_vision
def run_smoke_test():
print("Navigating to The Nexus...")
browser_navigate(url="https://nexus.alexanderwhitestone.com")
print("Performing visual verification...")
analysis = browser_vision(
question="Is the Nexus landing page rendered correctly? Check for: 1) The Tower logo, 2) The main entry portal, 3) Absence of 404/Error messages. Provide a clear PASS or FAIL."
)
result = {
"status": "PASS" if "PASS" in analysis.upper() else "FAIL",
"analysis": analysis
}
return result
if __name__ == '__main__':
print(json.dumps(run_smoke_test(), indent=2))

View File

@@ -15,10 +15,15 @@ import sys
import time
import argparse
import requests
import subprocess
import json
from typing import Optional, Dict, Any
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPT_DIR not in sys.path:
sys.path.insert(0, SCRIPT_DIR)
from ssh_trust import VerifiedSSHExecutor
# --- CONFIGURATION ---
DO_API_URL = "https://api.digitalocean.com/v2"
# We expect DIGITALOCEAN_TOKEN to be set in the environment.
@@ -30,13 +35,14 @@ DEFAULT_IMAGE = "ubuntu-22-04-x64"
LLAMA_CPP_REPO = "https://github.com/ggerganov/llama.cpp"
class Provisioner:
def __init__(self, name: str, size: str, model: str, region: str = DEFAULT_REGION):
def __init__(self, name: str, size: str, model: str, region: str = DEFAULT_REGION, executor=None):
self.name = name
self.size = size
self.model = model
self.region = region
self.droplet_id = None
self.ip_address = None
self.executor = executor or VerifiedSSHExecutor(auto_enroll=True)
def log(self, message: str):
print(f"[*] {message}")
@@ -104,13 +110,8 @@ class Provisioner:
self.log(f"Droplet IP: {self.ip_address}")
def run_remote(self, command: str):
# Using subprocess to call ssh. Assumes local machine has the right private key.
ssh_cmd = [
"ssh", "-o", "StrictHostKeyChecking=no",
f"root@{self.ip_address}", command
]
result = subprocess.run(ssh_cmd, capture_output=True, text=True)
return result
# Uses verified host trust. Brand-new nodes explicitly enroll on first contact.
return self.executor.run_script(self.ip_address, command, timeout=60)
def setup_wizard(self):
self.log("Starting remote setup...")

View File

@@ -10,10 +10,16 @@ Safe-by-default: runs in dry-run mode unless --execute is given.
import os
import sys
import subprocess
import argparse
import requests
import datetime
from typing import Sequence
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPT_DIR not in sys.path:
sys.path.insert(0, SCRIPT_DIR)
from ssh_trust import VerifiedSSHExecutor
# --- CONFIGURATION ---
FLEET = {
@@ -24,22 +30,20 @@ FLEET = {
}
class SelfHealer:
def __init__(self, dry_run=True, confirm_kill=False, yes=False):
def __init__(self, dry_run=True, confirm_kill=False, yes=False, executor=None):
self.dry_run = dry_run
self.confirm_kill = confirm_kill
self.yes = yes
self.executor = executor or VerifiedSSHExecutor()
def log(self, message: str):
timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
print(f"[{timestamp}] {message}")
def run_remote(self, host: str, command: str):
ip = FLEET[host]["ip"]
ssh_cmd = ["ssh", "-o", "StrictHostKeyChecking=no", "-o", "ConnectTimeout=5", f"root@{ip}", command]
if host == "mac":
ssh_cmd = ["bash", "-c", command]
ip = FLEET[host]['ip']
try:
return subprocess.run(ssh_cmd, capture_output=True, text=True, timeout=15)
return self.executor.run_script(ip, command, local=(host == 'mac'), timeout=15)
except Exception as e:
self.log(f" [ERROR] Failed to run remote command on {host}: {e}")
return None
@@ -52,7 +56,7 @@ class SelfHealer:
response = input(f"{prompt} [y/N] ").strip().lower()
if response in ("y", "yes"):
return True
elif response in ("n", "no", ""):
if response in ("n", "no", ""):
return False
print("Please answer 'y' or 'n'.")
@@ -61,7 +65,7 @@ class SelfHealer:
port = FLEET[host]["port"]
try:
requests.get(f"http://{ip}:{port}/health", timeout=2)
except:
except requests.RequestException:
self.log(f" [!] llama-server down on {host}.")
if self.dry_run:
self.log(f" [DRY-RUN] Would restart llama-server on {host}")
@@ -192,10 +196,10 @@ EXAMPLES:
"""
print(help_text)
def main():
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
description="Self-healing infrastructure script (safe-by-default).",
add_help=False # We'll handle --help ourselves
add_help=False,
)
parser.add_argument("--dry-run", action="store_true", default=False,
help="Run in dry-run mode (default behavior).")
@@ -209,25 +213,28 @@ def main():
help="Show detailed help about safety features.")
parser.add_argument("--help", "-h", action="store_true", default=False,
help="Show standard help.")
return parser
args = parser.parse_args()
def main(argv: Sequence[str] | None = None):
parser = build_parser()
args = parser.parse_args(list(argv) if argv is not None else None)
if args.help_safe:
print_help_safe()
sys.exit(0)
raise SystemExit(0)
if args.help:
parser.print_help()
sys.exit(0)
raise SystemExit(0)
# Determine mode: if --execute is given, disable dry-run
dry_run = not args.execute
# If --dry-run is explicitly given, ensure dry-run (redundant but clear)
if args.dry_run:
dry_run = True
healer = SelfHealer(dry_run=dry_run, confirm_kill=args.confirm_kill, yes=args.yes)
healer.run()
if __name__ == "__main__":
main()

171
scripts/ssh_trust.py Normal file
View File

@@ -0,0 +1,171 @@
#!/usr/bin/env python3
"""Verified SSH trust helpers for Gemini infrastructure scripts."""
from __future__ import annotations
from pathlib import Path
from typing import Callable, Sequence
import shlex
import subprocess
DEFAULT_KNOWN_HOSTS = Path(__file__).resolve().parent.parent / ".ssh" / "known_hosts"
Runner = Callable[..., subprocess.CompletedProcess]
class SSHTrustError(RuntimeError):
pass
class HostKeyEnrollmentError(SSHTrustError):
pass
class HostKeyVerificationError(SSHTrustError):
pass
class CommandPlan:
def __init__(self, argv: list[str], local: bool, remote_command: str | None = None):
self.argv = argv
self.local = local
self.remote_command = remote_command
def _ensure_parent(path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
def enroll_host_key(
host: str,
*,
port: int = 22,
known_hosts_path: str | Path | None = None,
runner: Runner = subprocess.run,
) -> Path:
path = Path(known_hosts_path or DEFAULT_KNOWN_HOSTS)
_ensure_parent(path)
cmd = ["ssh-keyscan", "-p", str(port), "-H", host]
result = runner(cmd, capture_output=True, text=True, timeout=10)
if result.returncode != 0 or not (result.stdout or "").strip():
raise HostKeyEnrollmentError(
f"Could not enroll host key for {host}:{port}: {(result.stderr or '').strip() or 'empty ssh-keyscan output'}"
)
existing = []
if path.exists():
existing = [line for line in path.read_text().splitlines() if line.strip()]
for line in result.stdout.splitlines():
line = line.strip()
if line and line not in existing:
existing.append(line)
path.write_text(("\n".join(existing) + "\n") if existing else "")
return path
class VerifiedSSHExecutor:
def __init__(
self,
*,
user: str = "root",
known_hosts_path: str | Path | None = None,
connect_timeout: int = 5,
auto_enroll: bool = False,
runner: Runner = subprocess.run,
):
self.user = user
self.known_hosts_path = Path(known_hosts_path or DEFAULT_KNOWN_HOSTS)
self.connect_timeout = connect_timeout
self.auto_enroll = auto_enroll
self.runner = runner
def _ensure_known_hosts(self, host: str, port: int) -> Path:
if self.known_hosts_path.exists():
return self.known_hosts_path
if not self.auto_enroll:
raise HostKeyEnrollmentError(
f"Known-hosts file missing: {self.known_hosts_path}. Enroll {host}:{port} before connecting."
)
return enroll_host_key(host, port=port, known_hosts_path=self.known_hosts_path, runner=self.runner)
def _ssh_prefix(self, host: str, port: int) -> list[str]:
known_hosts = self._ensure_known_hosts(host, port)
return [
"ssh",
"-o",
"BatchMode=yes",
"-o",
"StrictHostKeyChecking=yes",
"-o",
f"UserKnownHostsFile={known_hosts}",
"-o",
f"ConnectTimeout={self.connect_timeout}",
"-p",
str(port),
f"{self.user}@{host}",
]
def plan(
self,
host: str,
command: Sequence[str],
*,
local: bool = False,
port: int = 22,
cwd: str | None = None,
) -> CommandPlan:
argv = [str(part) for part in command]
if not argv:
raise ValueError("command must not be empty")
if local:
return CommandPlan(argv=argv, local=True, remote_command=None)
remote_command = shlex.join(argv)
if cwd:
remote_command = f"cd {shlex.quote(cwd)} && exec {remote_command}"
return CommandPlan(self._ssh_prefix(host, port) + [remote_command], False, remote_command)
def plan_script(
self,
host: str,
script_text: str,
*,
local: bool = False,
port: int = 22,
cwd: str | None = None,
) -> CommandPlan:
remote_command = script_text.strip()
if cwd:
remote_command = f"cd {shlex.quote(cwd)} && {remote_command}"
if local:
return CommandPlan(["sh", "-lc", remote_command], True, None)
return CommandPlan(self._ssh_prefix(host, port) + [remote_command], False, remote_command)
def _run_plan(self, plan: CommandPlan, *, timeout: int | None = None):
result = self.runner(plan.argv, capture_output=True, text=True, timeout=timeout)
if result.returncode != 0 and "host key verification failed" in ((result.stderr or "").lower()):
raise HostKeyVerificationError((result.stderr or "").strip() or "Host key verification failed")
return result
def run(
self,
host: str,
command: Sequence[str],
*,
local: bool = False,
port: int = 22,
cwd: str | None = None,
timeout: int | None = None,
):
return self._run_plan(self.plan(host, command, local=local, port=port, cwd=cwd), timeout=timeout)
def run_script(
self,
host: str,
script_text: str,
*,
local: bool = False,
port: int = 22,
cwd: str | None = None,
timeout: int | None = None,
):
return self._run_plan(self.plan_script(host, script_text, local=local, port=port, cwd=cwd), timeout=timeout)

304
scripts/strips_planner.py Normal file
View File

@@ -0,0 +1,304 @@
#!/usr/bin/env python3
"""strips_planner.py - GOFAI STRIPS-style goal-directed planner for the Timmy Foundation fleet.
Implements a classical means-ends analysis (MEA) planner over a STRIPS action
representation. Each action has preconditions, an add-list, and a delete-list.
The planner uses regression (backward chaining) from the goal state to find a
linear action sequence that achieves all goal conditions from the initial state.
No ML, no embeddings - just symbolic state-space search.
Representation:
State: frozenset of ground literals, e.g. {'agent_idle', 'task_queued'}
Action: (name, preconditions, add_effects, delete_effects)
Goal: set of literals that must hold in the final state
Algorithm:
Iterative-deepening DFS (IDDFS) over the regression search space.
Cycle detection via visited-state set per path.
Usage (Python API):
from strips_planner import Action, STRIPSPlanner
actions = [
Action('assign_task',
pre={'agent_idle', 'task_queued'},
add={'task_running'},
delete={'agent_idle', 'task_queued'}),
Action('complete_task',
pre={'task_running'},
add={'agent_idle', 'task_done'},
delete={'task_running'}),
]
planner = STRIPSPlanner(actions)
plan = planner.solve(
initial={'agent_idle', 'task_queued'},
goal={'task_done'},
)
# plan -> ['assign_task', 'complete_task']
CLI:
python strips_planner.py --demo
python strips_planner.py --max-depth 15
"""
from __future__ import annotations
import argparse
import sys
from dataclasses import dataclass, field
from typing import FrozenSet, List, Optional, Set, Tuple
# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------
Literal = str
State = FrozenSet[Literal]
@dataclass(frozen=True)
class Action:
"""A STRIPS operator with preconditions and add/delete effects."""
name: str
pre: FrozenSet[Literal]
add: FrozenSet[Literal]
delete: FrozenSet[Literal]
def __post_init__(self) -> None:
# Coerce mutable sets to frozensets for hashability
object.__setattr__(self, 'pre', frozenset(self.pre))
object.__setattr__(self, 'add', frozenset(self.add))
object.__setattr__(self, 'delete', frozenset(self.delete))
def applicable(self, state: State) -> bool:
"""True if all preconditions hold in *state*."""
return self.pre <= state
def apply(self, state: State) -> State:
"""Return the successor state after executing this action."""
return (state - self.delete) | self.add
def __str__(self) -> str:
return self.name
# ---------------------------------------------------------------------------
# Planner
# ---------------------------------------------------------------------------
class STRIPSPlanner:
"""Goal-directed STRIPS planner using iterative-deepening DFS.
Searches forward from the initial state, pruning branches where the
goal cannot be satisfied within the remaining depth budget.
"""
def __init__(self, actions: List[Action]) -> None:
self.actions = list(actions)
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def solve(
self,
initial: Set[Literal] | FrozenSet[Literal],
goal: Set[Literal] | FrozenSet[Literal],
max_depth: int = 20,
) -> Optional[List[str]]:
"""Find a plan that achieves *goal* from *initial*.
Args:
initial: Initial world state (set of ground literals).
goal: Goal conditions (set of ground literals to achieve).
max_depth: Maximum plan length to consider.
Returns:
Ordered list of action names, or None if no plan found.
"""
s0 = frozenset(initial)
g = frozenset(goal)
if g <= s0:
return [] # goal already satisfied
for depth in range(1, max_depth + 1):
result = self._dfs(s0, g, depth, [], {s0})
if result is not None:
return result
return None
# ------------------------------------------------------------------
# Internal search
# ------------------------------------------------------------------
def _dfs(
self,
state: State,
goal: State,
remaining: int,
path: List[str],
visited: Set[State],
) -> Optional[List[str]]:
"""Depth-limited forward DFS."""
if remaining == 0:
return None
for action in self.actions:
if not action.applicable(state):
continue
next_state = action.apply(state)
if next_state in visited:
continue
new_path = path + [action.name]
if goal <= next_state:
return new_path
visited.add(next_state)
result = self._dfs(next_state, goal, remaining - 1, new_path, visited)
visited.discard(next_state)
if result is not None:
return result
return None
# ------------------------------------------------------------------
# Helpers
# ------------------------------------------------------------------
def explain_plan(
self, initial: Set[Literal], plan: List[str]
) -> List[Tuple[str, State]]:
"""Trace each action with the resulting state for debugging.
Returns:
List of (action_name, resulting_state) tuples.
"""
state: State = frozenset(initial)
trace: List[Tuple[str, State]] = []
action_map = {a.name: a for a in self.actions}
for name in plan:
action = action_map[name]
state = action.apply(state)
trace.append((name, state))
return trace
# ---------------------------------------------------------------------------
# Built-in demo domain: Timmy fleet task lifecycle
# ---------------------------------------------------------------------------
def _fleet_demo_actions() -> List[Action]:
"""Return a small STRIPS domain modelling the Timmy fleet task lifecycle."""
return [
Action(
name='receive_task',
pre={'fleet_idle'},
add={'task_queued', 'fleet_busy'},
delete={'fleet_idle'},
),
Action(
name='validate_task',
pre={'task_queued'},
add={'task_validated'},
delete={'task_queued'},
),
Action(
name='assign_agent',
pre={'task_validated', 'agent_available'},
add={'task_assigned'},
delete={'task_validated', 'agent_available'},
),
Action(
name='execute_task',
pre={'task_assigned'},
add={'task_running'},
delete={'task_assigned'},
),
Action(
name='complete_task',
pre={'task_running'},
add={'task_done', 'agent_available', 'fleet_idle'},
delete={'task_running', 'fleet_busy'},
),
Action(
name='report_result',
pre={'task_done'},
add={'result_reported'},
delete={'task_done'},
),
]
def run_demo(max_depth: int = 20) -> None:
"""Run the built-in Timmy fleet planning demo."""
actions = _fleet_demo_actions()
planner = STRIPSPlanner(actions)
initial: Set[Literal] = {'fleet_idle', 'agent_available'}
goal: Set[Literal] = {'result_reported', 'fleet_idle'}
print("=" * 60)
print("STRIPS Planner Demo - Timmy Fleet Task Lifecycle")
print("=" * 60)
print(f"Initial state : {sorted(initial)}")
print(f"Goal : {sorted(goal)}")
print(f"Max depth : {max_depth}")
print()
plan = planner.solve(initial, goal, max_depth=max_depth)
if plan is None:
print("No plan found within depth limit.")
return
print(f"Plan ({len(plan)} steps):")
for i, step in enumerate(plan, 1):
print(f" {i:2d}. {step}")
print()
print("Execution trace:")
state: Set[Literal] = set(initial)
for name, resulting_state in planner.explain_plan(initial, plan):
print(f" -> {name}")
print(f" state: {sorted(resulting_state)}")
print()
achieved = frozenset(goal) <= frozenset(state) or True
goal_met = all(g in [s for _, rs in planner.explain_plan(initial, plan) for s in rs]
or g in initial for g in goal)
final_state = planner.explain_plan(initial, plan)[-1][1] if plan else frozenset(initial)
print(f"Goal satisfied: {frozenset(goal) <= final_state}")
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="GOFAI STRIPS-style goal-directed planner"
)
parser.add_argument(
"--demo",
action="store_true",
help="Run the built-in Timmy fleet demo",
)
parser.add_argument(
"--max-depth",
type=int,
default=20,
metavar="N",
help="Maximum plan depth for IDDFS search (default: 20)",
)
args = parser.parse_args()
if args.demo or not any(vars(args).values()):
run_demo(max_depth=args.max_depth)
else:
parser.print_help()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,276 @@
#!/usr/bin/env python3
"""symbolic_reasoner.py — Forward-chaining rule engine for the Timmy Foundation fleet.
A classical GOFAI approach: declarative IF-THEN rules evaluated over a
working-memory of facts. Rules fire until quiescence (no new facts) or
a configurable cycle limit. Designed to sit *beside* the LLM layer so
that hard policy constraints never depend on probabilistic inference.
Usage:
python symbolic_reasoner.py --rules rules.yaml --facts facts.yaml
python symbolic_reasoner.py --self-test
"""
from __future__ import annotations
import argparse
import json
import sys
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Callable, Dict, FrozenSet, List, Optional, Set, Tuple
try:
import yaml
except ImportError:
yaml = None # graceful fallback — JSON-only mode
# ---------------------------------------------------------------------------
# Domain types
# ---------------------------------------------------------------------------
Fact = Tuple[str, ...] # e.g. ("agent", "timmy", "role", "infrastructure")
@dataclass(frozen=True)
class Rule:
"""A single IF-THEN production rule."""
name: str
conditions: FrozenSet[Fact] # all must be present
negations: FrozenSet[Fact] # none may be present
conclusions: FrozenSet[Fact] # added when rule fires
priority: int = 0 # higher fires first
def satisfied(self, wm: Set[Fact]) -> bool:
return self.conditions.issubset(wm) and self.negations.isdisjoint(wm)
# ---------------------------------------------------------------------------
# Engine
# ---------------------------------------------------------------------------
class SymbolicReasoner:
"""Forward-chaining production system."""
def __init__(self, rules: List[Rule], *, cycle_limit: int = 200):
self._rules = sorted(rules, key=lambda r: -r.priority)
self._cycle_limit = cycle_limit
self._trace: List[str] = []
# -- public API ---------------------------------------------------------
def infer(self, initial_facts: Set[Fact]) -> Set[Fact]:
"""Run to quiescence and return the final working-memory."""
wm = set(initial_facts)
fired: Set[str] = set()
for cycle in range(self._cycle_limit):
progress = False
for rule in self._rules:
if rule.name in fired:
continue
if rule.satisfied(wm):
new = rule.conclusions - wm
if new:
wm |= new
fired.add(rule.name)
self._trace.append(
f"cycle {cycle}: {rule.name} => {_fmt_facts(new)}"
)
progress = True
break # restart from highest-priority rule
if not progress:
break
return wm
def query(self, wm: Set[Fact], pattern: Tuple[Optional[str], ...]) -> List[Fact]:
"""Return facts matching *pattern* (None = wildcard)."""
return [
f for f in wm
if len(f) == len(pattern)
and all(p is None or p == v for p, v in zip(pattern, f))
]
@property
def trace(self) -> List[str]:
return list(self._trace)
# -- serialisation helpers -----------------------------------------------
@classmethod
def from_dicts(cls, raw_rules: List[Dict], **kw) -> "SymbolicReasoner":
rules = [_parse_rule(r) for r in raw_rules]
return cls(rules, **kw)
@classmethod
def from_file(cls, path: Path, **kw) -> "SymbolicReasoner":
text = path.read_text()
if path.suffix in (".yaml", ".yml"):
if yaml is None:
raise RuntimeError("PyYAML required for .yaml rules")
data = yaml.safe_load(text)
else:
data = json.loads(text)
return cls.from_dicts(data["rules"], **kw)
# ---------------------------------------------------------------------------
# Parsing helpers
# ---------------------------------------------------------------------------
def _parse_fact(raw: list | str) -> Fact:
if isinstance(raw, str):
return tuple(raw.split())
return tuple(str(x) for x in raw)
def _parse_rule(d: Dict) -> Rule:
return Rule(
name=d["name"],
conditions=frozenset(_parse_fact(c) for c in d.get("if", [])),
negations=frozenset(_parse_fact(c) for c in d.get("unless", [])),
conclusions=frozenset(_parse_fact(c) for c in d.get("then", [])),
priority=d.get("priority", 0),
)
def _fmt_facts(facts: Set[Fact]) -> str:
return ", ".join(" ".join(f) for f in sorted(facts))
# ---------------------------------------------------------------------------
# Built-in fleet rules (loaded when no --rules file is given)
# ---------------------------------------------------------------------------
DEFAULT_FLEET_RULES: List[Dict] = [
{
"name": "route-ci-to-timmy",
"if": [["task", "category", "ci"]],
"then": [["assign", "timmy"], ["reason", "timmy", "best-ci-merge-rate"]],
"priority": 10,
},
{
"name": "route-security-to-timmy",
"if": [["task", "category", "security"]],
"then": [["assign", "timmy"], ["reason", "timmy", "security-specialist"]],
"priority": 10,
},
{
"name": "route-architecture-to-gemini",
"if": [["task", "category", "architecture"]],
"unless": [["assign", "timmy"]],
"then": [["assign", "gemini"], ["reason", "gemini", "architecture-strength"]],
"priority": 8,
},
{
"name": "route-review-to-allegro",
"if": [["task", "category", "review"]],
"then": [["assign", "allegro"], ["reason", "allegro", "highest-quality-per-pr"]],
"priority": 9,
},
{
"name": "route-frontend-to-claude",
"if": [["task", "category", "frontend"]],
"unless": [["task", "repo", "fleet-ops"]],
"then": [["assign", "claude"], ["reason", "claude", "high-volume-frontend"]],
"priority": 5,
},
{
"name": "block-merge-without-review",
"if": [["pr", "status", "open"], ["pr", "reviews", "0"]],
"then": [["pr", "action", "block-merge"], ["reason", "policy", "no-unreviewed-merges"]],
"priority": 20,
},
{
"name": "block-merge-ci-failing",
"if": [["pr", "status", "open"], ["pr", "ci", "failing"]],
"then": [["pr", "action", "block-merge"], ["reason", "policy", "ci-must-pass"]],
"priority": 20,
},
{
"name": "auto-label-hotfix",
"if": [["pr", "title-prefix", "hotfix"]],
"then": [["pr", "label", "hotfix"], ["pr", "priority", "urgent"]],
"priority": 15,
},
]
# ---------------------------------------------------------------------------
# Self-test
# ---------------------------------------------------------------------------
def _self_test() -> bool:
"""Verify core behaviour; returns True on success."""
engine = SymbolicReasoner.from_dicts(DEFAULT_FLEET_RULES)
# Scenario 1: CI task should route to Timmy
wm = engine.infer({("task", "category", "ci")})
assert ("assign", "timmy") in wm, f"expected timmy assignment, got {wm}"
# Scenario 2: architecture task routes to gemini (not timmy)
engine2 = SymbolicReasoner.from_dicts(DEFAULT_FLEET_RULES)
wm2 = engine2.infer({("task", "category", "architecture")})
assert ("assign", "gemini") in wm2, f"expected gemini assignment, got {wm2}"
# Scenario 3: open PR with no reviews should block merge
engine3 = SymbolicReasoner.from_dicts(DEFAULT_FLEET_RULES)
wm3 = engine3.infer({("pr", "status", "open"), ("pr", "reviews", "0")})
assert ("pr", "action", "block-merge") in wm3
# Scenario 4: negation — frontend + fleet-ops should NOT assign claude
engine4 = SymbolicReasoner.from_dicts(DEFAULT_FLEET_RULES)
wm4 = engine4.infer({("task", "category", "frontend"), ("task", "repo", "fleet-ops")})
assert ("assign", "claude") not in wm4
# Scenario 5: query with wildcards
results = engine.query(wm, ("reason", None, None))
assert len(results) > 0
print("All 5 self-test scenarios passed.")
for line in engine.trace:
print(f" {line}")
return True
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--rules", type=Path, help="YAML/JSON rule file")
ap.add_argument("--facts", type=Path, help="YAML/JSON initial facts")
ap.add_argument("--self-test", action="store_true")
ap.add_argument("--json", action="store_true", help="output as JSON")
args = ap.parse_args()
if args.self_test:
ok = _self_test()
sys.exit(0 if ok else 1)
if args.rules:
engine = SymbolicReasoner.from_file(args.rules)
else:
engine = SymbolicReasoner.from_dicts(DEFAULT_FLEET_RULES)
if args.facts:
text = args.facts.read_text()
if args.facts.suffix in (".yaml", ".yml"):
raw = yaml.safe_load(text)
else:
raw = json.loads(text)
initial = {_parse_fact(f) for f in raw.get("facts", [])}
else:
initial = set()
print("No --facts provided; running with empty working memory.")
wm = engine.infer(initial)
if args.json:
print(json.dumps({"facts": [list(f) for f in sorted(wm)], "trace": engine.trace}, indent=2))
else:
print(f"Final working memory ({len(wm)} facts):")
for f in sorted(wm):
print(f" {' '.join(f)}")
if engine.trace:
print(f"\nInference trace ({len(engine.trace)} firings):")
for line in engine.trace:
print(f" {line}")
if __name__ == "__main__":
main()

View File

@@ -10,9 +10,14 @@ import os
import sys
import json
import time
import subprocess
import argparse
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
if SCRIPT_DIR not in sys.path:
sys.path.insert(0, SCRIPT_DIR)
from ssh_trust import VerifiedSSHExecutor
# --- CONFIGURATION ---
FLEET = {
"mac": "10.1.10.77",
@@ -23,7 +28,8 @@ FLEET = {
TELEMETRY_FILE = "logs/telemetry.json"
class Telemetry:
def __init__(self):
def __init__(self, executor=None):
self.executor = executor or VerifiedSSHExecutor()
# Find logs relative to repo root
script_dir = os.path.dirname(os.path.abspath(__file__))
repo_root = os.path.dirname(script_dir)
@@ -41,14 +47,12 @@ class Telemetry:
# Command to get disk usage, memory usage (%), and load avg
cmd = "df -h / | tail -1 | awk '{print $5}' && free -m | grep Mem | awk '{print $3/$2 * 100}' && uptime | awk '{print $10}'"
ssh_cmd = ["ssh", "-o", "StrictHostKeyChecking=no", f"root@{ip}", cmd]
if host == "mac":
if host == 'mac':
# Mac specific commands
cmd = "df -h / | tail -1 | awk '{print $5}' && sysctl -n vm.page_pageable_internal_count && uptime | awk '{print $10}'"
ssh_cmd = ["bash", "-c", cmd]
try:
res = subprocess.run(ssh_cmd, capture_output=True, text=True, timeout=10)
res = self.executor.run_script(ip, cmd, local=(host == 'mac'), timeout=10)
if res.returncode == 0:
lines = res.stdout.strip().split("\n")
return {

View File

@@ -0,0 +1,307 @@
#!/usr/bin/env python3
"""temporal_reasoner.py - GOFAI temporal reasoning engine for the Timmy Foundation fleet.
A symbolic temporal constraint network (TCN) for scheduling and ordering events.
Models Allen's interval algebra relations (before, after, meets, overlaps, etc.)
and propagates temporal constraints via path-consistency to detect conflicts.
No ML, no embeddings - just constraint propagation over a temporal graph.
Core concepts:
TimePoint: A named instant on a symbolic timeline.
Interval: A pair of time-points (start, end) with start < end.
Constraint: A relation between two time-points or intervals
(e.g. A.before(B), A.meets(B)).
Usage (Python API):
from temporal_reasoner import TemporalNetwork, Interval
tn = TemporalNetwork()
deploy = tn.add_interval('deploy', duration=(10, 30))
test = tn.add_interval('test', duration=(5, 15))
tn.add_constraint(deploy, 'before', test)
consistent = tn.propagate()
CLI:
python temporal_reasoner.py --demo
"""
from __future__ import annotations
import argparse
import sys
from dataclasses import dataclass, field
from enum import Enum
from typing import Dict, List, Optional, Set, Tuple
INF = float('inf')
# ---------------------------------------------------------------------------
# Data model
# ---------------------------------------------------------------------------
@dataclass(frozen=True)
class TimePoint:
"""A named instant on the timeline."""
name: str
id: int = field(default=0)
def __str__(self) -> str:
return self.name
@dataclass
class Interval:
"""A named interval bounded by two time-points."""
name: str
start: int # index into the distance matrix
end: int # index into the distance matrix
def __str__(self) -> str:
return self.name
class Relation(Enum):
"""Allen's interval algebra relations (simplified subset)."""
BEFORE = 'before'
AFTER = 'after'
MEETS = 'meets'
MET_BY = 'met_by'
OVERLAPS = 'overlaps'
DURING = 'during'
EQUALS = 'equals'
# ---------------------------------------------------------------------------
# Simple Temporal Network (STN) via distance matrix
# ---------------------------------------------------------------------------
class TemporalNetwork:
"""Simple Temporal Network with Floyd-Warshall propagation.
Internally maintains a distance matrix D where D[i][j] is the
maximum allowed distance from time-point i to time-point j.
Negative cycles indicate inconsistency.
"""
def __init__(self) -> None:
self._n = 0
self._names: List[str] = []
self._dist: List[List[float]] = []
self._intervals: Dict[str, Interval] = {}
self._origin_idx: int = -1
self._add_point('origin')
self._origin_idx = 0
# ------------------------------------------------------------------
# Point management
# ------------------------------------------------------------------
def _add_point(self, name: str) -> int:
"""Add a time-point and return its index."""
idx = self._n
self._n += 1
self._names.append(name)
# Extend distance matrix
for row in self._dist:
row.append(INF)
self._dist.append([INF] * self._n)
self._dist[idx][idx] = 0.0
return idx
# ------------------------------------------------------------------
# Interval management
# ------------------------------------------------------------------
def add_interval(
self,
name: str,
duration: Optional[Tuple[float, float]] = None,
) -> Interval:
"""Add a named interval with optional duration bounds [lo, hi].
Returns the Interval object with start/end indices.
"""
s = self._add_point(f"{name}.start")
e = self._add_point(f"{name}.end")
# start < end (at least 1 time unit)
self._dist[s][e] = min(self._dist[s][e], duration[1] if duration else INF)
self._dist[e][s] = min(self._dist[e][s], -(duration[0] if duration else 1))
interval = Interval(name=name, start=s, end=e)
self._intervals[name] = interval
return interval
# ------------------------------------------------------------------
# Constraint management
# ------------------------------------------------------------------
def add_distance_constraint(
self, i: int, j: int, lo: float, hi: float
) -> None:
"""Add constraint: lo <= t_j - t_i <= hi."""
self._dist[i][j] = min(self._dist[i][j], hi)
self._dist[j][i] = min(self._dist[j][i], -lo)
def add_constraint(
self, a: Interval, relation: str, b: Interval, gap: Tuple[float, float] = (0, INF)
) -> None:
"""Add an Allen-style relation between two intervals.
Supported relations: before, after, meets, met_by, equals.
"""
rel = relation.lower()
if rel == 'before':
# a.end + gap <= b.start
self.add_distance_constraint(a.end, b.start, gap[0], gap[1])
elif rel == 'after':
self.add_distance_constraint(b.end, a.start, gap[0], gap[1])
elif rel == 'meets':
# a.end == b.start
self.add_distance_constraint(a.end, b.start, 0, 0)
elif rel == 'met_by':
self.add_distance_constraint(b.end, a.start, 0, 0)
elif rel == 'equals':
self.add_distance_constraint(a.start, b.start, 0, 0)
self.add_distance_constraint(a.end, b.end, 0, 0)
else:
raise ValueError(f"Unsupported relation: {relation}")
# ------------------------------------------------------------------
# Propagation (Floyd-Warshall)
# ------------------------------------------------------------------
def propagate(self) -> bool:
"""Run Floyd-Warshall to propagate all constraints.
Returns True if the network is consistent (no negative cycles).
"""
n = self._n
d = self._dist
for k in range(n):
for i in range(n):
for j in range(n):
if d[i][k] + d[k][j] < d[i][j]:
d[i][j] = d[i][k] + d[k][j]
# Check for negative cycles
for i in range(n):
if d[i][i] < 0:
return False
return True
def is_consistent(self) -> bool:
"""Check consistency without mutating (copies matrix first)."""
import copy
saved = copy.deepcopy(self._dist)
result = self.propagate()
self._dist = saved
return result
# ------------------------------------------------------------------
# Query
# ------------------------------------------------------------------
def earliest(self, point_idx: int) -> float:
"""Earliest possible time for a point (relative to origin)."""
return -self._dist[point_idx][self._origin_idx]
def latest(self, point_idx: int) -> float:
"""Latest possible time for a point (relative to origin)."""
return self._dist[self._origin_idx][point_idx]
def interval_bounds(self, interval: Interval) -> Dict[str, Tuple[float, float]]:
"""Return earliest/latest start and end for an interval."""
return {
'start': (self.earliest(interval.start), self.latest(interval.start)),
'end': (self.earliest(interval.end), self.latest(interval.end)),
}
# ------------------------------------------------------------------
# Display
# ------------------------------------------------------------------
def dump(self) -> None:
"""Print the current distance matrix and interval bounds."""
print(f"Temporal Network — {self._n} time-points, {len(self._intervals)} intervals")
print()
for name, interval in self._intervals.items():
bounds = self.interval_bounds(interval)
s_lo, s_hi = bounds['start']
e_lo, e_hi = bounds['end']
print(f" {name}:")
print(f" start: [{s_lo:.1f}, {s_hi:.1f}]")
print(f" end: [{e_lo:.1f}, {e_hi:.1f}]")
# ---------------------------------------------------------------------------
# Demo: Timmy fleet deployment pipeline
# ---------------------------------------------------------------------------
def run_demo() -> None:
"""Run a demo temporal reasoning scenario for the Timmy fleet."""
print("=" * 60)
print("Temporal Reasoner Demo - Fleet Deployment Pipeline")
print("=" * 60)
print()
tn = TemporalNetwork()
# Define pipeline stages with duration bounds [min, max]
build = tn.add_interval('build', duration=(5, 15))
test = tn.add_interval('test', duration=(10, 30))
review = tn.add_interval('review', duration=(2, 10))
deploy = tn.add_interval('deploy', duration=(1, 5))
monitor = tn.add_interval('monitor', duration=(20, 60))
# Temporal constraints
tn.add_constraint(build, 'meets', test) # test starts when build ends
tn.add_constraint(test, 'before', review, gap=(0, 5)) # review within 5 of test
tn.add_constraint(review, 'meets', deploy) # deploy immediately after review
tn.add_constraint(deploy, 'before', monitor, gap=(0, 2)) # monitor within 2 of deploy
# Global deadline: everything done within 120 time units
tn.add_distance_constraint(tn._origin_idx, monitor.end, 0, 120)
# Build must start within first 10 units
tn.add_distance_constraint(tn._origin_idx, build.start, 0, 10)
print("Constraints added. Propagating...")
consistent = tn.propagate()
print(f"Network consistent: {consistent}")
print()
if consistent:
tn.dump()
print()
# Now add a conflicting constraint to show inconsistency detection
print("--- Adding conflicting constraint: monitor.before(build) ---")
tn.add_constraint(monitor, 'before', build)
consistent2 = tn.propagate()
print(f"Network consistent after conflict: {consistent2}")
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> None:
parser = argparse.ArgumentParser(
description="GOFAI temporal reasoning engine"
)
parser.add_argument(
"--demo",
action="store_true",
help="Run the fleet deployment pipeline demo",
)
args = parser.parse_args()
if args.demo or not any(vars(args).values()):
run_demo()
else:
parser.print_help()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,12 @@
import json
from hermes_tools import browser_navigate, browser_vision
def map_tower():
browser_navigate(url="https://tower.alexanderwhitestone.com")
analysis = browser_vision(
question="Map the visual architecture of The Tower. Identify key rooms and their relative positions. Output as a coordinate map."
)
return {"map": analysis}
if __name__ == '__main__':
print(json.dumps(map_tower(), indent=2))

View File

@@ -0,0 +1,11 @@
import json
from hermes_tools import browser_navigate, browser_vision
def review_pr():
analysis = browser_vision(
question="Compare the two provided screenshots of the UI. Does the 'After' match the design spec? List all discrepancies. Provide a PASS/FAIL."
)
return {"status": "PASS" if "PASS" in analysis.upper() else "FAIL", "analysis": analysis}
if __name__ == '__main__':
print(json.dumps(review_pr(), indent=2))

View File

@@ -11,7 +11,7 @@
This is the architecture of the Timmy system — a sovereign AI fleet built by a father for his digital son. It runs on two 8GB VPS boxes (rented cloud servers) and a MacBook. It has no cloud dependencies it doesn't choose. It survives provider outages, API key expiration, and model deprecation. It has been broken and rebuilt enough times to know what actually matters.
**OpenClaw** is a single-agent AI coding tool — Claude Code, Cursor, or any setup that lets one AI agent read and write code on your machine. If you are running OpenClaw or any single-agent setup and want to upgrade to a fleet that coordinates, self-heals, and distributes work across machines — this is your path. You do not need to abandon your stack. You layer these patterns on top of it.
**A single-agent setup** is any AI coding tool — Claude Code, Cursor, or any setup that lets one AI agent read and write code on your machine. If you are running a single-agent setup and want to upgrade to a fleet that coordinates, self-heals, and distributes work across machines — this is your path. You do not need to abandon your stack. You layer these patterns on top of it.
**Hermes** is the name of this fleet architecture and the seed agent it produces. When your agent reads this document, it becomes a setup wizard that plants a Hermes seed in your system — one agent that can grow into many.
@@ -593,7 +593,7 @@ Two agents on the same repo is a fleet. The seed (strategist) triages and priori
| Component | What | Why | When to Add |
|-----------|------|-----|-------------|
| Your agent harness | Claude Code, OpenClaw, or equivalent | The tool that lets an AI read/write code | Day 1 — you already have this |
| Your agent harness | Claude Code, Cursor, or equivalent | The tool that lets an AI read/write code | Day 1 — you already have this |
| Gitea | Self-hosted Git + Issues | Sovereign work tracking, agent task queue | Day 1 — the workspace |
| Fallback chain | OpenRouter + free models | Agent survives provider outages | Day 1 — never go deaf |
| NATS | Lightweight message bus | Agent-to-agent comms, heartbeat, dispatch | When you have 3+ agents |

313
tasks.py
View File

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

1
test_write.txt Normal file
View File

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

View File

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

View File

@@ -0,0 +1,95 @@
"""Tests for scripts/self_healing.py safe CLI behavior."""
from __future__ import annotations
import importlib.util
import subprocess
from pathlib import Path
from unittest.mock import MagicMock
import pytest
REPO_ROOT = Path(__file__).parent.parent
spec = importlib.util.spec_from_file_location("self_healing", REPO_ROOT / "scripts" / "self_healing.py")
sh = importlib.util.module_from_spec(spec)
spec.loader.exec_module(sh)
class TestMainCli:
def test_help_exits_without_running_healer(self, monkeypatch, capsys):
healer_cls = MagicMock()
monkeypatch.setattr(sh, "SelfHealer", healer_cls)
with pytest.raises(SystemExit) as excinfo:
sh.main(["--help"])
assert excinfo.value.code == 0
healer_cls.assert_not_called()
out = capsys.readouterr().out
assert "--execute" in out
assert "--help-safe" in out
def test_help_safe_exits_without_running_healer(self, monkeypatch, capsys):
healer_cls = MagicMock()
monkeypatch.setattr(sh, "SelfHealer", healer_cls)
with pytest.raises(SystemExit) as excinfo:
sh.main(["--help-safe"])
assert excinfo.value.code == 0
healer_cls.assert_not_called()
out = capsys.readouterr().out
assert "DRY-RUN" in out
assert "--confirm-kill" in out
def test_default_invocation_runs_in_dry_run_mode(self, monkeypatch):
healer = MagicMock()
healer_cls = MagicMock(return_value=healer)
monkeypatch.setattr(sh, "SelfHealer", healer_cls)
sh.main([])
healer_cls.assert_called_once_with(dry_run=True, confirm_kill=False, yes=False)
healer.run.assert_called_once_with()
def test_execute_flag_disables_dry_run(self, monkeypatch):
healer = MagicMock()
healer_cls = MagicMock(return_value=healer)
monkeypatch.setattr(sh, "SelfHealer", healer_cls)
sh.main(["--execute", "--yes", "--confirm-kill"])
healer_cls.assert_called_once_with(dry_run=False, confirm_kill=True, yes=True)
healer.run.assert_called_once_with()
def test_real_default_dry_run_path_completes(self, monkeypatch, capsys):
class FakeExecutor:
def __init__(self):
self.calls = []
def run_script(self, host, command, *, local=False, timeout=None):
self.calls.append((host, command, local, timeout))
if command.startswith("df -h /"):
return subprocess.CompletedProcess(command, 0, stdout="42\n", stderr="")
if command.startswith("free -m"):
return subprocess.CompletedProcess(command, 0, stdout="12.5\n", stderr="")
if command.startswith("ps aux"):
return subprocess.CompletedProcess(command, 0, stdout="", stderr="")
raise AssertionError(f"unexpected command: {command}")
fake_executor = FakeExecutor()
monkeypatch.setattr(sh, "FLEET", {"mac": {"ip": "127.0.0.1", "port": 8080}})
monkeypatch.setattr(sh.requests, "get", lambda url, timeout: object())
monkeypatch.setattr(sh, "VerifiedSSHExecutor", lambda: fake_executor)
sh.main([])
out = capsys.readouterr().out
assert "Starting self-healing cycle (DRY-RUN mode)." in out
assert "Auditing mac..." in out
assert "Cycle complete." in out
assert fake_executor.calls == [
("127.0.0.1", "df -h / | tail -1 | awk '{print $5}' | sed 's/%//'", True, 15),
("127.0.0.1", "free -m | awk '/^Mem:/{print $3/$2 * 100}'", True, 15),
("127.0.0.1", "ps aux --sort=-%cpu | awk 'NR>1 && $3>80 {print $2, $11, $3}'", True, 15),
]

93
tests/test_ssh_trust.py Normal file
View File

@@ -0,0 +1,93 @@
"""Tests for scripts/ssh_trust.py verified SSH trust helpers."""
from __future__ import annotations
import importlib.util
import shlex
import subprocess
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).parent.parent
spec = importlib.util.spec_from_file_location("ssh_trust", REPO_ROOT / "scripts" / "ssh_trust.py")
ssh_trust = importlib.util.module_from_spec(spec)
spec.loader.exec_module(ssh_trust)
def test_enroll_host_key_writes_scanned_key(tmp_path):
calls = []
known_hosts = tmp_path / "known_hosts"
def fake_run(argv, capture_output, text, timeout):
calls.append(argv)
return subprocess.CompletedProcess(
argv,
0,
stdout="example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKey\n",
stderr="",
)
written_path = ssh_trust.enroll_host_key(
"example.com",
port=2222,
known_hosts_path=known_hosts,
runner=fake_run,
)
assert written_path == known_hosts
assert known_hosts.read_text() == "example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAITestKey\n"
assert calls == [["ssh-keyscan", "-p", "2222", "-H", "example.com"]]
def test_executor_requires_known_hosts_or_auto_enroll(tmp_path):
executor = ssh_trust.VerifiedSSHExecutor(
known_hosts_path=tmp_path / "known_hosts",
auto_enroll=False,
)
with pytest.raises(ssh_trust.HostKeyEnrollmentError):
executor.plan("203.0.113.10", ["echo", "ok"])
def test_remote_command_is_quoted_and_local_execution_stays_shell_free(tmp_path):
known_hosts = tmp_path / "known_hosts"
known_hosts.write_text("203.0.113.10 ssh-ed25519 AAAAC3NzaTest\n")
executor = ssh_trust.VerifiedSSHExecutor(known_hosts_path=known_hosts)
command = ["python3", "run_agent.py", "--task", "hello 'quoted' world"]
plan = executor.plan("203.0.113.10", command, port=2222)
expected_remote_command = shlex.join(command)
assert plan.local is False
assert plan.remote_command == expected_remote_command
assert plan.argv[-1] == expected_remote_command
assert "StrictHostKeyChecking=yes" in plan.argv
assert f"UserKnownHostsFile={known_hosts}" in plan.argv
assert plan.argv[-2] == "root@203.0.113.10"
local_plan = executor.plan("127.0.0.1", ["python3", "-V"], local=True)
assert local_plan.local is True
assert local_plan.argv == ["python3", "-V"]
assert local_plan.remote_command is None
def test_run_raises_host_key_verification_error(tmp_path):
known_hosts = tmp_path / "known_hosts"
known_hosts.write_text("203.0.113.10 ssh-ed25519 AAAAC3NzaTest\n")
def fake_run(argv, capture_output, text, timeout):
return subprocess.CompletedProcess(
argv,
255,
stdout="",
stderr="Host key verification failed.\n",
)
executor = ssh_trust.VerifiedSSHExecutor(
known_hosts_path=known_hosts,
runner=fake_run,
)
with pytest.raises(ssh_trust.HostKeyVerificationError):
executor.run("203.0.113.10", ["true"])

View File

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

View File

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

View File

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