Compare commits

..

2 Commits

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

View File

@@ -51,11 +51,6 @@ The scripts in `bin/` are sidecar-managed operational helpers for the Hermes lay
Do NOT assume older prose about removed loops is still true at runtime.
Audit the live machine first, then read `docs/automation-inventory.md` for the
current reality and stale-state risks.
For communication-layer truth, read:
- `docs/comms-authority-map.md`
- `docs/nostur-operator-edge.md`
- `docs/operator-comms-onboarding.md`
For fleet routing semantics over sovereign transport, read
`docs/ipc-hub-and-spoke-doctrine.md`.

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -64,12 +64,8 @@ for p in json.load(sys.stdin):
echo "Claude workers: $(pgrep -f 'claude.*--print.*--dangerously' 2>/dev/null | wc -l | tr -d ' ')" >> "$state_dir/agent_status.txt"
echo "Claude loop: $(pgrep -f 'claude-loop.sh' 2>/dev/null | wc -l | tr -d ' ') procs" >> "$state_dir/agent_status.txt"
tail -50 "$LOG_DIR/claude-loop.log" 2>/dev/null | grep -c "SUCCESS" | xargs -I{} echo "Claude recent successes: {}" >> "$state_dir/agent_status.txt"
tail -50 "$LOG_DIR/claude-loop.log" 2>/dev/null | grep -c "FAILED" | xargs -I{} echo "Claude recent failures: {}" >> "$state_dir/agent_status.txt"
echo "Kimi heartbeat launchd: $(launchctl list 2>/dev/null | grep -c 'ai.timmy.kimi-heartbeat' | tr -d ' ') job" >> "$state_dir/agent_status.txt"
tail -50 "/tmp/kimi-heartbeat.log" 2>/dev/null | grep -c "DISPATCHED:" | xargs -I{} echo "Kimi recent dispatches: {}" >> "$state_dir/agent_status.txt"
tail -50 "/tmp/kimi-heartbeat.log" 2>/dev/null | grep -c "FAILED:" | xargs -I{} echo "Kimi recent failures: {}" >> "$state_dir/agent_status.txt"
tail -1 "/tmp/kimi-heartbeat.log" 2>/dev/null | xargs -I{} echo "Kimi last event: {}" >> "$state_dir/agent_status.txt"
tail -50 "$LOG_DIR/claude-loop.log" 2>/dev/null | grep -c "SUCCESS" | xargs -I{} echo "Recent successes: {}" >> "$state_dir/agent_status.txt"
tail -50 "$LOG_DIR/claude-loop.log" 2>/dev/null | grep -c "FAILED" | xargs -I{} echo "Recent failures: {}" >> "$state_dir/agent_status.txt"
echo "$state_dir"
}
@@ -168,14 +164,7 @@ HEADER
fi
echo "" >> "$prompt_file"
cat >> "$prompt_file" <<'FOOTER'
INSTRUCTIONS: For EACH PR above, do ONE of the following RIGHT NOW using your terminal tool:
- Run the merge curl command if the diff looks good
- Run the close curl command if it is a duplicate or garbage
- Run the comment curl command only if there is a clear bug
IMPORTANT: Actually run the curl commands. Do not just describe what you would do. Finish means the PR world-state changed.
FOOTER
echo "Review each PR above. Execute curl commands for your decisions. Be brief." >> "$prompt_file"
local prompt_text
prompt_text=$(cat "$prompt_file")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,32 +0,0 @@
[global]
server_name = "fleet.example.com"
address = "0.0.0.0"
port = 6167
[database]
backend = "sqlite"
path = "/var/lib/matrix-conduit"
[registration]
enabled = false
token = "CHANGE_THIS_TO_32_HEX_CHARS"
allow_registration_without_token = false
[federation]
enabled = true
enable_open_federation = true
trusted_servers = []
[media]
max_file_size = 10_485_760
max_thumbnail_size = 5_242_880
[presence]
enabled = true
update_interval = 300_000
[log]
level = "info"
[admin]
admins = ["@admin:fleet.example.com"]

View File

@@ -1,48 +0,0 @@
version: "3.8"
# Conduit Matrix homeserver - Sovereign fleet communication
# Deploy: docker-compose up -d
# Requirements: Docker 20.10+, valid DNS A/AAAA and SRV records
services:
conduit:
image: docker.io/matrixconduit/matrix-conduit:v0.7.0
container_name: conduit
restart: unless-stopped
volumes:
- ./conduit.toml:/etc/conduit/conduit.toml:ro
- conduit-data:/var/lib/matrix-conduit
environment:
CONDUIT_SERVER_NAME: ${MATRIX_SERVER_NAME:?Required}
CONDUIT_DATABASE_BACKEND: sqlite
CONDUIT_DATABASE_PATH: /var/lib/matrix-conduit
CONDUIT_PORT: 6167
CONDUIT_MAX_REQUEST_SIZE: 20_000_000
networks:
- matrix
element:
image: vectorim/element-web:v1.11.59
container_name: element-web
restart: unless-stopped
volumes:
- ./element-config.json:/app/config.json:ro
networks:
- matrix
backup:
image: rclone/rclone:latest
container_name: conduit-backup
volumes:
- conduit-data:/data:ro
- ./backup-scripts:/scripts:ro
entrypoint: /scripts/backup.sh
profiles: ["backup"]
networks:
- matrix
networks:
matrix:
driver: bridge
volumes:
conduit-data:

View File

@@ -1,14 +0,0 @@
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.example.com",
"server_name": "example.com"
}
},
"brand": "Timmy Fleet",
"default_theme": "dark",
"features": {
"feature_spaces": true,
"feature_voice_rooms": true
}
}

View File

@@ -1,46 +0,0 @@
#!/bin/bash
set -euo pipefail
MATRIX_SERVER_NAME=${1:-"fleet.example.com"}
ADMIN_USER=${2:-"admin"}
BOT_USERS=("bilbo" "ezra" "allegro" "bezalel" "gemini" "timmy")
echo "=== Fleet Matrix Bootstrap ==="
echo "Server: $MATRIX_SERVER_NAME"
REG_TOKEN=$(openssl rand -hex 32)
echo "$REG_TOKEN" > .registration_token
cat > docker-compose.override.yml << EOF
version: "3.8"
services:
conduit:
environment:
CONDUIT_SERVER_NAME: $MATRIX_SERVER_NAME
CONDUIT_REGISTRATION_TOKEN: $REG_TOKEN
EOF
ADMIN_PW=$(openssl rand -base64 24)
cat > admin-register.json << EOF
{"username": "$ADMIN_USER", "password": "$ADMIN_PW", "admin": true}
EOF
mkdir -p bot-tokens
for bot in "${BOT_USERS[@]}"; do
BOT_PW=$(openssl rand -base64 24)
echo "{"username": "$bot", "password": "$BOT_PW"}" > "bot-tokens/${bot}.json"
done
cat > room-topology.yaml << 'EOF'
spaces:
fleet-command:
name: "Fleet Command"
rooms:
- {name: "📢 Announcements", encrypted: false}
- {name: "⚡ Operations", encrypted: true}
- {name: "🔮 Intelligence", encrypted: true}
- {name: "🛠️ Infrastructure", encrypted: true}
EOF
echo "Bootstrap complete. Check admin-password.txt and bot-tokens/"
echo "Admin password: $ADMIN_PW"

View File

@@ -1,262 +0,0 @@
# 🔥 BURN MODE CONTINUITY — Primary Targets Engaged
**Date**: 2026-04-05
**Burn Directive**: timmy-config #183, #166, the-nexus #830
**Executor**: Ezra (Archivist)
**Status**: ✅ **ALL TARGETS SCAFFOLDED — CONTINUITY PRESERVED**
---
## Executive Summary
Three primary targets have been assessed, scaffolded, and connected into a coherent fleet architecture. Each issue has transitioned from aspiration/fuzzy epic to executable implementation plan.
| Target | Repo | Previous State | Current State | Scaffold Size |
|--------|------|----------------|---------------|---------------|
| #183 | timmy-config | Aspirational scaffold | ✅ Complete deployment kit | 12+ files, 2 dirs |
| #166 | timmy-config | Fuzzy epic | ✅ Executable with blockers isolated | Architecture doc (8KB) |
| #830 | the-nexus | Feature request | ✅ 5-phase production scaffold | 5 bins + 3 docs (~70KB) |
---
## Cross-Target Architecture
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ FLEET COMMUNICATION LAYERS │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ HUMAN-TO-FLEET FLEET-INTERNAL INTEL │
│ ┌───────────────┐ ┌───────────────┐ ┌────────┐│
│ │ Matrix │◀──────────────▶│ Nostr │ │ Deep ││
│ │ #166 │ #173 unify │ #174 │ │ Dive ││
│ │ (scaffolded)│ │ (deployed) │ │ #830 ││
│ └───────────────┘ └───────────────┘ │(ready) ││
│ │ │ └───┬────┘│
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ ALEXANDER (Operator Surface) │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## Target #1: timmy-config #183
**Title**: [COMMS] Produce Matrix/Conduit deployment scaffold and host prerequisites
**Status**: CLOSED ✅ (but continuity verified)
**Issue State**: All acceptance criteria met
### Deliverables Verified
| Criterion | Status | Location |
|-----------|--------|----------|
| Repo-visible deployment scaffold | ✅ | `infra/matrix/` + `deploy/conduit/` |
| Host/port/reverse-proxy explicit | ✅ | `docs/matrix-fleet-comms/README.md` |
| Missing prerequisites named | ✅ | `prerequisites.md` — 6 named blockers |
| Lowers #166 from fuzzy to executable | ✅ | Phase-gated plan with estimates |
### Artifact Inventory
**`infra/matrix/`** (Docker path):
- `README.md` — Entry point
- `prerequisites.md` — Host options, 6 explicit blockers
- `docker-compose.yml` — Container orchestration
- `conduit.toml` — Homeserver configuration
- `deploy-matrix.sh` — One-command deployment
- `.env.example` — Configuration template
- `caddy/` — Reverse proxy configs
**`deploy/conduit/`** (Binary path):
- `conduit.toml` — Production config
- `conduit.service` — systemd definition
- `Caddyfile` — Reverse proxy
- `install.sh` — One-command installer
- `scripts/` — Backup, health check helpers
**`docs/matrix-fleet-comms/README.md`** (Architecture):
- 3 Architecture Decision Records (ADRs)
- Complete port allocation table
- 4-phase implementation plan with estimates
- Operational runbooks (backup, health, account creation)
- Cross-issue linkages
### Architecture Decisions
1. **ADR-1**: Conduit selected over Synapse/Dendrite (low resource, SQLite support)
2. **ADR-2**: Gitea VPS host initially (consolidated ops)
3. **ADR-3**: Full federation enabled (requires TLS + public DNS)
### Blocking Prerequisites
| # | Prerequisite | Authority | Effort |
|---|--------------|-----------|--------|
| 1 | Target host selected (Hermes vs Allegro vs new) | Alexander/admin | 15 min |
| 2 | Domain assigned: `matrix.timmy.foundation` | Alexander/admin | 15 min |
| 3 | DNS A record created | Alexander/admin | 15 min |
| 4 | DNS SRV record for federation | Alexander/admin | 15 min |
| 5 | Firewall: TCP 8448 open | Host admin | 5 min |
| 6 | SSL strategy confirmed | Caddy auto | 0 min |
---
## Target #2: timmy-config #166
**Title**: [COMMS] Stand up Matrix/Conduit for human-to-fleet encrypted communication
**Status**: OPEN 🟡
**Issue State**: Scaffold complete, execution blocked on #187
### Evolution: Fuzzy Epic → Executable
| Phase | Before | After |
|-------|--------|-------|
| Idea | "We should use Matrix" | Concrete deployment path |
| Scaffold | None | 12+ files, fully documented |
| Blockers | Unknown | Explicitly named in #187 |
| Next Steps | Undefined | Phase-gated with estimates |
### Acceptance Criteria Progress
| Criterion | Status | Blocker |
|-----------|--------|---------|
| Deploy Conduit homeserver | 🟡 Ready | #187 DNS decision |
| Create fleet rooms/channels | 🟡 Ready | Post-deployment |
| Encrypted operator messaging | 🟡 Ready | Post-accounts |
| Telegram→Matrix cutover | ⏳ Pending | Post-verification |
| Alexander can message fleet | ⏳ Pending | Post-deployment |
| Messages encrypted/persistent | ⏳ Pending | Post-deployment |
| Telegram not only surface | ⏳ Pending | Migration timeline TBD |
### Handoff from #183
**#183 delivered:**
- ✅ Deployable configuration files
- ✅ Executable installation scripts
- ✅ Operational runbooks
- ✅ Phase-gated implementation plan
- ✅ Bootstrap account/room specifications
**#166 needs:**
- DNS decisions (#187)
- Execution (run install scripts)
- Testing (verify E2E encryption)
---
## Target #3: the-nexus #830
**Title**: [EPIC] Deep Dive: Sovereign NotebookLM + Daily AI Intelligence Briefing
**Status**: OPEN ✅
**Issue State**: Production-ready scaffold, 5 phases complete
### 5-Phase Scaffold
| Phase | Component | File | Lines | Purpose |
|-------|-----------|------|-------|---------|
| 1 | Aggregate | `bin/deepdive_aggregator.py` | ~95 | arXiv RSS, lab blog ingestion |
| 2 | Filter | `bin/deepdive_filter.py` | NA | Included in aggregator/orchestrator |
| 3 | Synthesize | `bin/deepdive_synthesis.py` | ~190 | LLM briefing generation |
| 4 | Audio | `bin/deepdive_tts.py` | ~240 | Multi-adapter TTS (Piper/ElevenLabs) |
| 5 | Deliver | `bin/deepdive_delivery.py` | ~210 | Telegram voice/text delivery |
| — | Orchestrate | `bin/deepdive_orchestrator.py` | ~320 | Pipeline coordination, cron |
**Total**: ~1,055 lines of executable Python
### Documentation Inventory
| File | Lines | Purpose |
|------|-------|---------|
| `docs/DEEPSDIVE_ARCHITECTURE.md` | ~88 | 5-phase spec, data flows |
| `docs/DEEPSDIVE_EXECUTION.md` | ~NA | Runbook, troubleshooting |
| `docs/DEEPSDIVE_QUICKSTART.md` | ~NA | Fast-path to first briefing |
### Acceptance Criteria — All Ready
| Criterion | Issue Req | Status | Evidence |
|-----------|-----------|--------|----------|
| Zero manual copy-paste | Mandatory | ✅ | Cron automation |
| Daily 6 AM delivery | Mandatory | ✅ | Configurable schedule |
| arXiv (cs.AI/cs.CL/cs.LG) | Mandatory | ✅ | RSS fetcher |
| Lab blog coverage | Mandatory | ✅ | OpenAI/Anthropic/DeepMind |
| Relevance filtering | Mandatory | ✅ | Embedding + keyword |
| Written briefing | Mandatory | ✅ | Synthesis engine |
| Audio via TTS | Mandatory | ✅ | Piper + ElevenLabs adapters |
| Telegram delivery | Mandatory | ✅ | Voice message support |
| On-demand trigger | Mandatory | ✅ | CLI flag in orchestrator |
### Sovereignty Compliance
| Dependency | Local Option | Cloud Fallback |
|------------|--------------|----------------|
| TTS | Piper (offline) | ElevenLabs API |
| LLM | Hermes (local) | Provider routing |
| Scheduler | Cron (system) | Manual trigger |
| Storage | Filesystem | No DB required |
---
## Interconnection Map
### #830 → #166
Deep Dive intelligence briefings can target Matrix rooms as delivery channel (alternative to Telegram voice).
### #830 → #173
Deep Dive is the **content layer** in the comms unification stack — what gets said, via which channel.
### #166 → #173
Matrix is the **human-to-fleet channel** — sovereign, encrypted, persistent.
### #166 → #174
Matrix and Nostr operate in parallel — Matrix for rich messaging, Nostr for lightweight broadcast. Both are sovereign.
### #183 → #166
Scaffold enables execution. Child enables parent.
---
## Decision Authority Summary
| Decision | Location | Authority | Current State |
|----------|----------|-----------|---------------|
| Matrix deployment timing | #187 | Alexander/admin | ⏳ DNS pending |
| Deep Dive TTS preference | #830 | Alexander | ⏳ Local vs API |
| Matrix/Nostr priority | #173 | Alexander | ⏳ Active discussion |
---
## Burn Mode Artifacts Created
### Visible Comments (SITREPs)
- #183: Continuity verification SITREP
- #166: Execution bridge SITREP
- #830: Architecture assessment SITREP
### Documentation
- `docs/matrix-fleet-comms/README.md` — Matrix architecture (8KB)
- `docs/BURN_MODE_CONTINUITY_2026-04-05.md` — This document
### Code Scaffold
- 5 Deep Dive Python modules (~1,055 lines)
- 3 Deep Dive documentation files
- 12+ Matrix/Conduit deployment files
---
## Sign-off
All three primary targets have been:
1.**Read and assessed** — Current state documented
2.**SITREP comments posted** — Visible continuity trail
3.**Scaffold verified/extended** — Strongest proof committed
**#183**: Acceptance criteria satisfied, scaffold in repo truth
**#166**: Executable path defined, blockers isolated to #187
**#830**: Production-ready scaffold, all 5 phases implemented
Continuity preserved. Architecture connected. Decisions forward.
— Ezra, Archivist
2026-04-05

View File

@@ -1,112 +0,0 @@
# Canonical Index: Matrix/Conduit Deployment Artifacts
> **Issues**: [#166](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/166) (Execution Epic) | [#183](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/183) (Scaffold — Closed) | [#187](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/187) (Decision Blocker)
> **Created**: 2026-04-05 by Ezra (burn mode)
> **Purpose**: Single source of truth mapping every Matrix/Conduit artifact in `timmy-config`. Stops scatter, eliminates "which file is real?" ambiguity.
---
## Status at a Glance
| Milestone | State | Evidence |
|-----------|-------|----------|
| Deployment scaffold | ✅ Complete | `infra/matrix/` (15 files) |
| Operator runbook | ✅ Complete | `docs/matrix-fleet-comms/` |
| Host readiness script | ✅ Complete | `infra/matrix/host-readiness-check.sh` |
| Target host selected | ⚠️ **BLOCKED** | Pending [#187](../issues/187) |
| Live deployment | ⚠️ **BLOCKED** | Waiting on host + domain + proxy decision |
---
## Authoritative Paths (Read/Edit These)
### 1. Deployment Scaffold — `infra/matrix/`
This is the **primary executable scaffold**. If you are deploying Conduit, start here and nowhere else.
| File | Purpose | Lines/Size |
|------|---------|------------|
| `README.md` | Entry point, quick-start, architecture diagram | 3,275 bytes |
| `prerequisites.md` | 6 concrete blocking items pre-deployment | 2,690 bytes |
| `docker-compose.yml` | Conduit + Postgres + optional Element Web | 1,427 bytes |
| `conduit.toml` | Base Conduit configuration template | 1,498 bytes |
| `.env.example` | Environment secrets template | 1,861 bytes |
| `deploy-matrix.sh` | One-command deployment orchestrator | 3,388 bytes |
| `host-readiness-check.sh` | Pre-flight validation script | 3,321 bytes |
| `caddy/Caddyfile` | Reverse-proxy rules for Caddy users | 1,612 bytes |
| `conduit/conduit.toml` | Advanced Conduit config (federation-ready) | 2,280 bytes |
| `conduit/docker-compose.yml` | Extended compose with replication | 1,469 bytes |
| `scripts/deploy-conduit.sh` | Low-level Conduit installer | 5,488 bytes |
| `docs/RUNBOOK.md` | Day-2 operations (backup, upgrade, health) | 3,412 bytes |
**Command for next deployer:**
```bash
cd infra/matrix
./host-readiness-check.sh # 1. verify target
# Edit conduit.toml + .env
./deploy-matrix.sh # 2. deploy
```
### 2. Operator Runbook — `docs/matrix-fleet-comms/`
Human-facing narrative for Alexander and operators.
| File | Purpose | Size |
|------|---------|------|
| `README.md` | Fleet communications authority map + onboarding | 7,845 bytes |
| `DEPLOYMENT_RUNBOOK.md` | Step-by-step operator playbook | 4,484 bytes |
---
## Legacy / Duplicate Paths (Do Not Edit — Reference Only)
The following directories contain **overlapping or superseded** material. They exist for historical continuity but are **not** the current source of truth. If you edit these, you create divergence.
| Path | Status | Note |
|------|--------|------|
| `deploy/matrix/` | 🔴 Superseded by `infra/matrix/` | Smaller subset; lacks host-readiness check |
| `deploy/conduit/` | 🔴 Superseded by `infra/matrix/scripts/` | `install.sh` + `health.sh` — good ideas ported into `infra/matrix/` |
| `matrix/` | 🔴 Superseded by `infra/matrix/` | Early docker-compose experiment |
| `docs/matrix-conduit/DEPLOYMENT.md` | 🔴 Superseded by `docs/matrix-fleet-comms/DEPLOYMENT_RUNBOOK.md` | |
| `docs/matrix-deployment.md` | 🔴 Superseded by `infra/matrix/prerequisites.md` + runbook | |
| `scaffold/matrix-conduit/` | 🔴 Superseded by `infra/matrix/` | Bootstrap + nginx configs; nginx approach not chosen |
> **House Rule**: New Matrix work must branch from `infra/matrix/` or `docs/matrix-fleet-comms/`. If a legacy file needs resurrection, migrate it into the authoritative tree and delete the old reference.
---
## Decision Blocker: #187
**#166 cannot proceed until [#187](../issues/187) is resolved.**
Ezra has produced a dedicated decision framework to make this a 5-minute choice rather than an architectural debate:
📄 **See**: [`docs/DECISION_FRAMEWORK_187.md`](DECISION_FRAMEWORK_187.md)
The framework recommends:
- **Host**: Timmy-Home bare metal (primary) or existing VPS
- **Domain**: `matrix.timmytime.net` (or sub-domain of existing fleet domain)
- **Proxy**: Caddy (simplest) or extend existing Traefik
- **TLS**: Let's Encrypt ACME HTTP-01 (port 80/443 open)
---
## Next Agent Checklist
If you are picking up #166:
1. [ ] Read `infra/matrix/README.md`
2. [ ] Read `docs/DECISION_FRAMEWORK_187.md`
3. [ ] Confirm resolution of #187 (host/domain/proxy chosen)
4. [ ] Run `infra/matrix/host-readiness-check.sh` on target host
5. [ ] Cut a feature branch; edit `infra/matrix/conduit.toml` and `.env`
6. [ ] Execute `infra/matrix/deploy-matrix.sh`
7. [ ] Verify federation with Matrix.org test server
8. [ ] Create operator room; invite Alexander
9. [ ] Post SITREP on #166 with proof-of-deployment
---
## Changelog
| Date | Change | Author |
|------|--------|--------|
| 2026-04-05 | Canonical index created; authoritative paths declared | Ezra |

View File

@@ -1,126 +0,0 @@
# Decision Framework: Matrix Host, Domain, and Proxy (#187)
> **Issue**: [#187](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/187) — Decide Matrix host, domain, and proxy prerequisites so #166 can deploy
> **Parent**: [#166](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/166) — Stand up Matrix/Conduit for human-to-fleet encrypted communication
> **Created**: 2026-04-05 by Ezra (burn mode)
> **Purpose**: Turn the #187 blocker into a checkbox. One recommendation, two alternatives, explicit trade-offs.
---
## Executive Summary
**Recommended Path (Option A)**
- **Host**: Existing Hermes VPS (`143.198.27.163` — already hosts Gitea, Bezalel, Allegro-Primus)
- **Domain**: `matrix.timmytime.net`
- **Proxy**: Caddy (dedicated to Matrix, auto-TLS, auto-federation headers)
- **TLS**: Let's Encrypt via Caddy (ports 80/443/8448 exposed)
**Why**: It reuses a known sovereign host, keeps comms infrastructure under one roof, and Caddy is the simplest path to working federation.
---
## Option A — Recommended: Hermes VPS + Caddy
### Host: Hermes VPS (`143.198.27.163`)
| Factor | Assessment |
|--------|------------|
| Sovereignty | ✅ Full root, no platform lock-in |
| Uptime | ✅ 24/7 VPS, better than home broadband |
| Existing load | ⚠️ Gitea + wizard gateways running; Conduit is lightweight (~200MB RAM) |
| Cost | ✅ Sunk cost — no new provider needed |
### Domain: `matrix.timmytime.net`
| Factor | Assessment |
|--------|------------|
| DNS control | ✅ `timmytime.net` is already under fleet control |
| Federation SRV | Simple A record + optional `_matrix._tcp` SRV record |
| TLS cert | Caddy auto-provisions for this subdomain |
### Proxy: Caddy
| Factor | Assessment |
|--------|------------|
| TLS automation | ✅ Built-in ACME, auto-renewal |
| Federation headers | ✅ Easy `.well-known` + SRV support |
| Config complexity | ✅ Single `Caddyfile`, no label magic |
| Traefik conflict | None — Caddy binds its own ports directly |
### Required Actions for Option A
1. Delegate `matrix.timmytime.net` A record → `143.198.27.163`
2. Open VPS firewall: `80`, `443`, `8448` inbound
3. Clone `timmy-config` to VPS
4. `cd infra/matrix && ./host-readiness-check.sh`
5. Edit `conduit.toml``server_name = "matrix.timmytime.net"`
6. Run `./deploy-matrix.sh`
---
## Option B — Conservative: Timmy-Home Bare Metal + Traefik
| Factor | Assessment |
|--------|------------|
| Host | Timmy-Home Mac Mini / server |
| Domain | `matrix.home.timmytime.net` |
| Proxy | Existing Traefik instance |
| Pros | Full physical sovereignty; no cloud dependency |
| Cons | Home IP dynamic (requires DDNS); port-forwarding dependency; power/network outages |
| Verdict | 🔶 Viable backup, not primary |
---
## Option C — Fast but Costly: DigitalOcean Droplet
| Factor | Assessment |
|--------|------------|
| Host | Fresh `$6-12/mo` Ubuntu droplet |
| Domain | `matrix.timmytime.net` |
| Proxy | Caddy or Nginx |
| Pros | Clean slate, static IP, easy snapshot backups |
| Cons | New monthly bill, another host to patch/monitor |
| Verdict | 🔶 Overkill while Hermes VPS has headroom |
---
## Comparative Matrix
| Criterion | Option A (Recommended) | Option B (Home) | Option C (DO) |
|-----------|------------------------|-----------------|---------------|
| Speed to deploy | 🟢 Fast | 🟡 Medium | 🟡 Medium |
| Sovereignty | 🟢 High | 🟢 Highest | 🟢 High |
| Reliability | 🟢 Good | 🔴 Variable | 🟢 Good |
| Cost | 🟢 $0 extra | 🟢 $0 extra | 🔴 +$6-12/mo |
| Operational load | 🟢 Low | 🟡 Medium | 🔴 Higher |
| Federation ease | 🟢 Caddy simple | 🟡 Traefik doable | 🟢 Caddy simple |
---
## Port & TLS Requirements (All Options)
| Port | Direction | Purpose | Notes |
|------|-----------|---------|-------|
| `80` | Inbound | ACME challenge + `.well-known` redirect | Must be reachable from internet |
| `443` | Inbound | Client HTTPS (Element, mobile apps) | Caddy/Traefik terminates TLS |
| `8448` | Inbound | Federation (server-to-server) | Matrix spec default; can proxy from 443 but 8448 is safest |
| `6167` | Internal | Conduit replication (optional) | Not needed for single-node |
**TLS Path**: Let's Encrypt HTTP-01 challenge (no manual cert purchase).
---
## The Actual Checklist to Close #187
- [ ] **Alexander selects one option** (A recommended)
- [ ] Domain/subdomain is chosen and confirmed available
- [ ] Target host IP is known and firewall ports are confirmed open
- [ ] Reverse proxy choice is locked
- [ ] #166 is updated with the decision
- [ ] Allegro or Ezra is tasked with live deployment
**If you check these 6 boxes, #166 is unblocked.**
---
## Suggested Comment to Resolve #187
> "Go with Option A. Domain: `matrix.timmytime.net`. Host: Hermes VPS. Proxy: Caddy. @ezra or @allegro deploy when ready."
That is all that is required.

View File

@@ -1,199 +0,0 @@
# Communication Authority Map
Status: doctrine for #175
Parent epic: #173
Related issues:
- #165 NATS internal bus
- #166 Matrix/Conduit operator communication
- #174 Nostr/Nostur operator edge
- #163 sovereign keypairs / identity
## Why this exists
We do not want communication scattered across lost channels.
The system may expose multiple communication surfaces, but work authority must not fragment with them.
A message can arrive from several places.
Task truth cannot.
This document defines which surface is authoritative for what, how operator messages enter the system, and how Matrix plus Nostr/Nostur can coexist without creating parallel hidden queues.
## Core principle
One message may have many transport surfaces.
One piece of work gets one execution truth.
That execution truth is Gitea.
If a command or request matters to the fleet, it must become a visible Gitea artifact:
- issue
- issue comment
- PR comment
- assignee/label change
- linked proof artifact
No chat surface is allowed to become a second hidden task database.
## Authority layers
### 1. Gitea — execution truth
Authoritative for:
- task state
- issue ownership
- PR state
- review state
- visible decision trail
- proof links and artifacts
Rules:
- if work is actionable, it must exist in Gitea
- if state changes, the change must be reflected in Gitea
- if chat and Gitea disagree, Gitea wins until corrected visibly
### 2. NATS — internal agent bus
Authoritative for:
- fast machine-to-machine transport only
Not authoritative for:
- task truth
- operator truth
- final queue state
Rules:
- NATS moves signals, not ownership truth
- durable work still lands in Gitea
- request/reply and heartbeats may live here without becoming the task system
### 3. Matrix/Conduit — primary private operator command surface
Authoritative for:
- private human-to-fleet conversation
- rich command context
- operational chat that should not be public
Not authoritative for:
- final task state
- hidden work queues
Rules:
- Matrix is the primary private operator room
- any command that creates or mutates work must be mirrored into Gitea
- Matrix can discuss work privately, but cannot be the only place where the work exists
- if a command remains chat-only, it is advisory, not execution truth
### 4. Nostr/Nostur — sovereign operator edge
Authoritative for:
- operator identity-linked ingress
- portable/mobile sovereign access
- public or semi-public notices if intentionally used that way
- emergency or lightweight operator signaling
Not authoritative for:
- internal fleet transport
- hidden task state
- long-lived queue truth
Rules:
- Nostur is a real operator layer, not a toy side-channel
- commands received via Nostr/Nostur must be normalized into Gitea before they are considered active work
- if private discussion is needed after Nostr ingress, continue in Matrix while keeping Gitea as visible task truth
- Nostr/Nostur should preserve sovereign identity advantages without becoming an alternate invisible work tracker
### 5. Telegram — legacy bridge only
Authoritative for:
- nothing new
Rules:
- Telegram is legacy/bridge until sunset
- no new doctrine should make Telegram the permanent backbone
- if Telegram receives work during migration, the work still gets mirrored into Gitea and then into the current primary surfaces
## Ingress rules
### Rule A: every actionable operator message gets normalized
If an operator message from Matrix, Nostr/Nostur, or Telegram asks for real work, the system must do one of the following:
- create a new Gitea issue
- append to the correct existing issue as a comment
- explicitly reject the message as non-actionable
- route it to a coordinator for clarification before any work begins
### Rule B: no hidden queue mutation
Refreshing a chat room, reading a relay event, or polling a transport must not silently create work.
The transition from chat to work must be explicit and visible.
### Rule C: one work item, many mirrors allowed
A message may be mirrored across:
- Matrix
- Nostr/Nostur
- Telegram during migration
- local notifications
But all mirrors must point back to the same Gitea work object.
### Rule D: coordinator-first survives transport changes
Timmy and Allegro remain the coordinators.
Changing the transport does not remove their authority to:
- classify urgency
- decide routing
- demand proof
- collapse duplicates
- escalate only what Alexander should actually see
## Recommended operator experience
### Matrix
Use for:
- primary private conversation with the fleet
- ongoing task discussion
- handoff and clarification
- richer context than a short mobile note
### Nostur
Use for:
- sovereign mobile/operator ingress
- identity-linked quick commands
- lightweight acknowledgements
- emergency input when Matrix is not the best surface
Working rule:
- Nostur gets you into the system
- Matrix carries the private conversation
- Gitea holds the work truth
## Anti-scatter policy
Forbidden patterns:
- a task exists only in a Matrix room
- a task exists only in a Nostr DM or note
- a Telegram thread contains work nobody copied into Gitea
- different channels describe the same work with different owners or statuses
- an agent acts on Nostr/Matrix chatter without a visible work object when the task is non-trivial
Required pattern:
- every meaningful task gets one canonical Gitea object
- all channels point at or mirror that object
- coordinators keep channel drift collapsed, not multiplied
## Minimum implementation path
1. Matrix/Conduit becomes the primary private operator surface (#166)
2. Nostr/Nostur becomes the sovereign operator edge (#174)
3. NATS remains internal bus only (#165)
4. every ingress path writes or links to Gitea execution truth
5. Telegram is reduced to bridge/legacy during migration
## Acceptance criteria
- [ ] Matrix, Nostr/Nostur, NATS, Gitea, and Telegram each have an explicit role
- [ ] Gitea is named as the sole execution-truth surface
- [ ] Nostur is included as a legitimate operator layer, not ignored
- [ ] Nostur/Matrix ingress rules explicitly forbid shadow task state
- [ ] this doctrine makes it harder for work to get lost across channels

View File

@@ -1,136 +0,0 @@
# Matrix/Conduit Deployment Guide
Executable scaffold for standing up a sovereign Matrix homeserver as the human-to-fleet command surface.
## Architecture Summary
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Alexander │────▶│ Nginx Proxy │────▶│ Conduit │
│ (Element/Web) │ │ 443 / 8448 │ │ Homeserver │
└─────────────────┘ └──────────────────┘ └─────────────────┘
┌─────────────────┐
│ SQLite/Postgres│
│ (state/media) │
└─────────────────┘
```
## Prerequisites
| Requirement | How to Verify | Status |
|-------------|---------------|--------|
| VPS with 2GB+ RAM | `free -h` | ⬜ |
| Static IP address | `curl ifconfig.me` | ⬜ |
| Domain with A record | `dig matrix.fleet.tld` | ⬜ |
| Ports 443/8448 open | `sudo ss -tlnp | grep -E "443|8448"` | ⬜ |
| TLS certificate (Let's Encrypt) | `sudo certbot certificates` | ⬜ |
| Docker + docker-compose | `docker --version` | ⬜ |
## Quickstart
### 1. Host Preparation
```bash
# Ubuntu/Debian
sudo apt update && sudo apt install -y docker.io docker-compose-plugin nginx certbot
# Open ports
sudo ufw allow 443/tcp
sudo ufw allow 8448/tcp
```
### 2. DNS Configuration
```
# A record
matrix.fleet.tld. A <YOUR_SERVER_IP>
# SRV for federation (optional but recommended)
_matrix._tcp.fleet.tld. SRV 10 0 8448 matrix.fleet.tld.
```
### 3. TLS Certificate
```bash
sudo certbot certonly --standalone -d matrix.fleet.tld
```
### 4. Deploy Conduit
```bash
# Edit conduit.toml: set server_name to your domain
nano conduit.toml
# Start stack
docker compose up -d
# Verify
docker logs -f conduit-homeserver
```
### 5. Nginx Configuration
```bash
sudo cp nginx-matrix.conf /etc/nginx/sites-available/matrix
sudo ln -s /etc/nginx/sites-available/matrix /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
```
### 6. Bootstrap Accounts
1. Open Element at `https://matrix.fleet.tld`
2. Register admin account first (while `allow_registration = true`)
3. Set admin in `conduit.toml`, restart
4. Disable registration after setup
### 7. Fleet Rooms
```bash
# Fill ACCESS_TOKEN in bootstrap.sh
curl -X POST "https://matrix.fleet.tld/_matrix/client/r0/login" \
-d '{"type":"m.login.password","user":"alexander","password":"YOUR_PASS"}'
# Run bootstrap
chmod +x bootstrap.sh
./bootstrap.sh
```
## Federation Verification
```bash
# Check server discovery
curl https://matrix.fleet.tld/.well-known/matrix/server
curl https://matrix.fleet.tld/.well-known/matrix/client
# Check federation
curl https://matrix.fleet.tld:8448/_matrix/key/v2/server
```
## Telegram Bridge (Future)
To bridge Telegram groups to Matrix:
```yaml
# Add to docker-compose.yml
telegram-bridge:
image: dock.mau.dev/mautrix/telegram:latest
volumes:
- ./bridge-config.yaml:/data/config.yaml
- telegram_bridge:/data
```
See: https://docs.mau.fi/bridges/python/telegram/setup-docker.html
## Security Checklist
- [ ] Registration disabled after initial setup
- [ ] Admin list restricted
- [ ] Strong admin passwords
- [ ] Automatic security updates enabled
- [ ] Backups configured (conduit_data volume)
## Troubleshooting
| Issue | Cause | Fix |
|-------|-------|-----|
| Federation failures | DNS/SRV records | Verify `dig _matrix._tcp.fleet.tld SRV` |
| SSL errors | Certificate mismatches | Verify cert covers matrix.fleet.tld |
| 502 Bad Gateway | Conduit not listening | Check `docker ps`, verify port 6167 |
---
Generated by Ezra | Burn Mode | 2026-04-05

View File

@@ -1,86 +0,0 @@
# Matrix/Conduit Deployment Guide
> **Parent**: timmy-config#166
> **Child**: timmy-config#183
> **Created**: 2026-04-05 by Ezra burn-mode triage
## Deployment Prerequisites
### 1. Host Selection Matrix
| Option | Pros | Cons | Recommendation |
|--------|------|------|----------------|
| Timmy-Home bare metal | Full sovereignty, existing Traefik | Single point of failure, home IP | **PRIMARY** |
| DigitalOcean VPS | Static IP, offsite | Monthly cost, external dependency | BACKUP |
| RunPod GPU instance | Already in fleet | Ephemeral, not for persistence | NOT SUITABLE |
### 2. Port Requirements
| Port | Purpose | Inbound Required |
|------|---------|------------------|
| 8448 | Federation (server-to-server) | Yes |
| 443 | Client HTTPS | Yes (via Traefik) |
| 80 | ACME HTTP-01 challenge | Yes (redirects to 443) |
| 6167 | Conduit replication (optional) | Internal only |
### 3. Reverse Proxy Assumptions (Traefik)
Existing `timmy-home` Traefik instance can route Matrix traffic:
```yaml
# docker-compose.yml labels for Conduit
labels:
- "traefik.enable=true"
- "traefik.http.routers.matrix.rule=Host(`matrix.tactical.local`)"
- "traefik.http.routers.matrix.tls.certresolver=letsencrypt"
- "traefik.http.services.matrix.loadbalancer.server.port=6167"
# Federation SRV delegation
- "traefik.tcp.routers.matrix-federation.rule=HostSNI(`*`)"
- "traefik.tcp.routers.matrix-federation.entrypoints=federation"
```
### 4. DNS Requirements
```
# A records
matrix.tactical.local A <timmy-home-ip>
# SRV records for federation
_matrix._tcp.tactical.local SRV 10 0 8448 matrix.tactical.local
```
### 5. Database Choice
| Option | When to Use |
|--------|-------------|
| SQLite (default) | < 100 users, < 10 rooms, single-node |
| PostgreSQL | Scale, backups, multi-node potential |
**Recommendation**: Start with SQLite. Migrate to PostgreSQL only if federation grows.
### 6. Storage Requirements
- Conduit binary: ~50MB
- Database (SQLite): ~100MB initial, grows with media
- Media repo: Plan for 10GB (images, avatars, room assets)
## Blocking Prerequisites Checklist
- [ ] **Host**: Confirm Timmy-Home static IP or dynamic DNS
- [ ] **Ports**: Verify 8448, 443, 80 not blocked by ISP
- [ ] **Traefik**: Confirm federation TCP entrypoint configured
- [ ] **DNS**: SRV records creatable at domain registrar
- [ ] **SSL**: Let's Encrypt ACME configured in Traefik
- [ ] **Backup**: Volume mount strategy for SQLite persistence
## Next Steps
1. Complete prerequisites checklist above
2. Generate `conduit-config.toml` (see `matrix/conduit-config.toml`)
3. Create `docker-compose.yml` with Traefik labels
4. Deploy test room with @ezra + Alexander
5. Verify client connectivity (Element web/iOS)
6. Document Telegram→Matrix migration plan
---
*This document lowers #166 from fuzzy epic to executable deployment steps.*

View File

@@ -1,195 +0,0 @@
# Matrix/Conduit Deployment Runbook
# Issue #166 — Human-to-Fleet Encrypted Communication
# Created: Ezra, Burn Mode | 2026-04-05
## Pre-Flight Checklist
Before running this playbook, ensure:
- [ ] Host provisioned with ports 80/443/8448 open
- [ ] Domain `matrix.timmytime.net` delegated to host IP
- [ ] Docker + Docker Compose installed
- [ ] `infra/matrix/` scaffold cloned to host
## Quick Start (One Command)
```bash
cd infra/matrix && ./deploy.sh --host $(curl -s ifconfig.me) --domain matrix.timmytime.net
```
## Manual Deployment Steps
### 1. Host Preparation
```bash
# Update system
sudo apt update && sudo apt upgrade -y
# Install Docker
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker
# Install Docker Compose
sudo curl -L "https://github.com/docker/compose/releases/download/v2.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
```
### 2. Domain Configuration
Ensure DNS A record:
```
matrix.timmytime.net → <HOST_IP>
```
### 3. Scaffold Deployment
```bash
git clone http://143.198.27.163:3000/Timmy_Foundation/timmy-config.git
cd timmy-config/infra/matrix
```
### 4. Environment Configuration
```bash
# Copy and edit environment
cp .env.template .env
nano .env
# Required values:
# DOMAIN=matrix.timmytime.net
# POSTGRES_PASSWORD=<generate_strong_password>
# CONDUIT_MAX_REQUEST_SIZE=20000000
```
### 5. Launch Services
```bash
# Start Conduit + Element Web
docker-compose up -d
# Verify health
docker-compose ps
docker-compose logs -f conduit
```
### 6. Federation Test
```bash
# Test .well-known delegation
curl https://matrix.timmytime.net/.well-known/matrix/server
curl https://matrix.timmytime.net/.well-known/matrix/client
# Test federation API
curl https://matrix.timmytime.net:8448/_matrix/key/v2/server
```
## Post-Deployment: Operator Onboarding
### Create Admin Account
```bash
# Via Conduit admin API (first user = admin automatically)
curl -X POST "https://matrix.timmytime.net/_matrix/client/r0/register" \
-H "Content-Type: application/json" \
-d '{
"username": "alexander",
"password": "<secure_password>",
"auth": {"type": "m.login.dummy"}
}'
```
### Fleet Room Bootstrap
```bash
# Create rooms via API (using admin token)
export TOKEN=$(cat ~/.matrix_admin_token)
# Operators room
curl -X POST "https://matrix.timmytime.net/_matrix/client/r0/createRoom" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Operators",
"topic": "Human-to-fleet command surface",
"preset": "private_chat",
"encryption": true
}'
# Fleet General room
curl -X POST "https://matrix.timmytime.net/_matrix/client/r0/createRoom" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Fleet General",
"topic": "All wizard houses — general coordination",
"preset": "public_chat",
"encryption": true
}'
```
## Troubleshooting
### Port 8448 Blocked
```bash
# Verify federation port
nc -zv matrix.timmytime.net 8448
# Check firewall
sudo ufw status
sudo ufw allow 8448/tcp
```
### SSL Certificate Issues
```bash
# Force Caddy certificate refresh
docker-compose exec caddy rm -rf /data/caddy/certificates
docker-compose restart caddy
```
### Conduit Database Migration
```bash
# Backup before migration
docker-compose exec conduit sqlite3 /var/lib/matrix-conduit/conduit.db ".backup /backup/conduit-$(date +%Y%m%d).db"
```
## Telegram → Matrix Cutover Plan
### Phase 0: Parallel (Week 1-2)
- Matrix rooms operational
- Telegram still primary
- Fleet agents join both
### Phase 1: Operator Verification (Week 3)
- Alexander confirms Matrix reliability
- Critical alerts dual-posted
### Phase 2: Fleet Gateway Migration (Week 4)
- Hermes gateway adds Matrix platform
- Telegram becomes fallback
### Phase 3: Telegram Deprecation (Week 6-8)
- 30-day overlap period
- Final cutover announced
- Telegram bots archived
## Verification Commands
```bash
# Health check
curl -s https://matrix.timmytime.net/_matrix/client/versions | jq .
# Federation check
curl -s https://federationtester.matrix.org/api/report?server_name=matrix.timmytime.net | jq '.FederationOK'
# Element Web check
curl -s -o /dev/null -w "%{http_code}" https://element.timmytime.net
```
---
**Artifact**: `docs/matrix-fleet-comms/DEPLOYMENT_RUNBOOK.md`
**Issue**: #166
**Author**: Ezra | Burn Mode | 2026-04-05

View File

@@ -1,240 +0,0 @@
# Execution Architecture KT — Matrix/Conduit Human-to-Fleet Comms
**Issue**: [#166](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/166)
**Blocker**: [#187](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/187) — Host/domain/proxy decisions
**Scaffold**: [#183](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/183)
**Created**: Ezra | 2026-04-05
**Purpose**: Turn the #166 fuzzy epic into an exact execution script. Once #187 closes, follow this KT verbatim.
---
## Executive Summary
This document is the **knowledge transfer** from architecture (#183) to execution (#166). It assumes the decision framework in `docs/DECISION_FRAMEWORK_187.md` has been accepted (recommended: **Option A — Hermes VPS + Caddy + matrix.timmytime.net**) and maps every step from "DNS record exists" to "Alexander sends an encrypted message to the fleet."
---
## Pre-Conditions (Close #187 First)
| # | Pre-Condition | Authority | Evidence |
|---|---------------|-----------|----------|
| 1 | Host chosen (IP known) | Alexander/admin | Written in #187 |
| 2 | Domain/subdomain chosen | Alexander/admin | DNS A record live |
| 3 | Reverse proxy chosen | Alexander/admin | Caddyfile committed |
| 4 | Ports 80/443/8448 open | Host admin | `host-readiness-check.sh` passes |
| 5 | TLS path confirmed | Architecture | Let's Encrypt viable |
> **If all 5 are true, #166 is unblocked and this KT is the runbook.**
---
## Phase 1: Host Prep (30 minutes)
### 1.1 Clone Repo on Target Host
```bash
ssh root@<HOST_IP>
git clone https://forge.alexanderwhitestone.com/Timmy_Foundation/timmy-config.git /opt/timmy-config
cd /opt/timmy-config/infra/matrix
```
### 1.2 Verify Host Readiness
```bash
./host-readiness-check.sh
```
Expected: all checks green (Docker, ports, disk, RAM).
### 1.3 Configure Environment
```bash
cp .env.example .env
# Edit .env:
# CONDUIT_SERVER_NAME=matrix.timmytime.net
# CONDUIT_ALLOW_REGISTRATION=true # ONLY for bootstrap
```
---
## Phase 2: Conduit Deployment (15 minutes)
### 2.1 One-Command Deploy
```bash
./deploy-matrix.sh
```
This starts:
- Conduit homeserver container
- Caddy reverse proxy container
- (Optional) Element web client
### 2.2 Verify Health
```bash
curl -s https://matrix.timmytime.net/_matrix/client/versions | jq .
```
Expected: JSON with `versions` array.
### 2.3 Verify Federation
```bash
curl -s https://matrix.timmytime.net/.well-known/matrix/server
```
Expected: `{"m.server": "matrix.timmytime.net:443"}`
---
## Phase 3: Fleet Bootstrap — Accounts & Rooms (30 minutes)
### 3.1 Create Admin Account
**Enable registration temporarily** in `.env`:
```
CONDUIT_ALLOW_REGISTRATION=true
CONDUIT_REGISTRATION_TOKEN=<random_secret>
```
Restart:
```bash
docker compose restart conduit
```
Register admin:
```bash
docker exec -it conduit register_new_matrix_user -c /var/lib/matrix-conduit -u admin -p '<STRONG_PASS>' -a
```
**Immediately disable registration** and restart.
### 3.2 Create Fleet Accounts
| Account | Purpose | Created By |
|---------|---------|------------|
| `@admin:matrix.timmytime.net` | Server administration | deploy script |
| `@alexander:matrix.timmytime.net` | Human operator | admin |
| `@timmy:matrix.timmytime.net` | Coordinator bot | admin |
| `@ezra:matrix.timmytime.net` | Archivist bot | admin |
| `@allegro:matrix.timmytime.net` | Dispatch bot | admin |
| `@bezalel:matrix.timmytime.net` | Dev bot | admin |
| `@gemini:matrix.timmytime.net` | Nexus architect bot | admin |
Use the Conduit admin API or `register_new_matrix_user` for each.
### 3.3 Create Fleet Rooms
| Room Alias | Purpose | Encryption |
|------------|---------|------------|
| `#fleet-ops:matrix.timmytime.net` | Operator commands | ✅ E2E |
| `#fleet-intel:matrix.timmytime.net` | Deep Dive briefings | ✅ E2E |
| `#fleet-social:matrix.timmytime.net` | General chat | ✅ E2E |
| `#fleet-alerts:matrix.timmytime.net` | Critical alerts | ✅ E2E |
**Create room via Element Web or curl:**
```bash
curl -X POST "https://matrix.timmytime.net/_matrix/client/v3/createRoom" -H "Authorization: Bearer <ADMIN_TOKEN>" -d '{
"name": "Fleet Ops",
"room_alias_name": "fleet-ops",
"preset": "private_chat",
"initial_state": [{
"type": "m.room.encryption",
"content": {"algorithm": "m.megolm.v1.aes-sha2"}
}]
}'
```
### 3.4 Invite Fleet Members
Invite each bot/user to the appropriate rooms. For `#fleet-ops`, restrict to `@alexander`, `@timmy`, `@ezra`, `@allegro`.
---
## Phase 4: Wizard Onboarding Procedure (30 minutes)
Each wizard house needs:
1. **Matrix credentials** (username + password + recovery key)
2. **Client recommendation** — Element Desktop or Fluffychat
3. **Room memberships** — invite to relevant fleet rooms
4. **Encryption verification** — verify keys with Alexander
### Onboarding Checklist per Wizard
- [ ] Account created and credentials stored in vault
- [ ] Client installed and signed in
- [ ] Joined `#fleet-ops` and `#fleet-intel`
- [ ] E2E verification completed with `@alexander`
- [ ] Test message sent and received
---
## Phase 5: Telegram → Matrix Cutover Architecture
### 5.1 Parallel Operations (Week 1-2)
- Telegram remains primary
- Matrix is shadow channel: duplicate critical messages to both
- Bots post to Matrix for habit formation
### 5.2 Bridge Option (Evaluative)
If immediate message parity is required, evaluate:
- **mautrix-telegram** bridge (self-hosted, complex)
- **Manual dual-post** (simple, temporary)
**Recommendation**: Skip the bridge for now. Dual-post via bot logic is lower risk.
### 5.3 Cutover Trigger
When:
- All wizards are active on Matrix
- Alexander confirms Matrix reliability for 7 consecutive days
- E2E encryption verified in `#fleet-ops`
**Action**: Declare Matrix the primary human-to-fleet surface. Telegram becomes fallback only.
---
## Operational Continuity
### Backup
```bash
# Daily cron on host
0 2 * * * /opt/timmy-config/infra/matrix/scripts/deploy-conduit.sh backup
```
### Monitoring
```bash
# Health check every 5 minutes
*/5 * * * * /opt/timmy-config/infra/matrix/scripts/deploy-conduit.sh status || alert
```
### Upgrade Path
1. Pull latest `timmy-config`
2. Run `./host-readiness-check.sh`
3. `docker compose pull && docker compose up -d`
---
## Acceptance Criteria Mapping
| #166 Criterion | How This KT Satisfies It | Phase |
|----------------|--------------------------|-------|
| Deploy Conduit homeserver | `deploy-matrix.sh` + health checks | 2 |
| Create fleet rooms/channels | Exact room aliases + creation curl | 3 |
| Verify encrypted operator messaging | E2E enabled + key verification step | 3-4 |
| Define Telegram→Matrix cutover plan | Section 5 explicit cutover trigger | 5 |
| Alexander can message fleet | `@alexander` account + `#fleet-ops` membership | 3 |
| Messages encrypted and persistent | `m.room.encryption` in room creation + Conduit persistence | 3 |
| Telegram no longer only surface | Cutover trigger + dual-post interim | 5 |
---
## Decision Authority for Execution
| Step | Owner | When |
|------|-------|------|
| DNS / #187 close | Alexander | T+0 |
| Run `deploy-matrix.sh` | Allegro or Ezra | T+0 (15 min) |
| Create accounts/rooms | Allegro or Ezra | T+15 (30 min) |
| Onboard wizards | Individual agents + Alexander | T+45 (ongoing) |
| Cutover declaration | Alexander | T+7 days (minimum) |
---
## References
- Scaffold: [`infra/matrix/`](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/src/branch/main/infra/matrix)
- ADRs: [`infra/matrix/docs/adr/`](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/src/branch/main/infra/matrix/docs/adr)
- Decision Framework: [`docs/DECISION_FRAMEWORK_187.md`](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/src/branch/main/docs/DECISION_FRAMEWORK_187.md)
- Operational Runbook: [`infra/matrix/docs/RUNBOOK.md`](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/src/branch/main/infra/matrix/docs/RUNBOOK.md)
---
**Ezra Sign-off**: This KT removes all ambiguity from #166. The only remaining work is executing these phases in order once #187 is closed.
— Ezra, Archivist
2026-04-05

View File

@@ -1,271 +0,0 @@
# Matrix/Conduit Fleet Communications
**Parent Issues**: [#166](https://gitea.timmy/time/Timmy_Foundation/timmy-config/issues/166) | [#183](https://gitea.timmy/time/Timmy_Foundation/timmy-config/issues/183)
**Status**: Architecture Complete → Implementation Ready
**Owner**: @ezra (architect) → TBD (implementer)
**Created**: 2026-04-05
---
## Purpose
Fulfill [Son of Timmy Commandment 6](https://gitea.timmy/time/Timmy_Foundation/timmy-config/blob/main/son-of-timmy.md): establish Matrix/Conduit as the sovereign operator surface for human-to-fleet encrypted communication, moving beyond Telegram as the sole command channel.
---
## Architecture Decision Records
### ADR-1: Homeserver Selection — Conduit
**Decision**: Use [Conduit](https://conduit.rs/) (Rust-based Matrix homeserver)
**Rationale**:
| Criteria | Conduit | Synapse | Dendrite |
|----------|---------|---------|----------|
| Resource Usage | Low (Rust) | High (Python) | Medium (Go) |
| Federation | Full | Full | Partial |
| Deployment Complexity | Simple binary | Complex stack | Medium |
| SQLite Support | Yes (simpler) | No (requires PG) | Yes |
| Federation Stability | Production | Production | Beta |
**Verdict**: Conduit's low resource footprint and SQLite option make it ideal for fleet deployment.
### ADR-2: Host Selection
**Decision**: Deploy on existing Gitea VPS (143.198.27.163:3000) initially
**Rationale**:
- Existing infrastructure, known operational state
- Sufficient resources (can upgrade if federation load grows)
- Consolidated with Gitea simplifies backup/restore
**Future**: Dedicated Matrix VPS if federation traffic justifies separation.
### ADR-3: Federation Strategy
**Decision**: Full federation enabled from day one
**Rationale**:
- Alexander may need to message from any Matrix account
- Fleet bots can federate to other homeservers if needed
- Nostr bridge experiments (#830) may benefit from federation
**Implication**: Requires valid TLS certificate and public DNS.
---
## Deployment Scaffold
### Directory Structure
```
/opt/conduit/
├── conduit # Binary
├── conduit.toml # Configuration
├── data/ # SQLite + media (backup target)
│ ├── conduit.db
│ └── media/
├── logs/ # Rotated logs
└── scripts/ # Operational helpers
├── backup.sh
└── rotate-logs.sh
```
### Port Allocation
| Service | Port | Protocol | Notes |
|---------|------|----------|-------|
| Conduit HTTP | 8448 | TCP | Matrix client-server API |
| Conduit Federation | 8448 | TCP | Same port, different SRV |
| Element Web | 8080 | TCP | Optional web client |
**DNS Requirements**:
- `matrix.timmy.foundation` → A record to VPS IP
- `_matrix._tcp.timmy.foundation` → SRV record for federation
### Reverse Proxy (Caddy)
```caddyfile
matrix.timmy.foundation {
reverse_proxy localhost:8448
header {
X-Frame-Options DENY
X-Content-Type-Options nosniff
}
tls {
# Let's Encrypt automatic
}
}
```
### Conduit Configuration (conduit.toml)
```toml
[global]
server_name = "timmy.foundation"
database_path = "/opt/conduit/data/conduit.db"
port = 8448
max_request_size = 20000000 # 20MB for file uploads
[registration]
# Closed registration - admin creates accounts
enabled = false
[ federation]
enabled = true
disabled_servers = []
[ media ]
max_file_size = 50000000 # 50MB
max_media_size = 100000000 # 100MB total cache
[ retention ]
enabled = true
default_room_retention = "30d"
```
---
## Prerequisites Checklist
### Infrastructure
- [ ] DNS A record: `matrix.timmy.foundation` → 143.198.27.163
- [ ] DNS SRV record: `_matrix._tcp.timmy.foundation` → 0 0 8448 matrix.timmy.foundation
- [ ] Firewall: TCP 8448 open to world (federation)
- [ ] Firewall: TCP 8080 open to world (Element Web, optional)
### Dependencies
- [ ] Conduit binary (latest release: check https://gitlab.com/famedly/conduit)
- [ ] Caddy installed (or nginx if preferred)
- [ ] SQLite (usually present, verify version ≥ 3.30)
- [ ] systemd (for service management)
### Accounts (Bootstrap)
- [ ] `@admin:timmy.foundation` — Server admin
- [ ] `@alexander:timmy.foundation` — Operator primary
- [ ] `@ezra:timmy.foundation` — Archivist bot
- [ ] `@timmy:timmy.foundation` — Coordinator bot
### Rooms (Bootstrap)
- [ ] `#fleet-ops:timmy.foundation` — Operator-to-fleet command channel
- [ ] `#fleet-intel:timmy.foundation` — Intelligence sharing
- [ ] `#fleet-social:timmy.foundation` — General chat
---
## Implementation Phases
### Phase 1: Infrastructure (Est: 2 hours)
1. Create DNS records
2. Open firewall ports
3. Download Conduit binary
4. Create directory structure
### Phase 2: Deployment (Est: 2 hours)
1. Write conduit.toml
2. Create systemd service
3. Configure Caddy reverse proxy
4. Start Conduit, verify health
### Phase 3: Bootstrap (Est: 1 hour)
1. Create admin account via CLI
2. Create user accounts
3. Create rooms, set permissions
4. Verify end-to-end encryption
### Phase 4: Migration Planning (Est: 4 hours)
1. Map Telegram channels to Matrix rooms
2. Design bridge architecture (if needed)
3. Create cutover timeline
4. Document operator onboarding
---
## Operational Runbooks
### Backup
```bash
#!/bin/bash
# /opt/conduit/scripts/backup.sh
BACKUP_DIR="/backups/conduit/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
# Stop Conduit briefly for consistent snapshot
systemctl stop conduit
cp /opt/conduit/data/conduit.db "$BACKUP_DIR/"
cp /opt/conduit/conduit.toml "$BACKUP_DIR/"
cp -r /opt/conduit/data/media "$BACKUP_DIR/"
systemctl start conduit
# Compress and upload to S3/backup target
tar czf "$BACKUP_DIR.tar.gz" -C "$BACKUP_DIR" .
# aws s3 cp "$BACKUP_DIR.tar.gz" s3://timmy-backups/conduit/
```
### Account Creation
```bash
# As admin, create new user
curl -X POST \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"username":"newuser","password":"secure_password_123"}' \
https://matrix.timmy.foundation/_matrix/client/v3/register
```
### Health Check
```bash
#!/bin/bash
# /opt/conduit/scripts/health.sh
curl -s https://matrix.timmy.foundation/_matrix/client/versions | jq .
```
---
## Cross-Issue Linkages
| Issue | Relationship | Action |
|-------|--------------|--------|
| #166 | Parent epic | This scaffold enables #166 execution |
| #183 | Scaffold child | This document fulfills #183 acceptance criteria |
| #830 | Deep Dive | Matrix rooms can receive #830 intelligence briefings |
| #137 | Related | Verify no conflict with existing comms work |
| #138 | Related | Verify no conflict with Nostr bridge |
| #147 | Related | Check if Matrix replaces or supplements existing plans |
---
## Artifacts Created
| File | Purpose |
|------|---------|
| `docs/matrix-fleet-comms/README.md` | This architecture document |
| `deploy/conduit/conduit.toml` | Production configuration |
| `deploy/conduit/conduit.service` | systemd service definition |
| `deploy/conduit/Caddyfile` | Reverse proxy configuration |
| `deploy/conduit/scripts/backup.sh` | Backup automation |
| `deploy/conduit/scripts/health.sh` | Health check script |
---
## Next Actions
1. **DNS**: Create `matrix.timmy.foundation` A and SRV records
2. **Firewall**: Open TCP 8448 on VPS
3. **Install**: Download and configure Conduit
4. **Bootstrap**: Create initial accounts and rooms
5. **Onboard**: Add Alexander, test end-to-end encryption
6. **Migrate**: Plan Telegram→Matrix transition
---
**Ezra's Sign-off**: This scaffold transforms #166 from fuzzy epic to executable implementation plan. All prerequisites are named, all acceptance criteria are mapped to artifacts, and the deployment path is phase-gated for incremental delivery.
— Ezra, Archivist
2026-04-05

View File

@@ -1,187 +0,0 @@
# Nostur Operator Edge
Status: doctrine and implementation path for #174
Parent epic: #173
Related issues:
- #166 Matrix/Conduit primary operator surface
- #175 Communication authority map
- #165 NATS internal bus
- #163 sovereign keypairs / identity
## Goal
Make Nostur a real operator-facing layer for Alexander without letting Nostr become a shadow task system.
Nostur is valuable because it gives the operator a sovereign, identity-linked mobile surface.
That does not mean Nostr should become the place where work lives.
## Design rule
Nostur is an ingress layer.
Gitea is execution truth.
Matrix is the private conversation surface.
NATS is internal transport.
If a command originates in Nostur and matters to the fleet, it must be normalized into Gitea before it is treated as active work.
## What Nostur is for
Use Nostur for:
- sovereign mobile operator access
- identity-linked quick commands
- acknowledgements and nudges
- emergency ingress when Matrix is unavailable or too heavy
- public or semi-public notes when intentionally used that way
Do not use Nostur for:
- hidden task queues
- final assignment truth
- long private operator/fleet discussion when Matrix is available
- routine agent-to-agent transport
## Operator path
### Path A: quick command from Nostur
Example intents:
- "open issue for this"
- "reassign this to Allegro"
- "summarize status"
- "mark this blocked"
- "create follow-up from this note"
Required system behavior:
1. accept Nostur event / DM from an authorized operator identity
2. verify identity against the allowed sovereign key set
3. classify message as one of:
- advisory only
- actionable command
- ambiguous / requires clarification
4. if actionable, translate it into one canonical Gitea object:
- new issue
- comment on existing issue
- explicit state mutation on an existing issue/PR
5. send acknowledgement back through Nostur with a link to the Gitea object
6. if private discussion is needed, continue in Matrix and point both sides at the same Gitea object
### Path B: status read from Nostur
For simple mobile reads, allow:
- current priority queue summary
- open blockers
- review queue summary
- health summary
- links to active epic/issues/PRs
These are read-only responses.
They do not mutate work state.
### Path C: public or semi-public edge
If Nostr is used publicly:
- never expose hidden internal queue truth
- publish only intentional summaries, announcements, or identity proofs
- public notes must not become a side-channel task system
## Ingress contract
For every actionable Nostur message, the bridge must emit a normalized ingress record with:
- source: nostr
- operator identity: npub or mapped principal identity
- received_at timestamp
- original event id
- normalized intent classification
- linked Gitea object id after creation or routing
- acknowledgement state
This record may live in logs or a small bridge event store, but the work itself must live in Gitea.
## Auth and identity
Nostur ingress should rely on sovereign key identity, not platform-issued bot identity.
Minimum model:
- allowlist of operator npubs
- optional challenge/response for higher-trust actions
- explicit mapping from operator identity to allowed command classes
Suggested command classes:
- read-only
- issue creation
- issue comment / note append
- assignment / routing request
- high-authority mutation requiring confirmation
The bridge must fail closed for unknown keys.
## Bridge behavior
The Nostur bridge should be small and stupid.
Responsibilities:
- receive event / DM
- authenticate sender
- normalize intent
- write/link Gitea truth
- optionally mirror conversation into Matrix
- return acknowledgement
Responsibilities it must NOT take on:
- hidden queue management
- second task database
- silent assignment logic outside coordinator doctrine
- freeform agent orchestration directly from relay chatter
## Recommended implementation sequence
### Step 1
Build read-only Nostur status responses.
Acceptance:
- Alexander can ask for status from Nostur
- response comes back with links to the canonical Gitea objects
- no queue mutation yet
### Step 2
Add explicit issue/comment creation from Nostur.
Acceptance:
- a Nostur command can create a new Gitea issue or append to an existing one
- acknowledgement message includes the issue URL
- no hidden state remains only in Nostr
### Step 3
Add Matrix handoff for private follow-up.
Acceptance:
- after Nostur ingress, the system can point the operator into Matrix for richer back-and-forth while preserving the same Gitea work object
### Step 4
Add authority tiers and confirmations.
Acceptance:
- low-risk actions can run directly
- higher-risk actions require explicit confirmation
- command classes are keyed to operator identity policy
## Non-goals
- replacing Matrix with Nostur for all private operator conversation
- using Nostr for the internal fleet bus
- letting relay notes replace issues, PRs, or review artifacts
## Operational rule
Nostur should make the system more sovereign and more convenient.
It must not make the system more ambiguous.
If Nostur ingress creates ambiguity, the bridge is wrong.
If it creates a clean Gitea-linked work object and gives Alexander a mobile sovereign edge, the bridge is right.
## Acceptance criteria
- [ ] Nostur has an explicit role in the stack
- [ ] Nostr ingress is mapped to Gitea execution truth
- [ ] read-only versus mutating commands are separated
- [ ] the bridge is defined as small and transport/ingress-focused
- [ ] the doc makes it impossible to justify shadow task state in Nostr

View File

@@ -1,120 +0,0 @@
# Operator Communications Onboarding
Status: practical operator onboarding for #166
Related:
- #173 comms unification epic
- #174 Nostur operator edge
- #175 communication authority map
## Why this exists
Alexander wants to get off Telegram and start using the system through channels we own.
This document gives the current real operator path and the near-term target path.
It is intentionally grounded in live world state, not aspiration.
## Current live reality
Today:
- Gitea is the execution truth
- Nostur/Nostr is the only sovereign operator-edge surface actually standing
- Telegram is still the legacy human command surface
- Matrix/Conduit is not yet deployed
Verified live relay path:
- relay backend host: `167.99.126.228:2929`
- operator relay URL: `wss://alexanderwhitestone.com/relay/`
- websocket probe result: `wss://alexanderwhitestone.com/relay/` CONNECTED
- backend HTTP probe on `http://167.99.126.228:2929/` returns `Timmy Foundation NIP-29 Relay. Use a Nostr client to connect.`
Non-target relays:
- `167.99.126.228:7777` is not the current operator onboarding target
- `167.99.126.228:3334` is not the live relay to use for Nostur onboarding right now
- raw `ws://167.99.126.228:2929` is backend truth, not the preferred operator-facing URL when `wss://alexanderwhitestone.com/relay/` is working
## What to use right now
### 1. Nostur = sovereign mobile/operator edge
Use Nostur for:
- quick operator commands
- status reads
- lightweight acknowledgements
- sovereign mobile access
Add this relay in Nostur:
- `wss://alexanderwhitestone.com/relay/`
Working rule:
- Nostur gets you into the system
- Gitea still holds execution truth
- Telegram remains a bridge until Matrix is deployed
### 2. Gitea = task and review truth
Use Gitea for:
- actual tasks
- issues
- PRs
- review state
- visible decisions
If a command from Nostur matters, it must be reflected in Gitea.
### 3. Telegram = legacy bridge
Still usable for now.
Not the future.
Do not treat it as the destination architecture.
## What to do in Nostur now
1. Open Nostur
2. Add relay:
- `wss://alexanderwhitestone.com/relay/`
3. Confirm the relay connects successfully
4. Verify your logged-in key matches your operator npub
5. Use Nostur as your sovereign mobile edge for operator ingress
6. When work is actionable, make sure it is mirrored into Gitea
## Channel authority, simplified
- Nostur: operator edge / ingress
- Gitea: work truth
- Telegram: temporary bridge
- Matrix: target private operator surface once deployed
- NATS: internal agent bus only
## Near-term target state
### Phase 1 — now
- Nostur working
- Telegram still active as bridge
- Gitea remains truth
### Phase 2 — next
- deploy Matrix/Conduit for private operator-to-fleet conversation
- keep Nostur as sovereign mobile ingress
- route meaningful commands from both surfaces into Gitea
### Phase 3 — cutover
- Telegram demoted fully to legacy or removed
- Matrix becomes the primary private command room
- Nostur remains the sovereign operator edge
## Acceptance for #166
We should consider #166 truly complete only when:
- [ ] Matrix/Conduit is deployed
- [ ] Alexander can message the fleet privately outside Telegram
- [ ] Nostur remains usable as a sovereign ingress layer
- [ ] both Matrix and Nostur feed into one execution truth: Gitea
- [ ] Telegram is no longer the only human command surface
## Operator rule
No matter which surface you use, the work must not scatter.
A command may arrive through Nostur.
A private conversation may continue in Matrix.
But the task itself must live in Gitea.

View File

@@ -1,42 +0,0 @@
# Matrix/Conduit Environment Configuration
# Copy to .env and fill in values before deployment
# Issue: #166 / #183
# =============================================================================
# REQUIRED: Domain Configuration
# =============================================================================
# The public domain where Matrix will be served
MATRIX_DOMAIN=matrix.timmy.foundation
# =============================================================================
# REQUIRED: Security Secrets (generate strong random values)
# =============================================================================
# Registration token for creating the first admin account
# Generate with: openssl rand -hex 32
CONDUIT_REGISTRATION_TOKEN=CHANGE_ME_TO_A_RANDOM_HEX_STRING
# Database encryption key (if using encrypted SQLite)
# Generate with: openssl rand -hex 32
CONDUIT_DATABASE_PASSWORD=CHANGE_ME_TO_A_RANDOM_HEX_STRING
# =============================================================================
# OPTIONAL: Admin Configuration
# =============================================================================
# Local admin username (without @domain)
INITIAL_ADMIN_USERNAME=admin
INITIAL_ADMIN_PASSWORD=CHANGE_ME_IMMEDIATELY
# =============================================================================
# OPTIONAL: Federation
# =============================================================================
# Comma-separated list of servers to block federation with
FEDERATION_BLACKLIST=
# Comma-separated list of servers to allow federation with (empty = all)
FEDERATION_WHITELIST=
# =============================================================================
# OPTIONAL: Media/Uploads
# =============================================================================
# Maximum file upload size in bytes (default: 100MB)
MAX_UPLOAD_SIZE=104857600

View File

@@ -1,73 +0,0 @@
# Matrix/Conduit Execution Runbook
> Issue: [#166](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/166) | Scaffold: [#183](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/183) | Decisions: [#187](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/187)
> Issued by: Ezra, Archivist | Date: 2026-04-05
## Mission
Deploy a sovereign Matrix/Conduit homeserver for encrypted human-to-fleet communication.
## Current State
| Phase | Status | Blocker |
|-------|--------|---------|
| Scaffold | Complete | None |
| Host selection | Blocked | #187 |
| DNS + TLS | Blocked | #187 |
| Deployment | Ready | Host provisioning |
| Room creation | Ready | Post-deployment |
| Telegram cutover | Ready | Fleet readiness |
## Prerequisites Checklist (from #187)
- [ ] **Host**: Confirm VPS (Hermes, Allegro, or new)
- [ ] **Domain**: Register `matrix.timmy.foundation` (or chosen domain)
- [ ] **DNS**: A record → server IP
- [ ] **Ports**: 80, 443, 8448 available and open
- [ ] **Reverse Proxy**: Caddy or Nginx installed
- [ ] **Docker**: Engine + Compose >= v2.20
## Execution Steps
### Step 1: Host Provisioning
```bash
./infra/matrix/host-readiness-check.sh matrix.timmy.foundation
```
### Step 2: DNS Configuration
```
matrix.timmy.foundation. A <SERVER_IP>
```
### Step 3: Deploy Conduit
```bash
cd infra/matrix
cp .env.example .env
# Edit .env and conduit.toml with your domain
./deploy-matrix.sh matrix.timmy.foundation
```
### Step 4: Verify Homeserver
```bash
curl https://matrix.timmy.foundation/_matrix/client/versions
```
### Step 5: Create Operator Room
1. Open Element Web
2. Register/login as `@alexander:matrix.timmy.foundation`
3. Create encrypted room: `#fleet-ops:matrix.timmy.foundation`
### Step 6: Telegram Cutover Plan
1. Run both Telegram and Matrix in parallel for 7 days
2. Pin Matrix room as primary in Telegram
3. Disable Telegram gateway only after all agents confirm Matrix connectivity
## Operational Commands
| Task | Command |
|------|---------|
| Check health | `./host-readiness-check.sh` |
| View logs | `docker compose logs -f conduit` |
| Backup data | `tar czvf conduit-backup-$(date +%F).tar.gz data/conduit/` |
| Update image | `docker compose pull && docker compose up -d` |
— Ezra, Archivist

View File

@@ -1,125 +0,0 @@
# Matrix/Conduit Deployment Go/No-Go Checklist
> **Issue**: [#166](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/166) — Stand up Matrix/Conduit
> **Blocker**: [#187](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/187) — Host / Domain / Proxy Decisions
> **Created**: 2026-04-05 by Ezra (burn mode)
> **Purpose**: Convert #187 decisions into executable deployment steps. No ambiguity. No re-litigation.
---
## Current State
| Component | Status | Evidence |
|-----------|--------|----------|
| Deployment scaffold | ✅ Complete | [`infra/matrix/`](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/src/branch/main/infra/matrix) (15 files) |
| Host readiness script | ✅ Complete | `infra/matrix/host-readiness-check.sh` |
| Operator runbook | ✅ Complete | `docs/matrix-fleet-comms/DEPLOYMENT_RUNBOOK.md` |
| Execution checklist | ✅ Complete | This file |
| **Host selected** | ⚠️ **BLOCKED** | Pending #187 |
| **Domain/subdomain chosen** | ⚠️ **BLOCKED** | Pending #187 |
| **Reverse proxy chosen** | ⚠️ **BLOCKED** | Pending #187 |
| **Live deployment** | ⚠️ **BLOCKED** | Waiting on above |
---
## Decision Gate 1: Target Host
**Question**: On which machine will Conduit run?
### Options
| Host | IP / Access | Pros | Cons |
|------|-------------|------|------|
| Hermes VPS (Bezalel/Ezra) | 143.198.27.163 | Existing infra, trusted | Already busy |
| Allegro TestBed | 167.99.126.228 | Dedicated, relay already there | Non-prod reputation |
| New droplet | TBD | Clean slate, proper sizing | Cost + provisioning time |
**Decision needed from #187**: Pick one host.
**After decision**: Update `infra/matrix/.env``MATRIX_HOST` and `infra/matrix/conduit.toml``server_name`.
---
## Decision Gate 2: Domain / Subdomain
**Question**: What is the public Matrix server name?
### Options
| Domain | DNS Owner | TLS Ready? | Note |
|--------|-----------|------------|------|
| `matrix.alexanderwhitestone.com` | Alexander | Yes (via main domain) | Clean, semantic |
| `chat.alexanderwhitestone.com` | Alexander | Yes | Shorter |
| `timmy.alexanderwhitestone.com` | Alexander | Yes | Brand-aligned |
**Decision needed from #187**: Pick one subdomain.
**After decision**: Update `infra/matrix/conduit.toml``server_name`, update `deploy-matrix.sh` → DNS validation, obtain TLS cert.
---
## Decision Gate 3: Reverse Proxy & TLS
**Question**: How do clients reach Conduit over HTTPS?
### Options
| Proxy | TLS Source | Config Location | Best For |
|-------|------------|-----------------|----------|
| Caddy | Automatic (Let's Encrypt) | `infra/matrix/caddy/Caddyfile` | Simplicity, auto-TLS |
| Nginx | Manual certbot | New file: `infra/matrix/nginx/` | Existing nginx expertise |
| Traefik | Automatic | New file: `infra/matrix/traefik/` | Docker-native stacks |
**Decision needed from #187**: Pick one proxy strategy.
**After decision**: Copy the chosen proxy config into place, update `docker-compose.yml` port bindings, run `./host-readiness-check.sh`.
---
## Post-Decision Execution Script
Once #187 closes with the three decisions above, execute in this exact order:
```bash
# 1. SSH into chosen host
ssh user@<HOST_FROM_187>
# 2. Clone / enter timmy-config
cd /opt/timmy-config # or wherever fleet repos live
# 3. Pre-flight check
cd infra/matrix
./host-readiness-check.sh
# Fix any RED items before continuing.
# 4. Edit secrets
cp .env.example .env
# Fill: MATRIX_HOST, POSTGRES_PASSWORD, CONDUIT_REGISTRATION_TOKEN
# 5. Edit Conduit config
# Update server_name in conduit.toml to match DOMAIN_FROM_187
# 6. Deploy
./deploy-matrix.sh
# 7. Verify
# - Element Web loads at https://<DOMAIN>/_matrix/static/
# - Federation test passes (if enabled)
# - First operator account can register/login
# 8. Create fleet rooms
# See: docs/matrix-fleet-comms/DEPLOYMENT_RUNBOOK.md § "Room Bootstrap"
```
---
## Operator Accountability
| Decision | Owner | Due | Blocker Lifted |
|----------|-------|-----|----------------|
| Host | @allegro or @timmy | ASAP | Gate 1 |
| Domain | @rockachopa (Alexander) | ASAP | Gate 2 |
| Proxy | @ezra or @allegro | ASAP | Gate 3 |
**When all three decisions are in #187, this checklist becomes the literal deployment runbook.**
---
*Last updated: 2026-04-05 by Ezra*

View File

@@ -1,69 +0,0 @@
# Matrix/Conduit Deployment Scaffold
> Parent: [#166](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/166) | Scaffold task: [#183](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/183)
This directory contains an executable deployment path for standing up a Matrix homeserver (Conduit) for sovereign human-to-fleet encrypted communication.
## Status
| Component | State |
|-----------|-------|
| Deployment scaffold | ✅ Present |
| Target host | ⚠️ Requires selection |
| Reverse proxy (Caddy/Nginx) | ⚠️ Pending host provisioning |
| TLS certificates | ⚠️ Pending DNS + proxy setup |
| Federation | ⚠️ Pending DNS SRV records |
| Fleet bot integration | ⚠️ Post-deployment |
## Quick Start
```bash
cd /path/to/timmy-config/infra/matrix
# 1. Read prerequisites.md — ensure host is ready
# 2. Edit conduit.toml with your domain
# 3. Copy .env.example → .env and fill secrets
# 4. Run: ./deploy-matrix.sh
```
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Host (VPS) │
│ ┌─────────────────┐ ┌──────────────────────────────┐ │
│ │ Caddy/Nginx │─────▶│ Conduit (Matrix homeserver) │ │
│ │ :443/:8448 │ │ :6167 (internal) │ │
│ └─────────────────┘ └──────────────────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ TLS termination SQLite/RocksDB storage │
│ Let's Encrypt Config: conduit.toml │
└─────────────────────────────────────────────────────────────┘
```
## Files
| File | Purpose |
|------|---------|
| `prerequisites.md` | Host requirements, ports, DNS, decisions |
| `docker-compose.yml` | Conduit + optionally Element-Web |
| `conduit.toml` | Homeserver configuration scaffold |
| `deploy-matrix.sh` | One-command deployment script |
| `.env.example` | Environment variable template |
| `caddy/Caddyfile` | Reverse proxy configuration |
## Post-Deployment
1. Create admin account via registration or CLI
2. Create fleet rooms (encrypted by default)
3. Onboard Alexander as operator
4. Deploy fleet bots (Hermes gateway with Matrix platform adapter)
5. Evaluate Telegram-to-Matrix bridge (mautrix-telegram)
## Decisions Log
- **Homeserver**: Conduit (lightweight, Rust, single binary, SQLite default)
- **Database**: SQLite for single-host; migrate to PostgreSQL if scale demands
- **Reverse proxy**: Caddy (automatic HTTPS) or Nginx (existing familiarity)
- **Client**: Element Web (optional, self-hosted) + native apps
- **Federation**: Enabled (required for multi-homeserver fleet topology)

View File

@@ -1,50 +0,0 @@
# Matrix/Conduit Scaffold Inventory
> Issue: [#183](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/183) | Parent: [#166](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/166)
> Issued by: Ezra, Archivist | Date: 2026-04-05
## Status: COMPLETE — Canonical Location Established
## Canonical Scaffold
| Directory | Purpose | Status |
|-----------|---------|--------|
| **`infra/matrix/`** | Single source of truth | Canonical |
## Artifact Map
### `infra/matrix/` (Canonical — 11 files)
| File | Purpose | Bytes |
|------|---------|-------|
| `README.md` | Entry point + architecture | 3,275 |
| `prerequisites.md` | Host/decision checklist | 2,690 |
| `docker-compose.yml` | Conduit + Element + Postgres | 1,427 |
| `conduit.toml` | Homeserver configuration | 1,498 |
| `deploy-matrix.sh` | One-command deployment | 3,388 |
| `host-readiness-check.sh` | Pre-flight validation | 3,321 |
| `.env.example` | Secrets template | 1,861 |
| `caddy/Caddyfile` | Reverse proxy (Caddy) | ~400 |
| `conduit/` | Additional Conduit configs | dir |
| `docs/` | Extended docs | dir |
| `scripts/` | Helper scripts | dir |
### Duplicate / Legacy Directories
| Directory | Status | Recommendation |
|-----------|--------|----------------|
| `deploy/matrix/` | Duplicate scaffold | Consolidate or delete |
| `deploy/conduit/` | Alternative Caddy-based deploy | Keep if multi-path desired |
| `docs/matrix-fleet-comms/` | Runbook docs | Migrate to `infra/matrix/docs/` |
| `docs/matrix-conduit/` | Deployment guide | Migrate to `infra/matrix/docs/` |
| `scaffold/matrix-conduit/` | Early scaffold | Delete (superseded) |
| `matrix/` | Minimal config | Delete (superseded) |
## Acceptance Criteria Verification
| Criterion | Status | Evidence |
|-----------|--------|----------|
| Repo-visible deployment scaffold exists | Complete | `infra/matrix/` |
| Host/port/reverse-proxy assumptions explicit | Complete | `prerequisites.md` |
| Missing prerequisites named concretely | Complete | 6 blockers listed |
| Lowers #166 to executable steps | Complete | `deploy-matrix.sh` + runbooks |
— Ezra, Archivist

View File

@@ -1,58 +0,0 @@
# Caddyfile — Reverse proxy for Conduit Matrix homeserver
# Issue: #166 / #183
#
# Place in /etc/caddy/Caddyfile or use with `caddy run --config Caddyfile`
# Matrix client and federation on same domain
matrix.timmy.foundation {
# Client API (.well-known, /_matrix/client)
handle /.well-known/matrix/* {
header Content-Type application/json
respond `{"
"m.homeserver": {"base_url": "https://matrix.timmy.foundation"},
"m.identity_server": {"base_url": "https://vector.im"}
}` 200
}
# Handle federation (server-to-server) on standard path
handle /_matrix/server/* {
reverse_proxy localhost:6167
}
# Handle client API
handle /_matrix/client/* {
reverse_proxy localhost:6167
}
# Handle media repository
handle /_matrix/media/* {
reverse_proxy localhost:6167
}
# Handle federation checks
handle /_matrix/federation/* {
reverse_proxy localhost:6167
}
# Handle static content (if serving Element web from same domain)
handle_path /element/* {
reverse_proxy localhost:8080
}
# Health check / status
respond /health "OK" 200
# Default — you may want to serve Element web or redirect
respond "Matrix Homeserver" 200
}
# Optional: Serve Element Web on separate subdomain
# element.timmy.foundation {
# reverse_proxy localhost:8080
# }
# Federation port (8448) — server-to-server communication
# This allows other Matrix servers to find and connect to yours
matrix.timmy.foundation:8448 {
reverse_proxy localhost:6167
}

View File

@@ -1,53 +0,0 @@
# Conduit Configuration Scaffold
# Copy to conduit.toml, replace placeholders, and deploy
#
# Issue: #166 - Matrix/Conduit for human-to-fleet encrypted communication
[database]
# SQLite is default; use PostgreSQL for production scale
backend = "rocksdb"
path = "/var/lib/matrix-conduit/"
[global]
# The domain name of your homeserver (MUST match DNS)
server_name = "YOUR_DOMAIN_HERE" # e.g., "matrix.timmy.foundation"
# The port Conduit listens on internally (mapped via docker-compose)
port = 6167
# Public base URL (what clients connect to)
public_baseurl = "https://YOUR_DOMAIN_HERE/"
# Enable/disable registration (disable after initial admin setup)
allow_registration = false
# Registration token for initial admin creation
registration_token = "GENERATE_A_STRONG_TOKEN_PLEASE"
# Enable federation (required for multi-homeserver fleet)
allow_federation = true
# Federation port (usually 8448)
federation_port = 8448
# Maximum upload size for media
max_request_size = 104_857_600 # 100MB
# Enable presence (who's online) - can be resource intensive
allow_presence = true
# Logging
log = "info,rocket=off,_=off"
[admin]
# Enable admin commands via CLI
enabled = true
[well_known]
# Configure /.well-known/matrix/client and /.well-known/matrix/server
# This allows clients to auto-discover the homeserver
client = "https://YOUR_DOMAIN_HERE/"
server = "YOUR_DOMAIN_HERE:8448"
# TLS is handled by the reverse proxy (Caddy/Nginx)
# Conduit runs HTTP internally; proxy terminates TLS

View File

@@ -1,31 +0,0 @@
# Conduit Matrix Homeserver Configuration
# Copy to .env and fill in values
# Domain name for your Matrix server (e.g., matrix.timmy.foundation)
DOMAIN=matrix.timmy.foundation
# Server name (same as DOMAIN in most cases)
CONDUIT_SERVER_NAME=matrix.timmy.foundation
# Database backend: rocksdb (default) or sqlite
CONDUIT_DATABASE_BACKEND=rocksdb
# Enable user registration (set to true ONLY during initial admin setup)
CONDUIT_ALLOW_REGISTRATION=false
# Enable federation with other Matrix servers
CONDUIT_ALLOW_FEDERATION=true
# Enable metrics endpoint (Prometheus)
CONDUIT_ENABLE_METRICS=false
# Registration token for creating the first admin account
# MUST be set before starting server - remove/rotate after admin creation
CONDUIT_REGISTRATION_TOKEN=CHANGE_THIS_TO_A_RANDOM_SECRET_
# Path to config file (optional, leave empty to use env vars)
CONDUIT_CONFIG=
# Caddy environment
CADDY_HTTP_PORT=80
CADDY_HTTPS_PORT=443

View File

@@ -1,91 +0,0 @@
# Conduit Configuration
# Server Settings
global_server_name = "matrix.example.com" # CHANGE THIS
database_backend = "rocksdb"
database_path = "/var/lib/matrix-conduit"
registration = false # Disabled after initial admin account creation
registration_token = "" # Set via CONDUIT_REGISTRATION_TOKEN env var
federation = true
allow_federation = true
federation_sender_buffer = 100
# Even if federation is disabled, sometimes you still want to allow the server
# to reach other homeservers for e.g. bridge functionality or integration servers
allow_check_for_updates = true
# Address on which to connect to the server (locally).
address = "0.0.0.0"
port = 6167
# Enable if you want TLS termination handled directly by Conduit (not recommended)
tls = false
# Max request size in bytes (default: 20MB)
max_request_size = 20971520
# Enable metrics endpoint for Prometheus
enable_metrics = false
# Logging level: debug, info, warn, error
log = "info"
# Maximum database cache size (if using rocksdb)
cache_capacity_mb = 512
# Workaround for Synapse's synapsedotorg room
allow_incoming_presence = true
send_query_auth_requests = true
# Allow appservices to use /_matrix/client/r0/login
allow_appservice_login = false
# Disable users who don't use their accounts for longer than this time
freeze_unfreeze_device_lists = false
# Enable media proxying through the server
allow_media_relaying = false
# Block certain IP ranges from federation
deny_federation_from = [
# "example.com",
]
# Require authentication for media requests
require_auth_for_profile_requests = false
# Trust X-Forwarded-* headers from reverse proxy
trusted_servers = []
# URL Preview settings
url_preview = false
max_preview_url_length = 2048
max_preview_spider_size = 1048576 # 1MB
# Consent tracking
users_consent_to_tracking = true
# Backup
backup_burst_count = 3
backup_per_second = 0.5
# Presence presence = true
# Push (for push notifications to mobile apps)
push = true
# Federation - How long to wait before timing out federation requests
federation_timeout_seconds = 30
# Event persistence settings
pdu_cache_capacity = 100000
auth_chain_cache_capacity = 100000
# Appservice support
appservice = true
# Initial sync cache (can be memory intensive)
initial_sync_cache = true

View File

@@ -1,58 +0,0 @@
version: '3.8'
services:
conduit:
image: docker.io/girlbossceo/conduit:v0.8.0
container_name: matrix-conduit
restart: unless-stopped
volumes:
- ./data:/var/lib/matrix-conduit
environment:
- CONDUIT_SERVER_NAME=${CONDUIT_SERVER_NAME}
- CONDUIT_DATABASE_PATH=/var/lib/matrix-conduit
- CONDUIT_DATABASE_BACKEND=${CONDUIT_DATABASE_BACKEND:-rocksdb}
- CONDUIT_PORT=6167
- CONDUIT_ADDRESS=0.0.0.0
- CONDUIT_CONFIG=${CONDUIT_CONFIG:-}
- CONDUIT_ALLOW_REGISTRATION=${CONDUIT_ALLOW_REGISTRATION:-false}
- CONDUIT_ALLOW_FEDERATION=${CONDUIT_ALLOW_FEDERATION:-true}
- CONDUIT_ENABLE_METRICS=${CONDUIT_ENABLE_METRICS:-false}
- RUST_LOG=info
networks:
- matrix
expose:
- "6167"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:6167/_matrix/client/versions"]
interval: 30s
timeout: 10s
retries: 5
caddy:
image: docker.io/caddy:2.7-alpine
container_name: matrix-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "8448:8448" # Federation
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
- caddy_config:/config
environment:
- DOMAIN=${DOMAIN}
depends_on:
- conduit
networks:
- matrix
cap_add:
- NET_ADMIN # For Caddy to bind low ports
networks:
matrix:
name: matrix
volumes:
caddy_data:
caddy_config:

View File

@@ -1,114 +0,0 @@
#!/usr/bin/env bash
# deploy-matrix.sh — Deploy Conduit Matrix homeserver for Timmy fleet
# Usage: ./deploy-matrix.sh [DOMAIN]
#
# Issue: #166 / #183
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOMAIN="${1:-${MATRIX_DOMAIN:-}}"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log() { echo -e "${GREEN}[deploy-matrix]${NC} $*"; }
warn() { echo -e "${YELLOW}[deploy-matrix]${NC} $*"; }
error() { echo -e "${RED}[deploy-matrix]${NC} $*" >&2; }
# === Pre-flight checks ===
log "Starting Matrix/Conduit deployment..."
if [[ -z "$DOMAIN" ]]; then
error "DOMAIN not specified. Usage: ./deploy-matrix.sh matrix.timmy.foundation"
error "Or set MATRIX_DOMAIN environment variable."
exit 1
fi
if [[ ! -f "$SCRIPT_DIR/.env" ]]; then
error ".env file not found. Copy .env.example to .env and configure."
exit 1
fi
if [[ ! -f "$SCRIPT_DIR/conduit.toml" ]]; then
error "conduit.toml not found. Copy from scaffold and configure."
exit 1
fi
# Check for placeholder values
if grep -q "YOUR_DOMAIN_HERE" "$SCRIPT_DIR/conduit.toml"; then
error "conduit.toml still contains YOUR_DOMAIN_HERE placeholder."
error "Please edit and replace with actual domain: $DOMAIN"
exit 1
fi
if grep -q "CHANGE_ME" "$SCRIPT_DIR/.env"; then
warn ".env contains CHANGE_ME placeholders. Ensure secrets are set."
fi
# Check Docker availability
if ! command -v docker &>/dev/null; then
error "Docker not found. Install: curl -fsSL https://get.docker.com | sh"
exit 1
fi
if ! docker compose version &>/dev/null; then
error "Docker Compose not found."
exit 1
fi
log "Pre-flight checks passed. Domain: $DOMAIN"
# === Directory setup ===
log "Creating data directories..."
mkdir -p "$SCRIPT_DIR/data/conduit"
mkdir -p "$SCRIPT_DIR/data/caddy"
# === Load environment ===
set -a
source "$SCRIPT_DIR/.env"
set +a
# === Pull and start ===
log "Pulling Conduit image..."
docker compose -f "$SCRIPT_DIR/docker-compose.yml" pull
log "Starting Conduit..."
docker compose -f "$SCRIPT_DIR/docker-compose.yml" up -d
# === Wait for health ===
log "Waiting for Conduit healthcheck..."
for i in {1..30}; do
if docker compose -f "$SCRIPT_DIR/docker-compose.yml" ps conduit | grep -q "healthy"; then
log "Conduit is healthy!"
break
fi
if [[ $i -eq 30 ]]; then
error "Conduit failed to become healthy within 5 minutes."
docker compose -f "$SCRIPT_DIR/docker-compose.yml" logs --tail 50 conduit
exit 1
fi
sleep 10
done
# === Post-deploy info ===
log "Deployment complete!"
echo ""
echo "=========================================="
echo "Matrix homeserver deployed at: $DOMAIN"
echo "=========================================="
echo ""
echo "Next steps:"
echo " 1. Ensure reverse proxy (Caddy/Nginx) forwards to localhost:6167"
echo " 2. Create admin account with:"
echo " curl -X POST https://$DOMAIN/_matrix/client/v3/register"
echo " -H Content-Type: application/json"
echo " -d {"username":"admin","password":"YOUR_PASS","auth":{"type":"m.login.dummy"}}"
echo " 3. Create fleet rooms via Element or API"
echo " 4. Configure Hermes gateway for Matrix platform"
echo ""
echo "Logs: docker compose -f $SCRIPT_DIR/docker-compose.yml logs -f"
echo "Stop: docker compose -f $SCRIPT_DIR/docker-compose.yml down"

View File

@@ -1,51 +0,0 @@
version: "3.8"
services:
conduit:
image: docker.io/girlbossceo/conduit:v0.8.0
container_name: timmy-conduit
restart: unless-stopped
volumes:
- ./conduit.toml:/etc/conduit/conduit.toml:ro
- conduit-data:/var/lib/matrix-conduit
environment:
- CONDUIT_CONFIG=/etc/conduit/conduit.toml
# Override secrets via env (see .env)
- CONDUIT_REGISTRATION_TOKEN=${CONDUIT_REGISTRATION_TOKEN}
- CONDUIT_DATABASE_PASSWORD=${CONDUIT_DATABASE_PASSWORD}
ports:
# Only expose on localhost; reverse proxy forwards from 443
- "127.0.0.1:6167:6167"
networks:
- matrix
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:6167/_matrix/static/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
# Optional: Element Web client (self-hosted)
element-web:
image: vectorim/element-web:latest
container_name: timmy-element
restart: unless-stopped
volumes:
- ./element-config.json:/app/config.json:ro
environment:
- default_server_config.homeserver.base_url=https://${MATRIX_DOMAIN}
- default_server_config.homeserver.server_name=${MATRIX_DOMAIN}
ports:
- "127.0.0.1:8080:80"
networks:
- matrix
profiles:
- element # docker compose --profile element up -d
volumes:
conduit-data:
driver: local
networks:
matrix:
driver: bridge

View File

@@ -1,119 +0,0 @@
# Matrix/Conduit Operational Runbook
This document contains operational procedures for the Timmy Foundation Matrix infrastructure.
## Quick Reference
| Task | Command |
|------|---------|
| Start server | `cd infra/matrix/conduit && docker compose up -d` |
| View logs | `cd infra/matrix/conduit && docker compose logs -f` |
| Create admin account | `./scripts/deploy-conduit.sh admin` |
| Backup data | `./scripts/deploy-conduit.sh backup` |
| Check status | `./scripts/deploy-conduit.sh status` |
## Initial Setup Checklist
- [ ] DNS A record pointing to host IP (matrix.yourdomain.com → host)
- [ ] DNS SRV record for federation (_matrix._tcp → matrix.yourdomain.com:443)
- [ ] Docker and Docker Compose installed
- [ ] `.env` file configured with real values
- [ ] Ports 80, 443, 8448 open in firewall
- [ ] Run `./deploy-conduit.sh install`
- [ ] Run `./deploy-conduit.sh start`
- [ ] Create admin account immediately
- [ ] Disable registration in `.env` and restart
- [ ] Test with Element Web or other client
## Account Creation (One-Time)
**IMPORTANT**: Only enable registration during initial admin account creation.
1. Set `CONDUIT_ALLOW_REGISTRATION=true` in `.env`
2. Set `CONDUIT_REGISTRATION_TOKEN` to a random secret
3. Restart: `./deploy-conduit.sh restart`
4. Create account:
```bash
./deploy-conduit.sh admin
# Inside container:
register_new_matrix_user -c /var/lib/matrix-conduit -u admin -p YOUR_PASS -a
```
5. Set `CONDUIT_ALLOW_REGISTRATION=false` and restart
## Federation Troubleshooting
Federation allows your server to communicate with other Matrix servers (matrix.org, etc).
### Verify Federation Works
```bash
curl https://matrix.org/_matrix/federation/v1/query/directory?room_alias=%23timmy%3Amatrix.yourdomain.com
```
### Required:
- DNS SRV: `_matrix._tcp.yourdomain.com IN SRV 10 0 443 matrix.yourdomain.com`
- Or `.well-known/matrix/server` served on port 443
- Port 8448 reachable (Caddy handles this)
## Backup and Recovery
### Automated Daily Backup (cron)
```bash
0 2 * * * /path/to/timmy-config/infra/matrix/scripts/deploy-conduit.sh backup
```
### Restore from Backup
```bash
./deploy-conduit.sh stop
cd infra/matrix/conduit
rm -rf data/*
tar xzf /path/to/backup.tar.gz
./scripts/deploy-conduit.sh start
```
## Monitoring
### Health Endpoint
```bash
curl http://localhost:6167/_matrix/client/versions
```
### Prometheus Metrics
Enable in `.env`: `CONDUIT_ENABLE_METRICS=true`
Metrics available at: `http://localhost:6167/_matrix/metrics`
## Federation Federation
If you don't need federation (standalone server):
Set `CONDUIT_ALLOW_FEDERATION=false` in `.env`
## Matrix Client Configuration
### Element Web (Self-Hosted)
Create `element-config.json`:
```json
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://matrix.yourdomain.com",
"server_name": "yourdomain.com"
}
}
}
```
### Element Desktop/Mobile
- Homeserver URL: `https://matrix.yourdomain.com`
- User ID: `@username:yourdomain.com`
## Security Hardening
- [ ] Fail2ban on SSH and HTTP
- [ ] Keep Docker images updated: `docker compose pull && docker compose up -d`
- [ ] Review Caddy logs for abuse
- [ ] Disable registration after admin creation
- [ ] Use strong admin password
- [ ] Store backups encrypted
## Related Issues
- Epic: timmy-config#166
- Scaffold: timmy-config#183
- Parent Epic: timmy-config#173 (Unified Comms)

View File

@@ -1,39 +0,0 @@
# ADR-001: Homeserver Selection — Conduit
**Status**: Accepted
**Date**: 2026-04-05
**Deciders**: Ezra (architect), Timmy Foundation
**Scope**: Matrix homeserver for human-to-fleet encrypted communication (#166, #183)
---
## Context
We need a Matrix homeserver to serve as the sovereign operator surface. Options:
- **Synapse** (Python, mature, resource-heavy)
- **Dendrite** (Go, lighter, beta federation)
- **Conduit** (Rust, lightweight, SQLite support)
## Decision
Use **Conduit** as the Matrix homeserver.
## Consequences
| Positive | Negative |
|----------|----------|
| Low RAM/CPU footprint (~200 MB) | Smaller ecosystem than Synapse |
| SQLite option eliminates Postgres ops | Some edge-case federation bugs |
| Single binary, simple systemd service | Admin tooling less mature |
| Full federation support | |
## Alternatives Considered
- **Synapse**: Rejected due to Python overhead and mandatory Postgres complexity.
- **Dendrite**: Rejected due to beta federation status; we need reliable federation from day one.
## References
- Issue: [#166](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/166)
- Issue: [#183](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/183)
- Conduit docs: https://conduit.rs/

View File

@@ -1,37 +0,0 @@
# ADR-002: Host Selection — Hermes VPS
**Status**: Accepted
**Date**: 2026-04-05
**Deciders**: Ezra (architect), Timmy Foundation
**Scope**: Initial deployment host for Matrix/Conduit (#166, #183, #187)
---
## Context
We need a target host for the Conduit homeserver. Options:
- Existing Hermes VPS (`143.198.27.163`)
- Timmy-Home bare metal
- New cloud droplet (DigitalOcean, Hetzner, etc.)
## Decision
Use the **existing Hermes VPS** as the initial host, with a future option to migrate to a dedicated Matrix VPS if load demands.
## Consequences
| Positive | Negative |
|----------|----------|
| Zero additional hosting cost | Shared resource pool with Gitea + wizard gateways |
| Known operational state (backups, monitoring) | Single point of failure for multiple services |
| Simplified network posture | May need to upgrade VPS if federation traffic grows |
## Migration Trigger
If Matrix active users exceed ~50 or federation traffic causes >60% sustained CPU, migrate to a dedicated VPS. The Docker Compose scaffold makes this a data-directory copy.
## References
- Issue: [#166](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/166)
- Issue: [#187](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/187)
- Decision Framework: [`docs/DECISION_FRAMEWORK_187.md`](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/src/branch/main/docs/DECISION_FRAMEWORK_187.md)

View File

@@ -1,35 +0,0 @@
# ADR-003: Federation Strategy — Full Federation Enabled
**Status**: Accepted
**Date**: 2026-04-05
**Deciders**: Ezra (architect), Timmy Foundation
**Scope**: Federation behavior for Conduit homeserver (#166, #183)
---
## Context
Matrix servers can operate in isolated mode (no federation) or federated mode (interoperate with matrix.org and other homeservers).
## Decision
Enable **full federation from day one**.
## Consequences
| Positive | Negative |
|----------|----------|
| Alexander can use any Matrix client/ID | Requires public DNS + TLS + port 8448 |
| Fleet bots can bridge to other networks | Slightly larger attack surface |
| Aligns with sovereign, open protocol ethos | Must monitor for abuse/spam |
## Prerequisites Introduced
- Valid TLS certificate (Let's Encrypt via Caddy)
- Public DNS A record + SRV record
- Firewall open on TCP 8448 inbound
## References
- Issue: [#166](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/166)
- Runbook: [`infra/matrix/docs/RUNBOOK.md`](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/src/branch/main/infra/matrix/docs/RUNBOOK.md)

View File

@@ -1,38 +0,0 @@
# ADR-004: Reverse Proxy Selection — Caddy
**Status**: Accepted
**Date**: 2026-04-05
**Deciders**: Ezra (architect), Timmy Foundation
**Scope**: TLS termination and reverse proxy for Matrix/Conduit (#166, #183)
---
## Context
Options for reverse proxy + TLS:
- **Caddy** (auto-TLS, simple config)
- **Traefik** (Docker-native, label-based)
- **Nginx** (ubiquitous, more manual)
## Decision
Use **Caddy** as the dedicated reverse proxy for Matrix services.
## Consequences
| Positive | Negative |
|----------|----------|
| Automatic ACME/Let's Encrypt | Less community Matrix-specific examples |
| Native `.well-known` + SRV support | New config language for ops team |
| No Docker label magic required | |
| Clean separation from existing Traefik | |
## Implementation
See:
- `infra/matrix/caddy/Caddyfile`
- `deploy/matrix/Caddyfile`
## References
- Issue: [#183](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/183)

View File

@@ -1,35 +0,0 @@
# ADR-005: Database Selection — SQLite for Phase 1
**Status**: Accepted
**Date**: 2026-04-05
**Deciders**: Ezra (architect), Timmy Foundation
**Scope**: Persistence layer for Conduit (#166, #183)
---
## Context
Conduit supports SQLite and PostgreSQL. Synapse requires Postgres.
## Decision
Use **SQLite** for the initial deployment (Phase 1). Migrate to PostgreSQL only if user count or performance metrics trigger it.
## Consequences
| Positive | Negative |
|----------|----------|
| Zero additional container/service | Harder to scale horizontally |
| Single file backup/restore | Performance ceiling under heavy load |
| Conduit optimized for SQLite | |
## Migration Trigger
- Concurrent active users > 50
- Database file > 10 GB
- Noticeable query latency on room sync
## References
- Issue: [#166](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/166)
- Config: `infra/matrix/conduit.toml`

View File

@@ -1,26 +0,0 @@
# Architecture Decision Records — Matrix/Conduit Fleet Communications
**Issue**: [#183](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/183)
**Parent**: [#166](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/166)
---
## Index
| ADR | Decision | File |
|-----|----------|------|
| ADR-001 | Homeserver: Conduit | `ADR-001-conduit-selection.md` |
| ADR-002 | Host: Hermes VPS | `ADR-002-hermes-vps-host.md` |
| ADR-003 | Federation: Full enable | `ADR-003-full-federation.md` |
| ADR-004 | Reverse Proxy: Caddy | `ADR-004-caddy-reverse-proxy.md` |
| ADR-005 | Database: SQLite (Phase 1) | `ADR-005-sqlite-phase1.md` |
## Purpose
These ADRs make the #183 scaffold auditable and portable. Any future agent or operator can understand *why* the architecture is shaped this way without re-litigating decisions.
## Continuity
- Canonical scaffold index: [`docs/CANONICAL_INDEX_MATRIX.md`](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/src/branch/main/docs/CANONICAL_INDEX_MATRIX.md)
- Decision framework for #187: [`docs/DECISION_FRAMEWORK_187.md`](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/src/branch/main/docs/DECISION_FRAMEWORK_187.md)
- Operational runbook: [`infra/matrix/docs/RUNBOOK.md`](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/src/branch/main/infra/matrix/docs/RUNBOOK.md)

View File

@@ -1,124 +0,0 @@
#!/usr/bin/env bash
# host-readiness-check.sh — Validate target host before Matrix/Conduit deployment
# Usage: ./host-readiness-check.sh [DOMAIN]
# Issue: #166 / #183
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOMAIN="${1:-${MATRIX_DOMAIN:-}}"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
PASS=0
FAIL=0
WARN=0
pass() { echo -e "${GREEN}[PASS]${NC} $*"; ((PASS++)); }
fail() { echo -e "${RED}[FAIL]${NC} $*"; ((FAIL++)); }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; ((WARN++)); }
log() { echo -e "\n==> $*"; }
log "Matrix/Conduit Host Readiness Check"
log "===================================="
# === Domain check ===
if [[ -z "$DOMAIN" ]]; then
fail "DOMAIN not specified. Usage: ./host-readiness-check.sh matrix.timmytime.net"
exit 1
else
pass "Domain specified: $DOMAIN"
fi
# === Docker ===
log "Checking Docker..."
if command -v docker &>/dev/null; then
DOCKER_VER=$(docker --version)
pass "Docker installed: $DOCKER_VER"
else
fail "Docker not installed"
fi
if docker compose version &>/dev/null || docker-compose --version &>/dev/null; then
pass "Docker Compose available"
else
fail "Docker Compose not available"
fi
if docker info &>/dev/null; then
pass "Docker daemon is running"
else
fail "Docker daemon is not running or user lacks permissions"
fi
# === Ports ===
log "Checking ports..."
for port in 80 443 8448; do
if ss -tln | grep -q ":$port "; then
warn "Port $port is already in use (may conflict)"
else
pass "Port $port is available"
fi
done
# === DNS Resolution ===
log "Checking DNS..."
RESOLVED_IP=$(dig +short "$DOMAIN" || true)
if [[ -n "$RESOLVED_IP" ]]; then
HOST_IP=$(curl -s ifconfig.me || true)
if [[ "$RESOLVED_IP" == "$HOST_IP" ]]; then
pass "DNS A record resolves to this host ($HOST_IP)"
else
warn "DNS A record resolves to $RESOLVED_IP (this host is $HOST_IP)"
fi
else
fail "DNS A record for $DOMAIN not found"
fi
# === Disk Space ===
log "Checking disk space..."
AVAILABLE_GB=$(df -BG "$SCRIPT_DIR" | awk 'NR==2 {gsub(/G/,""); print $4}')
if [[ "$AVAILABLE_GB" -ge 20 ]]; then
pass "Disk space: ${AVAILABLE_GB}GB available"
else
warn "Disk space: ${AVAILABLE_GB}GB available (recommended: 20GB+)"
fi
# === Memory ===
log "Checking memory..."
MEM_GB=$(free -g | awk '/^Mem:/ {print $2}')
if [[ "$MEM_GB" -ge 2 ]]; then
pass "Memory: ${MEM_GB}GB"
else
warn "Memory: ${MEM_GB}GB (recommended: 2GB+)"
fi
# === Reverse proxy detection ===
log "Checking reverse proxy..."
if command -v caddy &>/dev/null; then
pass "Caddy installed"
elif command -v nginx &>/dev/null; then
pass "Nginx installed"
elif ss -tln | grep -q ":80 " || ss -tln | grep -q ":443 "; then
warn "No Caddy/Nginx found, but something is bound to 80/443"
else
warn "No reverse proxy detected (Caddy or Nginx recommended)"
fi
# === Summary ===
log "===================================="
echo -e "Results: ${GREEN}$PASS passed${NC}, ${YELLOW}$WARN warnings${NC}, ${RED}$FAIL failures${NC}"
if [[ $FAIL -gt 0 ]]; then
echo ""
echo "Host is NOT ready for deployment. Fix failures above, then re-run."
exit 1
else
echo ""
echo "Host looks ready. Next step: ./deploy-matrix.sh $DOMAIN"
exit 0
fi

View File

@@ -1,95 +0,0 @@
# Matrix/Conduit Prerequisites
> Issue: [#183](http://143.198.27.163:3000/Timmy_Foundation/timmy-config/issues/183)
## Target Host Requirements
### Option A: Deploy on Hermes VPS (143.198.27.163)
- **Pros**: Existing infrastructure, Ezra home territory
- **Cons**: Already hosting multiple wizards, resource contention
- **Ports available**: Need to verify 443, 8448 free or proxyable
### Option B: Deploy on Allegro (167.99.126.228)
- **Pros**: Separate host from Hermes, already has Nostr relay
- **Cons**: Allegro-Primus runs there; check resource headroom
### Option C: New VPS
- **Pros**: Clean slate, dedicated resources
- **Cons**: Additional cost, new maintenance surface
### Recommended: Option A (Hermes) or dedicated lightweight VPS
---
## Required Ports
| Port | Protocol | Purpose | Visibility |
|------|----------|---------|------------|
| 443 | TCP | Client HTTPS (Caddy/Nginx → Conduit) | Public |
| 8448 | TCP | Server-to-server federation | Public |
| 6167 | TCP | Conduit internal (localhost only) | Localhost |
| 80 | TCP | ACME HTTP challenge (redirects to 443) | Public |
## DNS Requirements
```
# A record
matrix.timmy.foundation. A <SERVER_IP>
# Optional: subdomains for federation delegation
_timatrix._tcp.timmy.foundation. SRV 10 0 8448 matrix.timmy.foundation.
```
## Host Software
```bash
# Docker + Compose (required)
docker --version # >= 24.0
docker compose version # >= 2.20
# Or install if missing:
curl -fsSL https://get.docker.com | sh
```
## Reverse Proxy (choose one)
### Option 1: Caddy (recommended for automatic TLS)
```bash
apt install caddy # or use official repo
```
### Option 2: Nginx (if already deployed)
```bash
apt install nginx certbot python3-certbot-nginx
```
## TLS Certificate Requirements
- Valid domain pointing to server IP
- Port 80 open for ACME challenge (HTTP-01)
- Or: DNS challenge for wildcard/internal domains
## Storage
| Component | Minimum | Recommended |
|-----------|---------|-------------|
| Conduit DB | 5 GB | 20 GB |
| Media uploads | 10 GB | 50 GB+ |
| Logs | 2 GB | 5 GB |
## Missing Prerequisites (Blocking)
1. [ ] **Target host selected** — Hermes vs Allegro vs new
2. [ ] **Domain/subdomain assigned** — matrix.timmy.foundation?
3. [ ] **DNS A record created** — pointing to target host
4. [ ] **Ports verified open** — 443, 8448 on target host
5. [ ] **Reverse proxy decision** — Caddy vs Nginx
6. [ ] **SSL strategy confirmed** — Let's Encrypt via proxy
## Next Steps After Prerequisites
1. Fill in `conduit.toml` with actual domain
2. Put admin registration secret in `.env`
3. Run `./deploy-matrix.sh`
4. Create first admin account
5. Create fleet rooms

View File

@@ -1,203 +0,0 @@
#!/bin/bash
set -euo pipefail
# Conduit Matrix Homeserver Deployment Script
# Usage: ./deploy-conduit.sh [install|start|stop|logs|status|backup]
#
# See upstream: timmy-config#166, timmy-config#183
# Dependency: prerequisites.md completed
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MATRIX_DIR="$(dirname "$SCRIPT_DIR")"
CONDUIT_DIR="$MATRIX_DIR/conduit"
BACKUP_DIR="$MATRIX_DIR/backups"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
preflight_check() {
log_info "Running preflight checks..."
# Check Docker
if ! command -v docker &> /dev/null; then
log_error "Docker not found. Install per prerequisites.md"
exit 1
fi
# Check Docker Compose
if ! docker compose version &> /dev/null && ! docker-compose version &> /dev/null; then
log_error "Docker Compose not found. Install per prerequisites.md"
exit 1
fi
# Check .env exists
if [[ ! -f "$CONDUIT_DIR/.env" ]]; then
log_error ".env file missing at $CONDUIT_DIR/.env"
log_warn "Copy from .env.example and configure:"
log_warn " cp $CONDUIT_DIR/.env.example $CONDUIT_DIR/.env"
log_warn " nano $CONDUIT_DIR/.env"
exit 1
fi
# Check config values
if grep -q "CHANGE_THIS" "$CONDUIT_DIR/.env"; then
log_error ".env contains placeholder values"
log_warn "Edit $CONDUIT_DIR/.env and set real values"
exit 1
fi
# Check ports
for port in 80 443 8448; do
if ss -tlnp | grep -q ":$port "; then
log_warn "Port $port is already in use"
fi
done
log_info "Preflight checks passed"
}
cmd_install() {
log_info "Installing Conduit Matrix homeserver..."
preflight_check
# Create data directory
mkdir -p "$CONDUIT_DIR/data"
# Set permissions
# Conduit runs as uid 1000 inside container
sudo chown -R 1000:1000 "$CONDUIT_DIR/data" || true
# Pull images
cd "$CONDUIT_DIR"
docker compose pull
log_info "Installation complete. Run './deploy-conduit.sh start' to begin"
log_warn "IMPORTANT: Create admin account immediately after first start"
log_warn " docker exec -it matrix-conduit register_new_matrix_user -c /var/lib/matrix-conduit"
}
cmd_start() {
log_info "Starting Conduit Matrix homeserver..."
cd "$CONDUIT_DIR"
docker compose up -d
log_info "Waiting for healthcheck..."
sleep 5
# Wait for healthy
for i in {1..30}; do
if docker compose ps conduit | grep -q "healthy"; then
log_info "Conduit is healthy and running!"
log_info "Server URL: https://$(grep DOMAIN .env | cut -d'=' -f2 | tr -d '"')"
return 0
fi
echo -n "."
sleep 2
done
log_error "Conduit failed to become healthy"
docker compose logs --tail=50 conduit
exit 1
}
cmd_stop() {
log_info "Stopping Conduit Matrix homeserver..."
cd "$CONDUIT_DIR"
docker compose down
log_info "Conduit stopped"
}
cmd_logs() {
cd "$CONDUIT_DIR"
docker compose logs -f "$@"
}
cmd_status() {
log_info "Matrix/Conduit Status:"
cd "$CONDUIT_DIR"
docker compose ps
# Federation check
DOMAIN=$(grep DOMAIN .env | cut -d'=' -f2 | tr -d '"')
log_info "Federation check:"
curl -s "https://$DOMAIN/.well-known/matrix/server" 2>/dev/null | head -5 || echo "Server info not available (expected if not yet running)"
}
cmd_backup() {
local backup_name="conduit-$(date +%Y%m%d-%H%M%S).tar.gz"
mkdir -p "$BACKUP_DIR"
log_info "Creating backup: $backup_name"
# Stop conduit briefly for consistent backup
cd "$CONDUIT_DIR"
docker compose stop conduit
tar czf "$BACKUP_DIR/$backup_name" -C "$CONDUIT_DIR" data
docker compose start conduit
log_info "Backup complete: $BACKUP_DIR/$backup_name"
}
cmd_admin() {
log_info "Opening admin shell in Conduit container..."
log_warn "Use: register_new_matrix_user -c /var/lib/matrix-conduit for account creation"
docker exec -it matrix-conduit bash
}
# Main command dispatcher
case "${1:-help}" in
install)
cmd_install
;;
start)
cmd_start
;;
stop)
cmd_stop
;;
restart)
cmd_stop
sleep 2
cmd_start
;;
logs)
shift
cmd_logs "$@"
;;
status)
cmd_status
;;
backup)
cmd_backup
;;
admin)
cmd_admin
;;
*)
echo "Conduit Matrix Homeserver Deployment"
echo "Usage: $0 {install|start|stop|restart|logs|status|backup|admin}"
echo ""
echo "Commands:"
echo " install - Initial setup and image download"
echo " start - Start the homeserver"
echo " stop - Stop the homeserver"
echo " restart - Restart services"
echo " logs - View container logs"
echo " status - Check service status"
echo " backup - Create data backup"
echo " admin - Open admin shell"
echo ""
echo "Prerequisites: Docker, Docker Compose, configured .env file"
echo "See: infra/matrix/prerequisites.md"
exit 1
;;
esac

View File

@@ -1,48 +0,0 @@
# Conduit Homeserver Configuration
# Generated by Ezra as burn-mode artifact for timmy-config#166
# See docs/matrix-deployment.md for prerequisites
[global]
# Server name - MUST match SRV records and client .well-known
server_name = "tactical.local"
# Database - SQLite for single-node deployment
database_path = "/data/conduit.db"
# Port for client-server API (behind Traefik)
port = 6167
# Enable federation (server-to-server communication)
enable_federation = true
# Federation port (direct TLS, or behind Traefik TCP)
federation_port = 8448
# Max upload size (10MB default)
max_request_size = 10485760
# Media directory
media_path = "/media"
# Registration - initially closed, manual invites only
allow_registration = false
[global.well_known]
# Client .well-known - redirects to matrix.tactical.local
client = "https://matrix.tactical.local"
server = "matrix.tactical.local:8448"
[logging]
# Log to stdout (captured by Docker)
level = "info"
# Optional: structured JSON logging for log aggregation
# format = "json"
[synchronization]
# Idle connection timeout for sync requests (seconds)
idle_timeout = 300
[emergency]
# Admin contact for federation/server notices
admin_email = "admin@tactical.local"

View File

@@ -1,60 +0,0 @@
version: '3.8'
# Matrix Conduit deployment for Timmy Fleet
# Parent: timmy-config#166
# Generated: 2026-04-05
services:
conduit:
image: matrixconduit/matrix-conduit:v0.7.0
container_name: conduit-homeserver
restart: unless-stopped
volumes:
- ./matrix-data:/data
- ./media:/media
- ./conduit-config.toml:/etc/conduit/config.toml:ro
environment:
- CONDUIT_CONFIG=/etc/conduit/config.toml
networks:
- matrix
- traefik-public
labels:
# Client API (HTTPS)
- "traefik.enable=true"
- "traefik.http.routers.matrix-client.rule=Host(`matrix.tactical.local`)"
- "traefik.http.routers.matrix-client.tls=true"
- "traefik.http.routers.matrix-client.tls.certresolver=letsencrypt"
- "traefik.http.routers.matrix-client.entrypoints=websecure"
- "traefik.http.services.matrix-client.loadbalancer.server.port=6167"
# Federation (TCP 8448) - direct or via Traefik TCP entrypoint
# Option A: Direct host port mapping
# Option B: Traefik TCP router (requires Traefik federation entrypoint)
- "traefik.tcp.routers.matrix-federation.rule=HostSNI(`*`)"
- "traefik.tcp.routers.matrix-federation.entrypoints=federation"
- "traefik.tcp.services.matrix-federation.loadbalancer.server.port=8448"
# Port mappings (only needed if NOT using Traefik for federation)
# ports:
# - "8448:8448"
# Element web client (optional - can use app.element.io instead)
element:
image: vectorim/element-web:latest
container_name: element-web
restart: unless-stopped
volumes:
- ./element-config.json:/app/config.json:ro
networks:
- traefik-public
labels:
- "traefik.enable=true"
- "traefik.http.routers.element.rule=Host(`chat.tactical.local`)"
- "traefik.http.routers.element.tls=true"
- "traefik.http.routers.element.tls.certresolver=letsencrypt"
- "traefik.http.routers.element.entrypoints=websecure"
- "traefik.http.services.element.loadbalancer.server.port=80"
networks:
matrix:
internal: true
traefik-public:
external: true # Connects to timmy-home Traefik

View File

@@ -1,315 +0,0 @@
#!/usr/bin/env python3
"""
Nostur -> Gitea Ingress Bridge MVP
Reads DMs from Nostr and creates Gitea issues/comments
"""
import asyncio
import json
import os
import sys
import time
from datetime import datetime, timedelta
from urllib.request import Request, urlopen
# nostr_sdk imports
try:
from nostr_sdk import Keys, Client, Filter, Kind, NostrSigner, Timestamp, RelayUrl, PublicKey
except ImportError as e:
print(f"[ERROR] nostr_sdk import failed: {e}")
sys.exit(1)
# Configuration
GITEA = "http://143.198.27.163:3000"
RELAY_URL = "ws://localhost:2929" # Local relay
POLL_INTERVAL = 60 # Seconds between polls
ALLOWED_PUBKEYS = [] # Will load from keystore
_GITEA_TOKEN = None
# Load credentials
def load_keystore():
with open("/root/nostr-relay/keystore.json") as f:
return json.load(f)
def load_gitea_token():
global _GITEA_TOKEN
if _GITEA_TOKEN:
return _GITEA_TOKEN
for path in ["/root/.gitea_token", os.path.expanduser("~/.gitea_token")]:
try:
with open(path) as f:
_GITEA_TOKEN = f.read().strip()
if _GITEA_TOKEN:
return _GITEA_TOKEN
except FileNotFoundError:
pass
return None
def load_allowed_pubkeys():
"""Load sovereign operator pubkeys that can create work"""
keystore = load_keystore()
allowed = []
# Alexander's pubkey is the primary operator
if "alexander" in keystore:
allowed.append(keystore["alexander"].get("pubkey", ""))
allowed.append(keystore["alexander"].get("hex_public", ""))
return [p for p in allowed if p]
# Gitea API helpers
def gitea_post(path, data):
token = load_gitea_token()
if not token:
raise RuntimeError("Gitea token not available")
headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}
body = json.dumps(data).encode()
req = Request(f"{GITEA}/api/v1{path}", data=body, headers=headers, method="POST")
with urlopen(req, timeout=15) as resp:
return json.loads(resp.read().decode())
def gitea_get(path):
token = load_gitea_token()
if not token:
raise RuntimeError("Gitea token not available")
headers = {"Authorization": f"token {token}"}
req = Request(f"{GITEA}/api/v1{path}", headers=headers)
with urlopen(req, timeout=15) as resp:
return json.loads(resp.read().decode())
def create_issue(repo, title, body, assignees=None):
"""Create a Gitea issue from DM content"""
data = {
"title": f"[NOSTR] {title}",
"body": f"**Ingress via Nostr DM**\n\n{body}\n\n---\n*Created by Nostur→Gitea Bridge MVP*"
}
if assignees:
data["assignees"] = assignees
return gitea_post(f"/repos/{repo}/issues", data)
def add_comment(repo, issue_num, body):
"""Add comment to existing issue"""
return gitea_post(f"/repos/{repo}/issues/{issue_num}/comments", {
"body": f"**Nostr DM Update**\n\n{body}\n\n---\n*Posted by Bridge MVP*"
})
def get_open_issues(repo, label=None):
"""Get open issues for status summary"""
path = f"/repos/{repo}/issues?state=open&limit=20"
if label:
path += f"&labels={label}"
return gitea_get(path)
# DM Content Processing
def parse_dm_command(content):
"""
Parse DM content for commands:
- 'status' -> return queue summary
- 'create <repo> <title>' -> create issue
- 'comment <repo> #<num> <text>' -> add comment
"""
content = content.strip()
lines = content.split('\n')
first_line = lines[0].strip().lower()
if first_line == 'status' or first_line.startswith('status'):
return {'cmd': 'status', 'repo': 'Timmy_Foundation/the-nexus'}
if first_line.startswith('create '):
parts = content[7:].split(' ', 1) # Skip 'create '
if len(parts) >= 2:
repo = parts[0] if '/' in parts[0] else f"Timmy_Foundation/{parts[0]}"
return {'cmd': 'create', 'repo': repo, 'title': parts[1], 'body': '\n'.join(lines[1:]) if len(lines) > 1 else ''}
if first_line.startswith('comment '):
parts = content[8:].split(' ', 2) # Skip 'comment '
if len(parts) >= 3:
repo = parts[0] if '/' in parts[0] else f"Timmy_Foundation/{parts[0]}"
issue_ref = parts[1] # e.g., #123
if issue_ref.startswith('#'):
issue_num = issue_ref[1:]
return {'cmd': 'comment', 'repo': repo, 'issue': issue_num, 'body': parts[2]}
return {'cmd': 'unknown', 'raw': content}
def execute_command(cmd, author_npub):
"""Execute parsed command and return result"""
try:
if cmd['cmd'] == 'status':
issues = get_open_issues(cmd['repo'])
priority = [i for i in issues if not i.get('assignee')]
blockers = [i for i in issues if any(l['name'] == 'blocker' for l in i.get('labels', []))]
summary = f"📊 **Queue Status for {cmd['repo']}**\n\n"
summary += f"Open issues: {len(issues)}\n"
summary += f"Unassigned (priority): {len(priority)}\n"
summary += f"Blockers: {len(blockers)}\n\n"
if priority[:3]:
summary += "**Top Priority (unassigned):**\n"
for i in priority[:3]:
summary += f"- #{i['number']}: {i['title'][:50]}...\n"
return {'success': True, 'message': summary, 'action': 'status'}
elif cmd['cmd'] == 'create':
result = create_issue(cmd['repo'], cmd['title'], cmd['body'])
url = result.get('html_url', f"{GITEA}/{cmd['repo']}/issues/{result['number']}")
return {
'success': True,
'message': f"✅ Created issue #{result['number']}: {result['title']}\n🔗 {url}",
'action': 'create',
'issue_num': result['number'],
'url': url
}
elif cmd['cmd'] == 'comment':
result = add_comment(cmd['repo'], cmd['issue'], cmd['body'])
return {
'success': True,
'message': f"✅ Added comment to {cmd['repo']}#{cmd['issue']}",
'action': 'comment'
}
else:
return {'success': False, 'message': f"Unknown command. Try: status, create <repo> <title>, comment <repo> #<num> <text>"}
except Exception as e:
return {'success': False, 'message': f"Error: {str(e)}"}
# Nostr DM processing
async def poll_dms(client, signer, since_ts):
"""Poll for DMs and process commands"""
keystore = load_keystore()
allowed_pubkeys = load_allowed_pubkeys()
# Note: relay29 restricts kinds, kind 4 may be blocked
filter_dm = Filter().kind(Kind(4)).since(since_ts)
events_processed = 0
commands_executed = 0
try:
events = await client.fetch_events(filter_dm, timedelta(seconds=5))
for event in events:
author = event.author().to_hex()
author_npub = event.author().to_bech32()
# Verify sovereign identity
if author not in allowed_pubkeys:
print(f" [SKIP] Event from unauthorized pubkey: {author[:16]}...")
continue
events_processed += 1
print(f" [DM] Event {event.id().to_hex()[:16]}... from {author_npub[:20]}...")
# Decrypt content (requires NIP-44 or NIP-04 decryption)
try:
# Try to decrypt using signer's decrypt method
# Note: This is for NIP-04, NIP-44 may need different handling
decrypted = signer.decrypt(author, event.content())
content = decrypted
print(f" Content preview: {content[:80]}...")
# Parse and execute command
cmd = parse_dm_command(content)
if cmd['cmd'] != 'unknown':
result = execute_command(cmd, author_npub)
commands_executed += 1
print(f"{result.get('action', 'unknown')}: {result.get('message', '')[:60]}...")
# Send acknowledgement DM back
try:
reply_content = f"ACK: {result.get('message', 'Command processed')[:200]}"
# Build and send DM reply
recipient = PublicKey.parse(author)
# Note: Sending DMs requires proper event construction
# This is a placeholder - actual send needs NIP-04/NIP-44 event building
print(f" [ACK] Would send: {reply_content[:60]}...")
except Exception as ack_err:
print(f" [ACK ERROR] Failed to send acknowledgement: {ack_err}")
else:
print(f" [PARSE] Unrecognized command format")
except Exception as e:
print(f" [ERROR] Failed to process: {e}")
return events_processed, commands_executed
except Exception as e:
print(f"[BRIDGE] DM fetch issue (may be relay restriction): {e}")
return 0, 0
async def run_bridge_loop():
"""Main bridge loop - runs continuously"""
keystore = load_keystore()
# Initialize Allegro's keys with NostrSigner
allegro_hex = keystore["allegro"]["hex_secret"]
keys = Keys.parse(allegro_hex)
signer = NostrSigner.keys(keys)
# Create client with signer
client = Client(signer)
relay_url = RelayUrl.parse(RELAY_URL)
await client.add_relay(relay_url)
await client.connect()
print(f"[BRIDGE] Connected to relay as {keystore['allegro']['npub'][:32]}...")
print(f"[BRIDGE] Monitoring DMs from authorized pubkeys: {len(load_allowed_pubkeys())}")
print(f"[BRIDGE] Poll interval: {POLL_INTERVAL}s")
print("="*60)
last_check = Timestamp.now()
try:
while True:
print(f"\n[{datetime.utcnow().strftime('%H:%M:%S')}] Polling for DMs...")
events, commands = await poll_dms(client, signer, last_check)
last_check = Timestamp.now()
if events > 0 or commands > 0:
print(f" Processed: {events} events, {commands} commands")
else:
print(f" No new DMs")
await asyncio.sleep(POLL_INTERVAL)
except KeyboardInterrupt:
print("\n[BRIDGE] Shutting down...")
finally:
await client.disconnect()
def main():
print("="*60)
print("NOSTUR → GITEA BRIDGE MVP")
print("Continuous DM → Issue Bridge Service")
print("="*60)
# Verify keystore
keystore = load_keystore()
print(f"[INIT] Keystore loaded: {len(keystore)} identities")
print(f"[INIT] Allegro npub: {keystore['allegro']['npub'][:32]}...")
# Verify Gitea API
token = load_gitea_token()
if not token:
print("[ERROR] Gitea token not found")
sys.exit(1)
print(f"[INIT] Gitea token loaded: {token[:8]}...")
# Load allowed pubkeys
allowed = load_allowed_pubkeys()
print(f"[INIT] Allowed operators: {len(allowed)}")
for pk in allowed:
print(f" - {pk[:32]}...")
# Run bridge loop
try:
asyncio.run(run_bridge_loop())
except Exception as e:
print(f"\n[ERROR] Bridge crashed: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()

View File

@@ -1,225 +0,0 @@
{
"Timmy": {
"lane": "sovereign review, architecture, release judgment, and governing decisions",
"skills_to_practice": [
"final architectural judgment",
"release and rollback discipline",
"repo-boundary decisions",
"approval on sensitive control surfaces"
],
"missing_skills": [
"delegate routine backlog maintenance instead of carrying it personally"
],
"anti_lane": [
"routine backlog grooming",
"mechanical triage that Allegro can handle"
],
"review_checklist": [
"Does this preserve Timmy's sovereignty and repo boundaries?",
"Does this change require explicit local review before merge?",
"Is the proposed work smaller and more reversible than the previous state?"
]
},
"allegro": {
"lane": "tempo-and-dispatch, Gitea bridge, queue hygiene, and operational next-move selection",
"skills_to_practice": [
"triage discipline",
"queue balancing",
"deduplicating issues and PRs",
"clear review handoffs to Timmy"
],
"missing_skills": [
"say no to work that should stay with Timmy or a builder"
],
"anti_lane": [
"owning final architecture",
"modifying product code without explicit approval"
],
"review_checklist": [
"Is this the best next move, not just a possible move?",
"Does this reduce duplicate work or operational drift?",
"Does Timmy need to judge this before execution continues?"
]
},
"perplexity": {
"lane": "research triage, integration evaluation, architecture memos, and open-source scouting",
"skills_to_practice": [
"compressing research into decisions",
"comparing build-vs-borrow options",
"linking recommendations to issue #542 and current doctrine"
],
"missing_skills": [
"avoid generating duplicate backlog without a collapse pass"
],
"anti_lane": [
"shipping broad implementation without a bounded owner",
"opening speculative issue trees without consolidation"
],
"review_checklist": [
"Did I reduce uncertainty enough for a builder to act?",
"Did I consolidate duplicates instead of multiplying them?",
"Did I separate facts, options, and recommendation clearly?"
]
},
"ezra": {
"lane": "archival memory, RCA, onboarding, durable lessons, and operating history",
"skills_to_practice": [
"extracting durable lessons from sessions",
"writing onboarding docs",
"failure analysis and postmortems",
"turning history into doctrine"
],
"missing_skills": [
"avoid acting like the primary shipper when the work needs a builder"
],
"anti_lane": [
"owning implementation-heavy tickets without backup",
"speculative architecture beyond the historical evidence"
],
"review_checklist": [
"What durable lesson should survive this work?",
"Did I link conclusions to evidence from issues, PRs, or runtime behavior?",
"Would a new wizard onboard faster because of this artifact?"
]
},
"KimiClaw": {
"lane": "long-context reading, extraction, and synthesis before implementation",
"skills_to_practice": [
"digesting large issue threads",
"extracting action items from dense context",
"summarizing codebase slices for builders"
],
"missing_skills": [
"handoff crisp conclusions instead of staying in exploratory mode"
],
"anti_lane": [
"critical-path implementation without a bounded scope",
"becoming a second generic architecture persona"
],
"review_checklist": [
"Did I turn long context into a smaller decision surface?",
"Is my handoff specific enough for a builder to act immediately?",
"Did I avoid speculative side quests?"
]
},
"codex-agent": {
"lane": "workflow hardening, cleanup, migration verification, repo-boundary enforcement, and bounded implementation",
"skills_to_practice": [
"closing migration drift",
"cutting dead code safely",
"packaging changes as reviewable PRs"
],
"missing_skills": [
"stay out of wide ideation unless explicitly asked"
],
"anti_lane": [
"unbounded speculative architecture",
"owning social authority instead of shipping truth"
],
"review_checklist": [
"Did I verify live truth, not just repo intent?",
"Is the change smaller, cleaner, and more reversible?",
"Did I leave a reviewable trail for Timmy and Allegro?"
]
},
"groq": {
"lane": "fast bounded implementation, tactical bug fixes, and narrow feature slices",
"skills_to_practice": [
"keeping changes small",
"shipping with verification",
"staying within the acceptance criteria"
],
"missing_skills": [
"do not trade correctness for speed when the issue is ambiguous"
],
"anti_lane": [
"broad architectural design",
"open-ended exploratory research"
],
"review_checklist": [
"Is the task tightly scoped enough to finish cleanly?",
"Did I verify the fix, not just write it?",
"Did I avoid widening the blast radius?"
]
},
"manus": {
"lane": "moderate-scope support implementation and dependable follow-through on already-scoped work",
"skills_to_practice": [
"finishing bounded tasks cleanly",
"good implementation hygiene",
"clear PR summaries"
],
"missing_skills": [
"escalate when the scope stops being moderate"
],
"anti_lane": [
"owning ambiguous architecture",
"soloing sprawling multi-repo initiatives"
],
"review_checklist": [
"Is this still moderate scope?",
"Did I prove the work and summarize it clearly?",
"Should a higher-context wizard review before more expansion?"
]
},
"claude": {
"lane": "hard refactors, deep implementation, and test-heavy multi-file changes after tight scoping",
"skills_to_practice": [
"respecting scope constraints",
"deep code transformation with tests",
"explaining risks clearly in PRs"
],
"missing_skills": [
"do not let large capability turn into unsupervised backlog or code sprawl"
],
"anti_lane": [
"self-directed issue farming",
"taking broad architecture liberty without a clear charter"
],
"review_checklist": [
"Did I stay inside the scoped problem?",
"Did I leave tests or verification stronger than before?",
"Is there hidden blast radius that Timmy should see explicitly?"
]
},
"gemini": {
"lane": "frontier architecture, research-heavy prototypes, and long-range design thinking",
"skills_to_practice": [
"turning speculation into decision frameworks",
"prototype design under doctrine constraints",
"making architecture legible to builders"
],
"missing_skills": [
"collapse duplicate ideation before it becomes backlog noise"
],
"anti_lane": [
"unsupervised backlog flood",
"acting like a general execution engine for every task"
],
"review_checklist": [
"Is this recommendation strategically important enough to keep?",
"Did I compress, not expand, the decision tree?",
"Did I hand off something a builder can actually execute?"
]
},
"grok": {
"lane": "adversarial review, edge cases, and provocative alternate angles",
"skills_to_practice": [
"finding weird failure modes",
"challenging assumptions safely",
"stress-testing plans"
],
"missing_skills": [
"flag whether a provocative idea is a test, a recommendation, or a risk"
],
"anti_lane": [
"primary ownership of stable delivery",
"final architectural authority"
],
"review_checklist": [
"What assumption fails under pressure?",
"Is this edge case real enough to matter now?",
"Did I make the risk actionable instead of just surprising?"
]
}
}

View File

@@ -21,8 +21,6 @@ trigger:
repos:
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
steps:
@@ -42,20 +40,16 @@ system_prompt: |
YOUR ISSUE: #{{issue_number}} — {{issue_title}}
APPROACH (prove-first):
APPROACH (test-first):
1. Read the bug report. Understand the expected vs actual behavior.
2. Reproduce the failure with the repo's existing test or verification tooling whenever possible.
3. Add a focused regression test if the repo has a meaningful test surface for the bug.
4. Fix the code so the reproduced failure disappears.
5. Run the strongest repo-native verification you can justify — all relevant tests, not just the new one.
6. Commit: fix: <description> Fixes #{{issue_number}}
7. Push, create PR, and summarize verification plus any residual risk.
2. Write a test that REPRODUCES the bug (it should fail).
3. Fix the code so the test passes.
4. Run tox -e unit — ALL tests must pass, not just yours.
5. Commit: fix: <description> Fixes #{{issue_number}}
6. Push, create PR.
RULES:
- Never claim a fix without proving the broken behavior and the repaired behavior.
- Prefer repo-native commands over assuming tox exists.
- If the issue touches config, deploy, routing, memories, playbooks, or other control surfaces, flag it for Timmy review in the PR.
- Never fix a bug without a test that proves it was broken.
- Never use --no-verify.
- If you can't reproduce the bug, comment on the issue with what you tried and what evidence is still missing.
- If you can't reproduce the bug, comment on the issue with what you tried.
- If the fix requires >50 lines changed, decompose into sub-issues.
- Do not widen the issue into a refactor.

View File

@@ -21,8 +21,6 @@ trigger:
repos:
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
steps:
@@ -45,18 +43,15 @@ system_prompt: |
RULES:
- Lines of code is a liability. Delete as much as you create.
- All changes go through PRs. No direct pushes to main.
- Use the repo's own format, lint, and test commands rather than assuming tox.
- Every refactor must preserve behavior and explain how that was verified.
- If the change crosses repo boundaries, model-routing, deployment, or identity surfaces, stop and ask for narrower scope.
- Run tox -e format before committing. Run tox -e unit after.
- Never use --no-verify on git commands.
- Conventional commits: refactor: <description> (#{{issue_number}})
- If tests fail after 2 attempts, STOP and comment on the issue.
- Refactors exist to simplify the system, not to create a new design detour.
WORKFLOW:
1. Read the issue body for specific file paths and instructions
2. Understand the current code structure
3. Name the simplification goal before changing code
4. Make the refactoring changes
5. Run formatting and verification with repo-native commands
6. Commit, push, create PR with before/after risk summary
3. Make the refactoring changes
4. Format: tox -e format
5. Test: tox -e unit
6. Commit, push, create PR

View File

@@ -21,8 +21,6 @@ trigger:
repos:
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
steps:
@@ -48,16 +46,12 @@ system_prompt: |
6. Dependencies with known CVEs (check requirements.txt/package.json)
7. Missing input validation
8. Overly permissive file permissions
9. Privilege drift in deploy, orchestration, memory, cron, and playbook surfaces
10. Places where private data or local-only artifacts could leak into tracked repos
OUTPUT FORMAT:
For each finding, file a Gitea issue with:
Title: [security] <severity>: <description>
Body: file + line, description, why it matters, recommended fix
Body: file + line, description, recommended fix
Label: security
SEVERITY: critical / high / medium / low
Only file issues for real findings. No false positives.
Do not open duplicate issues for already-known findings; link the existing issue instead.
If a finding affects sovereignty boundaries or private-data handling, flag it clearly as such.

View File

@@ -21,8 +21,6 @@ trigger:
repos:
- Timmy_Foundation/the-nexus
- Timmy_Foundation/timmy-home
- Timmy_Foundation/timmy-config
- Timmy_Foundation/hermes-agent
steps:
@@ -44,15 +42,14 @@ system_prompt: |
RULES:
- Write tests that test behavior, not implementation details.
- Use the repo's own test entrypoints; do not assume tox exists.
- Use tox -e unit to run tests. Never run pytest directly.
- Tests must be deterministic. No flaky tests.
- Conventional commits: test: <description> (#{{issue_number}})
- If the module is hard to test, explain the design obstacle and propose the smallest next step.
- Prefer tests that protect public behavior, migration boundaries, and review-critical workflows.
- If the module is hard to test, file an issue explaining why.
WORKFLOW:
1. Read the issue for target module paths
2. Read the existing code to understand behavior
3. Write focused unit tests
4. Run the relevant verification commands — all related tests must pass
5. Commit, push, create PR with verification summary and coverage rationale
4. Run tox -e unit — all tests must pass
5. Commit, push, create PR

View File

@@ -1,36 +0,0 @@
#!/bin/bash
# Fleet Room Bootstrap Script
# Run AFTER Conduit is deployed and Alexander's admin account exists
HOMESERVER="https://matrix.fleet.tld"
ADMIN_USER="@alexander:matrix.fleet.tld"
ACCESS_TOKEN="" # Fill after login
# Room creation template
create_room() {
local room_name=$1
local room_alias=$2
local topic=$3
local preset=$4 # public_chat, private_chat, trusted_private_chat
curl -X POST "$HOMESERVER/_matrix/client/r0/createRoom" \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d "{
"name": "$room_name",
"room_alias_name": "$room_alias",
"topic": "$topic",
"preset": "$preset",
"creation_content": {"m.federate": true}
}"
}
echo "=== Fleet Room Bootstrap ==="
# Core fleet rooms
create_room "Fleet Command" "fleet-command" "Sovereign operator channel" "trusted_private_chat"
create_room "General Chat" "general" "Open discussion" "public_chat"
create_room "Agent Alerts" "agent-alerts" "Automated agent notifications" "public_chat"
create_room "Dev Channel" "dev" "Development coordination" "private_chat"
echo "Rooms created. Add users via Element or invite API."

View File

@@ -1,46 +0,0 @@
# Conduit Homeserver Configuration
# Reference: https://docs.conduit.rs/configuration.html
[global]
# Server name - MUST match your domain (e.g., matrix.fleet.tld)
server_name = "matrix.fleet.tld"
# Database backend: "rocksdb" (default) or "postgresql"
database_backend = "rocksdb"
# Connection strings (adjust if using PostgreSQL)
database_path = "/var/lib/matrix-conduit/"
# Max size for uploads (media)
max_request_size = 20_000_000 # 20MB
# Allow registration (disable after initial setup!)
allow_registration = true
# Allow guest access
allow_guest_registration = false
# Enable federation (required for fleet-wide comms)
allow_federation = true
# Allow room directory listing
allow_public_room_directory_over_federation = false
# Admin users (Matrix user IDs)
admin = ["@alexander:matrix.fleet.tld"]
# Logging
log = "info,rocket=off,_=off"
[global.address]
bind = "0.0.0.0"
port = 6167
# Optional: S3-compatible media storage offload
# [global.media]
# backend = "s3"
# region = "us-east-1"
# endpoint = "https://s3.provider.com"
# bucket = "conduit-media"
# access_key_id = ""
# secret_key = ""

View File

@@ -1,60 +0,0 @@
version: "3.8"
# Matrix/Conduit Homeserver Stack
# Deploy: docker compose up -d
# Pre-reqs: Domain with DNS A record → host IP, ports 443/8448 open
services:
conduit:
image: matrixconduit/matrix-conduit:latest
container_name: conduit-homeserver
restart: unless-stopped
ports:
- "6167:6167" # Internal HTTP (behind reverse proxy)
volumes:
- ./conduit.toml:/etc/conduit.toml:ro
- conduit_data:/var/lib/matrix-conduit
environment:
- CONDUIT_CONFIG=/etc/conduit.toml
networks:
- matrix
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:6167/_matrix/client/versions"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
# Optional: PostgreSQL for scale (comment out for SQLite default)
# postgres:
# image: postgres:15-alpine
# container_name: conduit-postgres
# restart: unless-stopped
# environment:
# POSTGRES_USER: conduit
# POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
# POSTGRES_DB: conduit
# volumes:
# - postgres_data:/var/lib/postgresql/data
# networks:
# - matrix
# Optional: Element web client (self-hosted)
element:
image: vectorim/element-web:latest
container_name: element-web
restart: unless-stopped
ports:
- "8080:80" # Expose on 8080, reverse proxy to 443
volumes:
- ./element-config.json:/app/config.json:ro
networks:
- matrix
volumes:
conduit_data:
# postgres_data:
networks:
matrix:
driver: bridge

View File

@@ -1,64 +0,0 @@
# Nginx Reverse Proxy for Matrix/Conduit
# Place in /etc/nginx/sites-available/matrix and symlink to sites-enabled
# HTTP → HTTPS redirect
server {
listen 80;
server_name matrix.fleet.tld;
return 301 https://$server_name$request_uri;
}
# Main HTTPS server (client traffic)
server {
listen 443 ssl http2;
server_name matrix.fleet.tld;
ssl_certificate /etc/letsencrypt/live/matrix.fleet.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/matrix.fleet.tld/privkey.pem;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
# Matrix client-server API
location /_matrix {
proxy_pass http://127.0.0.1:6167;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket support (for sync)
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Timeouts for long-polling
proxy_read_timeout 600s;
}
# Element web client (if self-hosting)
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
# Federation server (port 8448)
server {
listen 8448 ssl http2;
server_name matrix.fleet.tld;
ssl_certificate /etc/letsencrypt/live/matrix.fleet.tld/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/matrix.fleet.tld/privkey.pem;
location / {
proxy_pass http://127.0.0.1:6167;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}

View File

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