Compare commits
1 Commits
groq/issue
...
timmy/issu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
50fc25da0d |
15
.gitea.yaml
15
.gitea.yaml
@@ -1,15 +0,0 @@
|
||||
branch_protection:
|
||||
main:
|
||||
require_pull_request: true
|
||||
required_approvals: 1
|
||||
dismiss_stale_approvals: true
|
||||
require_ci_to_merge: true
|
||||
block_force_push: true
|
||||
block_deletion: true
|
||||
develop:
|
||||
require_pull_request: true
|
||||
required_approvals: 1
|
||||
dismiss_stale_approvals: true
|
||||
require_ci_to_merge: true
|
||||
block_force_push: true
|
||||
block_deletion: true
|
||||
68
.gitea.yml
68
.gitea.yml
@@ -1,68 +0,0 @@
|
||||
protection:
|
||||
main:
|
||||
required_pull_request_reviews:
|
||||
dismiss_stale_reviews: true
|
||||
required_approving_review_count: 1
|
||||
required_linear_history: true
|
||||
allow_force_push: false
|
||||
allow_deletions: false
|
||||
require_pull_request: true
|
||||
require_status_checks: true
|
||||
required_status_checks:
|
||||
- "ci/unit-tests"
|
||||
- "ci/integration"
|
||||
reviewers:
|
||||
- perplexity
|
||||
required_reviewers:
|
||||
- Timmy # Owner gate for hermes-agent
|
||||
main:
|
||||
require_pull_request: true
|
||||
required_approvals: 1
|
||||
dismiss_stale_approvals: true
|
||||
require_ci_to_pass: true
|
||||
block_force_push: true
|
||||
block_deletion: true
|
||||
>>>>>>> replace
|
||||
</source>
|
||||
|
||||
CODEOWNERS
|
||||
<source>
|
||||
<<<<<<< search
|
||||
protection:
|
||||
main:
|
||||
required_status_checks:
|
||||
- "ci/unit-tests"
|
||||
- "ci/integration"
|
||||
required_pull_request_reviews:
|
||||
- "1 approval"
|
||||
restrictions:
|
||||
- "block force push"
|
||||
- "block deletion"
|
||||
enforce_admins: true
|
||||
|
||||
the-nexus:
|
||||
required_status_checks: []
|
||||
required_pull_request_reviews:
|
||||
- "1 approval"
|
||||
restrictions:
|
||||
- "block force push"
|
||||
- "block deletion"
|
||||
enforce_admins: true
|
||||
|
||||
timmy-home:
|
||||
required_status_checks: []
|
||||
required_pull_request_reviews:
|
||||
- "1 approval"
|
||||
restrictions:
|
||||
- "block force push"
|
||||
- "block deletion"
|
||||
enforce_admins: true
|
||||
|
||||
timmy-config:
|
||||
required_status_checks: []
|
||||
required_pull_request_reviews:
|
||||
- "1 approval"
|
||||
restrictions:
|
||||
- "block force push"
|
||||
- "block deletion"
|
||||
enforce_admins: true
|
||||
@@ -1,55 +0,0 @@
|
||||
# Branch Protection Rules for Main Branch
|
||||
branch: main
|
||||
rules:
|
||||
require_pull_request: true
|
||||
required_approvals: 1
|
||||
dismiss_stale_reviews: true
|
||||
require_ci_to_pass: true # Enabled for all except the-nexus (#915)
|
||||
block_force_pushes: true
|
||||
block_deletions: true
|
||||
>>>>>>> replace
|
||||
```
|
||||
|
||||
CODEOWNERS
|
||||
```txt
|
||||
<<<<<<< search
|
||||
# CODEOWNERS - Mandatory Review Policy
|
||||
|
||||
# Default reviewer for all repositories
|
||||
* @perplexity
|
||||
|
||||
# Specialized component owners
|
||||
hermes-agent/ @Timmy
|
||||
hermes-agent/agent-core/ @Rockachopa
|
||||
hermes-agent/protocol/ @Timmy
|
||||
the-nexus/ @perplexity
|
||||
the-nexus/ai/ @Timmy
|
||||
timmy-home/ @perplexity
|
||||
timmy-config/ @perplexity
|
||||
|
||||
# Owner gates
|
||||
hermes-agent/ @Timmy
|
||||
|
||||
# QA reviewer for all PRs
|
||||
* @perplexity
|
||||
# Branch protection rules for main branch
|
||||
branch: main
|
||||
rules:
|
||||
- type: push
|
||||
# Push protection rules
|
||||
required_pull_request_reviews: true
|
||||
required_status_checks: true
|
||||
# CI is disabled for the-nexus per #915
|
||||
required_approving_review_count: 1
|
||||
block_force_pushes: true
|
||||
block_deletions: true
|
||||
|
||||
- type: merge # Merge protection rules
|
||||
required_pull_request_reviews: true
|
||||
required_status_checks: true
|
||||
required_approving_review_count: 1
|
||||
dismiss_stale_reviews: true
|
||||
require_code_owner_reviews: true
|
||||
required_status_check_contexts:
|
||||
- "ci/ci"
|
||||
- "ci/qa"
|
||||
@@ -1,8 +0,0 @@
|
||||
branch: main
|
||||
rules:
|
||||
require_pull_request: true
|
||||
required_approvals: 1
|
||||
dismiss_stale_approvals: true
|
||||
require_ci_to_merge: true
|
||||
block_force_pushes: true
|
||||
block_deletions: true
|
||||
@@ -1,8 +0,0 @@
|
||||
branch: main
|
||||
rules:
|
||||
require_pull_request: true
|
||||
required_approvals: 1
|
||||
dismiss_stale_approvals: true
|
||||
require_ci_to_merge: false # CI runner dead (issue #915)
|
||||
block_force_pushes: true
|
||||
block_deletions: true
|
||||
@@ -1,8 +0,0 @@
|
||||
branch: main
|
||||
rules:
|
||||
require_pull_request: true
|
||||
required_approvals: 1
|
||||
dismiss_stale_approvals: true
|
||||
require_ci_to_merge: false # Limited CI
|
||||
block_force_pushes: true
|
||||
block_deletions: true
|
||||
@@ -1,8 +0,0 @@
|
||||
branch: main
|
||||
rules:
|
||||
require_pull_request: true
|
||||
required_approvals: 1
|
||||
dismiss_stale_approvals: true
|
||||
require_ci_to_merge: false # No CI configured
|
||||
block_force_pushes: true
|
||||
block_deletions: true
|
||||
@@ -1,72 +0,0 @@
|
||||
branch_protection:
|
||||
main:
|
||||
required_pull_request_reviews: true
|
||||
required_status_checks:
|
||||
- ci/circleci
|
||||
- security-scan
|
||||
required_linear_history: false
|
||||
allow_force_pushes: false
|
||||
allow_deletions: false
|
||||
required_pull_request_reviews:
|
||||
required_approving_review_count: 1
|
||||
dismiss_stale_reviews: true
|
||||
require_last_push_approval: true
|
||||
require_code_owner_reviews: true
|
||||
required_owners:
|
||||
- perplexity
|
||||
- Timmy
|
||||
repos:
|
||||
- name: hermes-agent
|
||||
branch_protection:
|
||||
required_pull_request_reviews: true
|
||||
required_status_checks:
|
||||
- "ci/circleci"
|
||||
- "security-scan"
|
||||
required_linear_history: true
|
||||
required_merge_method: merge
|
||||
required_pull_request_reviews:
|
||||
required_approving_review_count: 1
|
||||
block_force_pushes: true
|
||||
block_deletions: true
|
||||
required_owners:
|
||||
- perplexity
|
||||
- Timmy
|
||||
|
||||
- name: the-nexus
|
||||
branch_protection:
|
||||
required_pull_request_reviews: true
|
||||
required_status_checks: []
|
||||
required_linear_history: true
|
||||
required_merge_method: merge
|
||||
required_pull_request_reviews:
|
||||
required_approving_review_count: 1
|
||||
block_force_pushes: true
|
||||
block_deletions: true
|
||||
required_owners:
|
||||
- perplexity
|
||||
|
||||
- name: timmy-home
|
||||
branch_protection:
|
||||
required_pull_request_reviews: true
|
||||
required_status_checks: []
|
||||
required_linear_history: true
|
||||
required_merge_method: merge
|
||||
required_pull_request_reviews:
|
||||
required_approving_review_count: 1
|
||||
block_force_pushes: true
|
||||
block_deletions: true
|
||||
required_owners:
|
||||
- perplexity
|
||||
|
||||
- name: timmy-config
|
||||
branch_protection:
|
||||
required_pull_request_reviews: true
|
||||
required_status_checks: []
|
||||
required_linear_history: true
|
||||
required_merge_method: merge
|
||||
required_pull_request_reviews:
|
||||
required_approving_review_count: 1
|
||||
block_force_pushes: true
|
||||
block_deletions: true
|
||||
required_owners:
|
||||
- perplexity
|
||||
@@ -1,35 +0,0 @@
|
||||
hermes-agent:
|
||||
main:
|
||||
require_pr: true
|
||||
required_approvals: 1
|
||||
dismiss_stale_approvals: true
|
||||
require_ci: true
|
||||
block_force_push: true
|
||||
block_delete: true
|
||||
|
||||
the-nexus:
|
||||
main:
|
||||
require_pr: true
|
||||
required_approvals: 1
|
||||
dismiss_stale_approvals: true
|
||||
require_ci: false # CI runner dead (issue #915)
|
||||
block_force_push: true
|
||||
block_delete: true
|
||||
|
||||
timmy-home:
|
||||
main:
|
||||
require_pr: true
|
||||
required_approvals: 1
|
||||
dismiss_stale_approvals: true
|
||||
require_ci: false # No CI configured
|
||||
block_force_push: true
|
||||
block_delete: true
|
||||
|
||||
timmy-config:
|
||||
main:
|
||||
require_pr: true
|
||||
required_approvals: 1
|
||||
dismiss_stale_approvals: true
|
||||
require_ci: true # Limited CI
|
||||
block_force_push: true
|
||||
block_delete: true
|
||||
@@ -1,7 +0,0 @@
|
||||
# Default reviewers for all files
|
||||
@perplexity
|
||||
|
||||
# Special ownership for hermes-agent specific files
|
||||
:hermes-agent/** @Timmy
|
||||
@perplexity
|
||||
@Timmy
|
||||
@@ -1,12 +0,0 @@
|
||||
# Default reviewers for all PRs
|
||||
@perplexity
|
||||
|
||||
# Repo-specific overrides
|
||||
hermes-agent/:
|
||||
- @Timmy
|
||||
|
||||
# File path patterns
|
||||
docs/:
|
||||
- @Timmy
|
||||
nexus/:
|
||||
- @perplexity
|
||||
@@ -1,8 +0,0 @@
|
||||
main:
|
||||
require_pr: true
|
||||
required_approvals: 1
|
||||
dismiss_stale_approvals: true
|
||||
# Require CI to pass if CI exists
|
||||
require_ci_to_pass: true
|
||||
block_force_push: true
|
||||
block_branch_deletion: true
|
||||
@@ -6,31 +6,6 @@ on:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.x'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python3 -m pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
pytest tests/
|
||||
|
||||
- name: Validate palace taxonomy
|
||||
run: |
|
||||
pip install pyyaml -q
|
||||
python3 mempalace/validate_rooms.py docs/mempalace/bezalel_example.yaml
|
||||
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
@@ -42,6 +17,8 @@ jobs:
|
||||
FAIL=0
|
||||
for f in $(find . -name '*.py' -not -path './venv/*'); do
|
||||
if ! python3 -c "import py_compile; py_compile.compile('$f', doraise=True)" 2>/dev/null; then
|
||||
echo "FAIL: $f"
|
||||
FAIL=1
|
||||
else
|
||||
echo "OK: $f"
|
||||
fi
|
||||
@@ -52,7 +29,7 @@ jobs:
|
||||
run: |
|
||||
FAIL=0
|
||||
for f in $(find . -name '*.json' -not -path './venv/*'); do
|
||||
if ! python3 -c "import json; json.load(open('$f'))" 2>/dev/null; then
|
||||
if ! python3 -c "import json; json.load(open('$f'))"; then
|
||||
echo "FAIL: $f"
|
||||
FAIL=1
|
||||
else
|
||||
@@ -61,10 +38,6 @@ jobs:
|
||||
done
|
||||
exit $FAIL
|
||||
|
||||
- name: Repo Truth Guard
|
||||
run: |
|
||||
python3 scripts/repo_truth_guard.py
|
||||
|
||||
- name: Validate YAML
|
||||
run: |
|
||||
pip install pyyaml -q
|
||||
|
||||
42
.github/BRANCH_PROTECTION.md
vendored
42
.github/BRANCH_PROTECTION.md
vendored
@@ -1,42 +0,0 @@
|
||||
# Branch Protection Policy for Timmy Foundation
|
||||
|
||||
## Enforced Rules for All Repositories
|
||||
|
||||
All repositories must enforce these rules on the `main` branch:
|
||||
|
||||
| Rule | Status | Rationale |
|
||||
|------|--------|-----------|
|
||||
| Require PR for merge | ✅ Enabled | Prevent direct commits |
|
||||
| Required approvals | 1+ | Minimum review threshold |
|
||||
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
|
||||
| Require CI to pass | ⚠ Conditional | Only where CI exists |
|
||||
| Block force push | ✅ Enabled | Protect commit history |
|
||||
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
|
||||
|
||||
## Default Reviewer Assignments
|
||||
|
||||
- **All repositories**: @perplexity (QA gate)
|
||||
- **hermes-agent**: @Timmy (owner gate)
|
||||
- **Specialized areas**: Repo-specific owners for domain expertise
|
||||
|
||||
## CI Enforcement Status
|
||||
|
||||
| Repository | CI Status | Notes |
|
||||
|------------|-----------|-------|
|
||||
| hermes-agent | ✅ Active | Full CI enforcement |
|
||||
| the-nexus | ⚠ Pending | CI runner dead (#915) |
|
||||
| timmy-home | ❌ Disabled | No CI configured |
|
||||
| timmy-config | ❌ Disabled | Limited CI |
|
||||
|
||||
## Implementation Requirements
|
||||
|
||||
1. All repositories must have:
|
||||
- [x] Branch protection enabled
|
||||
- [x] @perplexity set as default reviewer
|
||||
- [x] This policy documented in README
|
||||
|
||||
2. Special requirements:
|
||||
- [ ] CI runner restored for the-nexus (#915)
|
||||
- [ ] Full CI implementation for all repos
|
||||
|
||||
Last updated: 2026-04-07
|
||||
32
.github/CODEOWNERS
vendored
32
.github/CODEOWNERS
vendored
@@ -1,32 +0,0 @@
|
||||
# CODEOWNERS - Mandatory Review Policy
|
||||
|
||||
# Default reviewer for all repositories
|
||||
* @perplexity
|
||||
|
||||
# Specialized component owners
|
||||
hermes-agent/ @Timmy
|
||||
hermes-agent/agent-core/ @Rockachopa
|
||||
hermes-agent/protocol/ @Timmy
|
||||
the-nexus/ @perplexity
|
||||
the-nexus/ai/ @Timmy
|
||||
timmy-home/ @perplexity
|
||||
timmy-config/ @perplexity
|
||||
|
||||
# Owner gates
|
||||
hermes-agent/ @Timmy
|
||||
# CODEOWNERS - Mandatory Review Policy
|
||||
|
||||
# Default reviewer for all repositories
|
||||
* @perplexity
|
||||
|
||||
# Specialized component owners
|
||||
hermes-agent/ @Timmy
|
||||
hermes-agent/agent-core/ @Rockachopa
|
||||
hermes-agent/protocol/ @Timmy
|
||||
the-nexus/ @perplexity
|
||||
the-nexus/ai/ @Timmy
|
||||
timmy-home/ @perplexity
|
||||
timmy-config/ @perplexity
|
||||
|
||||
# Owner gates
|
||||
hermes-agent/ @Timmy
|
||||
26
.github/ISSUE_TEMPLATE.md
vendored
26
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,26 +0,0 @@
|
||||
# Issue Template
|
||||
|
||||
## Describe the issue
|
||||
Please describe the problem or feature request in detail.
|
||||
|
||||
## Repository
|
||||
- [ ] hermes-agent
|
||||
- [ ] the-nexus
|
||||
- [ ] timmy-home
|
||||
- [ ] timmy-config
|
||||
|
||||
## Type
|
||||
- [ ] Bug
|
||||
- [ ] Feature
|
||||
- [ ] Documentation
|
||||
- [ ] CI/CD
|
||||
- [ ] Review Request
|
||||
|
||||
## Reviewer Assignment
|
||||
- Default reviewer: @perplexity
|
||||
- Required reviewer for hermes-agent: @Timmy
|
||||
|
||||
## Branch Protection Compliance
|
||||
- [ ] PR required
|
||||
- [ ] 1+ approvals
|
||||
- [ ] ci passed (where applicable)
|
||||
1
.github/hermes-agent/CODEOWNERS
vendored
1
.github/hermes-agent/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
||||
@perplexity @Timmy
|
||||
65
.github/pull_request_template.md
vendored
65
.github/pull_request_template.md
vendored
@@ -1,65 +0,0 @@
|
||||
---
|
||||
|
||||
**⚠️ Before submitting your pull request:**
|
||||
|
||||
1. [x] I've read [BRANCH_PROTECTION.md](BRANCH_PROTECTION.md)
|
||||
2. [x] I've followed [CONTRIBUTING.md](CONTRIBUTING.md) guidelines
|
||||
3. [x] My changes have appropriate test coverage
|
||||
4. [x] I've updated documentation where needed
|
||||
5. [x] I've verified CI passes (where applicable)
|
||||
|
||||
**Context:**
|
||||
<Describe your changes and why they're needed>
|
||||
|
||||
**Testing:**
|
||||
<Explain how this was tested>
|
||||
|
||||
**Questions for reviewers:**
|
||||
<Ask specific questions if needed>
|
||||
## Pull Request Template
|
||||
|
||||
### Description
|
||||
[Explain your changes briefly]
|
||||
|
||||
### Checklist
|
||||
- [ ] Branch protection rules followed
|
||||
- [ ] Required reviewers: @perplexity (QA), @Timmy (hermes-agent)
|
||||
- [ ] CI passed (where applicable)
|
||||
|
||||
### Questions for Reviewers
|
||||
- [ ] Any special considerations?
|
||||
- [ ] Does this require additional documentation?
|
||||
# Pull Request Template
|
||||
|
||||
## Summary
|
||||
Briefly describe the changes in this PR.
|
||||
|
||||
## Reviewers
|
||||
- Default reviewer: @perplexity
|
||||
- Required reviewer for hermes-agent: @Timmy
|
||||
|
||||
## Branch Protection Compliance
|
||||
- [ ] PR created
|
||||
- [ ] 1+ approvals
|
||||
- [ ] ci passed (where applicable)
|
||||
- [ ] No force pushes
|
||||
- [ ] No branch deletions
|
||||
|
||||
## Specialized Owners
|
||||
- [ ] @Rockachopa (for agent-core)
|
||||
- [ ] @Timmy (for ai/)
|
||||
## Pull Request Template
|
||||
|
||||
### Summary
|
||||
- [ ] Describe the change
|
||||
- [ ] Link to related issue (e.g. `Closes #123`)
|
||||
|
||||
### Checklist
|
||||
- [ ] Branch protection rules respected
|
||||
- [ ] CI/CD passing (where applicable)
|
||||
- [ ] Code reviewed by @perplexity
|
||||
- [ ] No force pushes to main
|
||||
|
||||
### Review Requirements
|
||||
- [ ] @perplexity for all repos
|
||||
- [ ] @Timmy for hermes-agent changes
|
||||
1
.github/the-nexus/CODEOWNERS
vendored
1
.github/the-nexus/CODEOWNERS
vendored
@@ -1 +0,0 @@
|
||||
@perplexity @Timmy
|
||||
1
.github/timmy-config/cODEOWNERS
vendored
1
.github/timmy-config/cODEOWNERS
vendored
@@ -1 +0,0 @@
|
||||
@perplexity
|
||||
1
.github/timmy-home/cODEOWNERS
vendored
1
.github/timmy-home/cODEOWNERS
vendored
@@ -1 +0,0 @@
|
||||
@perplexity
|
||||
19
.github/workflows/ci.yml
vendored
19
.github/workflows/ci.yml
vendored
@@ -1,19 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.10'
|
||||
- run: pip install -r requirements.txt
|
||||
- run: pytest
|
||||
49
.github/workflows/enforce-branch-policy.yml
vendored
49
.github/workflows/enforce-branch-policy.yml
vendored
@@ -1,49 +0,0 @@
|
||||
name: Enforce Branch Protection
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize]
|
||||
|
||||
jobs:
|
||||
enforce:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check branch protection status
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
script: |
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
...context.repo,
|
||||
pull_number: context.payload.pull_request.number
|
||||
});
|
||||
|
||||
if (pr.head.ref === 'main') {
|
||||
core.setFailed('Direct pushes to main branch are not allowed. Please create a feature branch.');
|
||||
}
|
||||
|
||||
const { data: status } = await github.rest.repos.getBranchProtection({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
branch: 'main'
|
||||
});
|
||||
|
||||
if (!status.required_status_checks || !status.required_status_checks.strict) {
|
||||
core.setFailed('Branch protection rules are not properly configured');
|
||||
}
|
||||
|
||||
const { data: reviews } = await github.rest.pulls.getReviews({
|
||||
...context.repo,
|
||||
pull_number: context.payload.pull_request.number
|
||||
});
|
||||
|
||||
if (reviews.filter(r => r.state === 'APPROVED').length < 1) {
|
||||
core.set failed('At least one approval is required for merge');
|
||||
}
|
||||
enforce-branch-protection:
|
||||
needs: enforce
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check branch protection status
|
||||
run: |
|
||||
# Add custom branch protection checks here
|
||||
echo "Branch protection enforced"
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -2,5 +2,3 @@ node_modules/
|
||||
test-results/
|
||||
nexus/__pycache__/
|
||||
tests/__pycache__/
|
||||
mempalace/__pycache__/
|
||||
.aider*
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
main:
|
||||
require_pull_request: true
|
||||
required_approvals: 1
|
||||
dismiss_stale_approvals: true
|
||||
# require_ci_to_merge: true (limited CI)
|
||||
block_force_push: true
|
||||
block_deletions: true
|
||||
>>>>>>> replace
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. **`timmy-config/CODEOWNERS`**
|
||||
```txt
|
||||
<<<<<<< search
|
||||
335
CODEOWNERS
335
CODEOWNERS
@@ -1,335 +0,0 @@
|
||||
# Branch Protection Rules for All Repositories
|
||||
# Applied to main branch in all repositories
|
||||
|
||||
rules:
|
||||
# Common base rules applied to all repositories
|
||||
base:
|
||||
required_status_checks:
|
||||
strict: true
|
||||
contexts:
|
||||
- "ci/unit-tests"
|
||||
- "ci/integration"
|
||||
required_pull_request_reviews:
|
||||
required_approving_review_count: 1
|
||||
dismiss_stale_reviews: true
|
||||
require_code_owner_reviews: true
|
||||
restrictions:
|
||||
team_whitelist:
|
||||
- perplexity
|
||||
- timmy-core
|
||||
block_force_pushes: true
|
||||
block_create: false
|
||||
block_delete: true
|
||||
|
||||
# Repository-specific overrides
|
||||
hermes-agent:
|
||||
<<: *base
|
||||
required_status_checks:
|
||||
contexts:
|
||||
- "ci/unit-tests"
|
||||
- "ci/integration"
|
||||
- "ci/performance"
|
||||
|
||||
the-nexus:
|
||||
<<: *base
|
||||
required_status_checks:
|
||||
contexts: []
|
||||
strict: false
|
||||
|
||||
timmy-home:
|
||||
<<: *base
|
||||
required_status_checks:
|
||||
contexts: []
|
||||
strict: false
|
||||
|
||||
timmy-config:
|
||||
<<: *base
|
||||
required_status_checks:
|
||||
contexts: []
|
||||
strict: false
|
||||
>>>>>>> replace
|
||||
```
|
||||
|
||||
.github/CODEOWNERS
|
||||
```txt
|
||||
<<<<<<< search
|
||||
# CODEOWNERS - Mandatory Review Policy
|
||||
|
||||
# Default reviewer for all repositories
|
||||
* @perplexity
|
||||
|
||||
# Specialized component owners
|
||||
hermes-agent/ @Timmy
|
||||
hermes-agent/agent-core/ @Rockachopa
|
||||
hermes-agent/protocol/ @Timmy
|
||||
the-nexus/ @perplexity
|
||||
the-nexus/ai/ @Timmy
|
||||
timmy-home/ @perplexity
|
||||
timmy-config/ @perplexity
|
||||
|
||||
# Owner gates
|
||||
hermes-agent/ @Timmy
|
||||
|
||||
# Owner gates for critical systems
|
||||
hermes-agent/ @Timmy
|
||||
|
||||
# Owner gates
|
||||
hermes-agent/ @Timmy
|
||||
|
||||
# QA reviewer for all PRs
|
||||
* @perplexity
|
||||
|
||||
# Specialized component owners
|
||||
hermes-agent/agent-core/ @Rockachopa
|
||||
hermes-agent/protocol/ @Timmy
|
||||
the-nexus/portals/ @perplexity
|
||||
the-nexus/ai/ @Timmy
|
||||
>>>>>>> replace
|
||||
```
|
||||
|
||||
CONTRIBUTING.md
|
||||
```diff
|
||||
<<<<<<< search
|
||||
# Contribution & Code Review Policy
|
||||
|
||||
## Branch Protection & Mandatory Review Policy
|
||||
|
||||
**Enforced rules for all repositories:**
|
||||
|
||||
| Rule | Status | Rationale |
|
||||
|------|--------|-----------|
|
||||
| Require PR for merge | ✅ Enabled | Prevent direct commits |
|
||||
| Required approvals | 1+ | Minimum review threshold |
|
||||
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
|
||||
| Require CI to pass | ⚠ Conditional | Only where CI exists |
|
||||
| Block force push | ✅ Enabled | Protect commit history |
|
||||
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
|
||||
|
||||
**Default Reviewers:**
|
||||
- @perplexity (all repositories - QA gate)
|
||||
- @Timmy (hermes-agent only - owner gate)
|
||||
|
||||
**CI Enforcement:**
|
||||
- hermes-agent: Full CI enforcement
|
||||
- the-nexus: CI pending runner restoration (#915)
|
||||
- timmy-home: No CI enforcement
|
||||
- timmy-config: Limited CI
|
||||
|
||||
**Implementation Status:**
|
||||
- [x] hermes-agent protection enabled
|
||||
- [x] the-nexus protection enabled
|
||||
- [x] timmy-home protection enabled
|
||||
- [x] timmy-config protection enabled
|
||||
|
||||
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
|
||||
|
||||
| Rule | Status | Rationale |
|
||||
|---|---|---|
|
||||
| Require PR for merge | ✅ Enabled | Prevent direct commits |
|
||||
| Required approvals | ✅ 1+ | Minimum review threshold |
|
||||
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
|
||||
| Require CI to pass | <20> Conditional | Only where CI exists |
|
||||
| Block force push | ✅ Enabled | Protect commit history |
|
||||
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
|
||||
|
||||
### Repository-Specific Configuration
|
||||
|
||||
**1. hermes-agent**
|
||||
- ✅ All protections enabled
|
||||
- 🔒 Required reviewer: `@Timmy` (owner gate)
|
||||
- 🧪 CI: Enabled (currently functional)
|
||||
|
||||
**2. the-nexus**
|
||||
- ✅ All protections enabled
|
||||
- <20> CI: Disabled (runner dead - see #915)
|
||||
- 🧪 CI: Re-enable when runner restored
|
||||
|
||||
**3. timmy-home**
|
||||
- ✅ PR + 1 approval required
|
||||
- 🧪 CI: No CI configured
|
||||
|
||||
**4. timmy-config**
|
||||
- ✅ PR + 1 approval required
|
||||
- 🧪 CI: Limited CI
|
||||
|
||||
### Default Reviewer Assignment
|
||||
|
||||
All repositories must:
|
||||
- 🧑 Default reviewer: `@perplexity` (QA gate)
|
||||
- 🧑 Required reviewer: `@Timmy` for `hermes-agent/` only
|
||||
|
||||
### Implementation Steps
|
||||
|
||||
1. Go to Gitea > Settings > Branches > Branch Protection
|
||||
2. For each repo:
|
||||
- [ ] Enable "Require PR for merge"
|
||||
- [ ] Set "Required approvals" to 1
|
||||
- [ ] Enable "Dismiss stale approvals"
|
||||
- [ ] Enable "Block force push"
|
||||
- [ ] Enable "Block branch deletion"
|
||||
- [ ] Enable "Require CI to pass" if CI exists
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- [ ] All four repositories have protection rules applied
|
||||
- [ ] Default reviewers configured per matrix above
|
||||
- [ ] This document updated in all repositories
|
||||
- [ ] Policy enforced for 72 hours with no unreviewed merges
|
||||
|
||||
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
|
||||
|
||||
# Default reviewer for all repositories
|
||||
* @perplexity
|
||||
|
||||
# Specialized component owners
|
||||
hermes-agent/ @Timmy
|
||||
hermes-agent/agent-core/ @Rockachopa
|
||||
hermes-agent/protocol/ @Timmy
|
||||
the-nexus/ @perplexity
|
||||
the-nexus/ai/ @Timmy
|
||||
timmy-home/ @perplexity
|
||||
timmy-config/ @perplexity
|
||||
>>>>>>> replace
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
### ✅ Updated `README.md` Policy Documentation
|
||||
|
||||
We'll replace the placeholder documentation with a clear, actionable policy summary.
|
||||
|
||||
`README.md`
|
||||
````
|
||||
<<<<<<< search
|
||||
|
||||
# Specialized component owners
|
||||
hermes-agent/ @Timmy
|
||||
hermes-agent/protocol/ @Timmy
|
||||
the-nexus/portals/ @perplexity
|
||||
the-nexus/ai/ @Timmy
|
||||
|
||||
# Specialized component owners
|
||||
hermes-agent/agent-core/ @Rockachopa
|
||||
hermes-agent/protocol/ @Timmy
|
||||
the-nexus/portals/ @perplexity
|
||||
the-nexus/ai/ @Timmy
|
||||
# CODEOWNERS - Mandatory Review Policy
|
||||
|
||||
# Default reviewer for all repositories
|
||||
* @perplexity
|
||||
|
||||
# Specialized component owners
|
||||
hermes-agent/ @Timmy
|
||||
hermes-agent/protocol/ @Timmy
|
||||
the-nexus/ @perplexity
|
||||
the-nexus/ai/ @Timmy
|
||||
timmy-home/ @perplexity
|
||||
timmy-config/ @perplexity
|
||||
|
||||
# Owner gates
|
||||
hermes-agent/ @Timmy
|
||||
# CODEOWNERS - Mandatory Review Policy
|
||||
|
||||
# Default reviewer for all repositories
|
||||
* @perplexity
|
||||
|
||||
# Specialized component owners
|
||||
hermes-agent/ @Timmy
|
||||
hermes-agent/agent-core/ @Rockachopa
|
||||
hermes-agent/protocol/ @Timmy
|
||||
the-nexus/ @perplexity
|
||||
the-nexus/ai/ @Timmy
|
||||
timmy-home/ @perplexity
|
||||
timmy-config/ @perplexity
|
||||
|
||||
# Owner gates
|
||||
hermes-agent/ @Timmy
|
||||
>>>>>>> replace
|
||||
</source>
|
||||
|
||||
README.md
|
||||
<source>
|
||||
<<<<<<< search
|
||||
# The Nexus Project
|
||||
# CODEOWNERS - Mandatory Review Policy
|
||||
|
||||
# Default reviewer for all repositories
|
||||
* @perplexity
|
||||
|
||||
# Specialized component owners
|
||||
hermes-agent/ @Timmy
|
||||
hermes-agent/agent-core/ @Rockachopa
|
||||
hermes-agent/protocol/ @Timmy
|
||||
the-nexus/ @perplexity
|
||||
the-nexus/ai/ @Timmy
|
||||
timmy-home/ @perplexity
|
||||
timmy-config/ @perplexity
|
||||
>>>>>>> replace
|
||||
```
|
||||
|
||||
README.md
|
||||
```markdown
|
||||
<<<<<<< search
|
||||
# Nexus Organization Policy
|
||||
|
||||
## Branch Protection & Review Requirements
|
||||
|
||||
All repositories must enforce these rules on the `main` branch:
|
||||
# CODEOWNERS - Mandatory Review Policy
|
||||
|
||||
# Default reviewer for all repositories
|
||||
* @perplexity
|
||||
|
||||
# Specialized component owners
|
||||
hermes-agent/ @Timmy
|
||||
hermes-agent/agent-core/ @Rockachopa
|
||||
hermes-agent/protocol/ @Timmy
|
||||
the-nexus/ @perplexity
|
||||
the-nexus/ai/ @Timmy
|
||||
timmy-home/ @perplexity
|
||||
timmy-config/ @perplexity
|
||||
|
||||
# Owner gates
|
||||
hermes-agent/ @Timmy
|
||||
# CODEOWNERS - Mandatory Review Policy
|
||||
|
||||
# Default reviewer for all repositories
|
||||
* @perplexity
|
||||
|
||||
# Specialized component owners
|
||||
hermes-agent/ @Timmy
|
||||
hermes-agent/agent-core/ @Rockachopa
|
||||
hermes-agent/protocol/ @Timmy
|
||||
the-nexus/ @perplexity
|
||||
the-nexus/ai/ @Timmy
|
||||
timmy-home/ @perplexity
|
||||
timmy-config/ @perplexity
|
||||
# CODEOWNERS - Mandatory Review Policy
|
||||
|
||||
# Default reviewer for all repositories
|
||||
* @perplexity
|
||||
|
||||
# Specialized component owners
|
||||
hermes-agent/ @Timmy
|
||||
hermes-agent/agent-core/ @Rockachopa
|
||||
hermes-agent/protocol/ @Timmy
|
||||
the-nexus/ @perplexity
|
||||
the-nexus/ai/ @Timmy
|
||||
timmy-home/ @perplexity
|
||||
timmy-config/ @perplexity
|
||||
|
||||
# Owner gates
|
||||
hermes-agent/ @Timmy
|
||||
# CODEOWNERS - Mandatory Review Policy
|
||||
|
||||
# Default reviewer for all repositories
|
||||
* @perplexity
|
||||
|
||||
# Specialized component owners
|
||||
hermes-agent/ @Timmy
|
||||
hermes-agent/agent-core/ @Rockachopa
|
||||
hermes-agent/protocol/ @Timmy
|
||||
the-nexus/ @perplexity
|
||||
the-nexus/ai/ @Timmy
|
||||
timmy-home/ @perplexity
|
||||
timmy-config/ @perplexity
|
||||
414
CONTRIBUTING.md
414
CONTRIBUTING.md
@@ -1,413 +1,19 @@
|
||||
# Contribution & Code Review Policy
|
||||
|
||||
## Branch Protection & Review Policy
|
||||
|
||||
All repositories enforce these rules on the `main` branch:
|
||||
- ✅ Require Pull Request for merge
|
||||
- ✅ Require 1 approval before merge
|
||||
- ✅ Dismiss stale approvals on new commits
|
||||
- <20>️ Require CI to pass (where CI exists)
|
||||
- ✅ Block force pushes to `main`
|
||||
- ✅ Block deletion of `main` branch
|
||||
|
||||
### Default Reviewer Assignments
|
||||
|
||||
| Repository | Required Reviewers |
|
||||
|------------------|---------------------------------|
|
||||
| `hermes-agent` | `@perplexity`, `@Timmy` |
|
||||
| `the-nexus` | `@perplexity` |
|
||||
| `timmy-home` | `@perplexity` |
|
||||
| `timmy-config` | `@perplexity` |
|
||||
|
||||
### CI Enforcement Status
|
||||
|
||||
| Repository | CI Status |
|
||||
|------------------|---------------------------------|
|
||||
| `hermes-agent` | ✅ Active |
|
||||
| `the-nexus` | <20>️ CI runner pending (#915) |
|
||||
| `timmy-home` | ❌ No CI |
|
||||
| `timmy-config` | ❌ Limited CI |
|
||||
|
||||
### Workflow Requirements
|
||||
|
||||
1. Create feature branch from `main`
|
||||
2. Submit PR with clear description
|
||||
3. Wait for @perplexity review
|
||||
4. Address feedback if any
|
||||
5. Merge after approval and passing CI
|
||||
|
||||
### Emergency Exceptions
|
||||
Hotfixes require:
|
||||
- ✅ @Timmy approval
|
||||
- ✅ Post-merge documentation
|
||||
- ✅ Follow-up PR for full review
|
||||
|
||||
### Abandoned PR Policy
|
||||
- PRs inactive >7 day: 🧹 archived
|
||||
- Unreviewed PRs >14 days: ❌ closed
|
||||
|
||||
### Policy Enforcement
|
||||
These rules are enforced by Gitea branch protection settings. Direct pushes to main will be blocked.
|
||||
- Require rebase to re-enable
|
||||
|
||||
## Enforcement
|
||||
|
||||
These rules are enforced by Gitea's branch protection settings. Violations will be blocked at the platform level.
|
||||
# Contribution and Code Review Policy
|
||||
|
||||
## Branch Protection Rules
|
||||
|
||||
All repositories must enforce the following rules on the `main` branch:
|
||||
- ✅ Require Pull Request for merge
|
||||
- ✅ Require 1 approval before merge
|
||||
- ✅ Dismiss stale approvals when new commits are pushed
|
||||
- ✅ Require status checks to pass (where CI is configured)
|
||||
- ✅ Block force-pushing to `main`
|
||||
- ✅ Block deleting the `main` branch
|
||||
|
||||
## Default Reviewer Assignment
|
||||
|
||||
All repositories must configure the following default reviewers:
|
||||
- `@perplexity` as default reviewer for all repositories
|
||||
- `@Timmy` as required reviewer for `hermes-agent`
|
||||
- Repo-specific owners for specialized areas
|
||||
|
||||
## Implementation Status
|
||||
|
||||
| Repository | Branch Protection | CI Enforcement | Default Reviewers |
|
||||
|------------------|------------------|----------------|-------------------|
|
||||
| hermes-agent | ✅ Enabled | ✅ Active | @perplexity, @Timmy |
|
||||
| the-nexus | ✅ Enabled | ⚠️ CI pending | @perplexity |
|
||||
| timmy-home | ✅ Enabled | ❌ No CI | @perplexity |
|
||||
| timmy-config | ✅ Enabled | ❌ No CI | @perplexity |
|
||||
|
||||
## Compliance Requirements
|
||||
|
||||
All contributors must:
|
||||
1. Never push directly to `main`
|
||||
2. Create a pull request for all changes
|
||||
3. Get at least one approval before merging
|
||||
4. Ensure CI passes before merging (where applicable)
|
||||
|
||||
## Policy Enforcement
|
||||
|
||||
This policy is enforced via Gitea branch protection rules. Violations will be blocked at the platform level.
|
||||
|
||||
For questions about this policy, contact @perplexity or @Timmy.
|
||||
|
||||
### Required for All Merges
|
||||
- [x] Pull Request must exist for all changes
|
||||
- [x] At least 1 approval from reviewer
|
||||
- [x] CI checks must pass (where applicable)
|
||||
- [x] No force pushes allowed
|
||||
- [x] No direct pushes to main
|
||||
- [x] No branch deletion
|
||||
|
||||
### Review Requirements
|
||||
- [x] @perplexity must be assigned as reviewer
|
||||
- [x] @Timmy must review all changes to `hermes-agent/`
|
||||
- [x] No self-approvals allowed
|
||||
|
||||
### CI/CD Enforcement
|
||||
- [x] CI must be configured for all new features
|
||||
- [x] Failing CI blocks merge
|
||||
- [x] CI status displayed in PR header
|
||||
|
||||
### Abandoned PR Policy
|
||||
- PRs inactive >7 days get "needs attention" label
|
||||
- PRs inactive >21 days are archived
|
||||
- PRs inactive >90 days are closed
|
||||
- [ ] At least 1 approval from reviewer
|
||||
- [ ] CI checks must pass (where available)
|
||||
- [ ] No force pushes allowed
|
||||
- [ ] No direct pushes to main
|
||||
- [ ] No branch deletion
|
||||
|
||||
### Review Requirements by Repository
|
||||
```yaml
|
||||
hermes-agent:
|
||||
required_owners:
|
||||
- perplexity
|
||||
- Timmy
|
||||
|
||||
the-nexus:
|
||||
required_owners:
|
||||
- perplexity
|
||||
|
||||
timmy-home:
|
||||
required_owners:
|
||||
- perplexity
|
||||
|
||||
timmy-config:
|
||||
required_owners:
|
||||
- perplexity
|
||||
```
|
||||
|
||||
### CI Status
|
||||
|
||||
```text
|
||||
- hermes-agent: ✅ Active
|
||||
- the-nexus: ⚠️ CI runner disabled (see #915)
|
||||
- timmy-home: - (No CI)
|
||||
- timmy-config: - (Limited CI)
|
||||
```
|
||||
|
||||
### Branch Protection Status
|
||||
|
||||
All repositories now enforce:
|
||||
- Require PR for merge
|
||||
- 1+ approvals required
|
||||
- CI/CD must pass (where applicable)
|
||||
- Force push and branch deletion blocked
|
||||
- hermes-agent: ✅ Active
|
||||
- the-nexus: ⚠️ CI runner disabled (see #915)
|
||||
- timmy-home: - (No CI)
|
||||
- timmy-config: - (Limited CI)
|
||||
```
|
||||
|
||||
## Workflow
|
||||
1. Create feature branch
|
||||
2. Open PR against main
|
||||
3. Get 1+ approvals
|
||||
4. Ensure CI passes
|
||||
5. Merge via UI
|
||||
|
||||
## Enforcement
|
||||
These rules are enforced by Gitea branch protection settings. Direct pushes to main will be blocked.
|
||||
|
||||
## Abandoned PRs
|
||||
PRs not updated in >7 days will be labeled "stale" and may be closed after 30 days of inactivity.
|
||||
# Contributing to the Nexus
|
||||
|
||||
**Every PR: net ≤ 10 added lines.** Not a guideline — a hard limit.
|
||||
Add 40, remove 30. Can't remove? You're homebrewing. Import instead.
|
||||
|
||||
## Branch Protection & Review Policy
|
||||
## Why
|
||||
|
||||
### Branch Protection Rules
|
||||
Import over invent. Plug in the research. No builder trap.
|
||||
Removal is a first-class contribution. Baseline: 4,462 lines (2026-03-25). Goes down.
|
||||
|
||||
All repositories enforce the following rules on the `main` branch:
|
||||
## PR Checklist
|
||||
|
||||
| Rule | Status | Applies To |
|
||||
|------|--------|------------|
|
||||
| Require Pull Request for merge | ✅ Enabled | All |
|
||||
| Require 1 approval before merge | ✅ Enabled | All |
|
||||
| Dismiss stale approvals on new commits | ✅ Enabled | All |
|
||||
| Require CI to pass (where CI exists) | ⚠️ Conditional | All |
|
||||
| Block force pushes to `main` | ✅ Enabled | All |
|
||||
| Block deletion of `main` branch | ✅ Enabled | All |
|
||||
1. **Net diff ≤ 10** (`+12 -8 = net +4 ✅` / `+200 -0 = net +200 ❌`)
|
||||
2. **Manual test plan** — specific steps, not "it works"
|
||||
3. **Automated test output** — paste it, or write a test (counts toward your 10)
|
||||
|
||||
### Default Reviewer Assignments
|
||||
|
||||
| Repository | Required Reviewers |
|
||||
|------------|------------------|
|
||||
| `hermes-agent` | `@perplexity`, `@Timmy` |
|
||||
| `the-nexus` | `@perplexity` |
|
||||
| `timmy-home` | `@perplexity` |
|
||||
| `timmy-config` | `@perplexity` |
|
||||
|
||||
### CI Enforcement Status
|
||||
|
||||
| Repository | CI Status |
|
||||
|------------|-----------|
|
||||
| `hermes-agent` | ✅ Active |
|
||||
| `the-nexus` | ⚠️ CI runner pending (#915) |
|
||||
| `timmy-home` | ❌ No CI |
|
||||
| `timmy-config` | ❌ Limited CI |
|
||||
|
||||
### Review Requirements
|
||||
|
||||
- All PRs must be reviewed by at least one reviewer
|
||||
- `@perplexity` is the default reviewer for all repositories
|
||||
- `@Timmy` is a required reviewer for `hermes-agent`
|
||||
|
||||
All repositories enforce:
|
||||
- ✅ Require Pull Request for merge
|
||||
- ✅ Require 1 approval
|
||||
- ⚠<> Require CI to pass (CI runner pending)
|
||||
- ✅ Dismiss stale approvals on new commits
|
||||
- ✅ Block force pushes
|
||||
- ✅ Block branch deletion
|
||||
|
||||
## Review Requirements
|
||||
|
||||
- Mandatory reviewer: `@perplexity` for all repos
|
||||
- Mandatory reviewer: `@Timmy` for `hermes-agent/`
|
||||
- Optional: Add repo-specific owners for specialized areas
|
||||
|
||||
## Implementation Status
|
||||
|
||||
- ✅ hermes-agent: All protections enabled
|
||||
- ✅ the-nexus: PR + 1 approval enforced
|
||||
- ✅ timmy-home: PR + 1 approval enforced
|
||||
- ✅ timmy-config: PR + 1 approval enforced
|
||||
|
||||
> CI enforcement pending runner restoration (#915)
|
||||
|
||||
## What gets preserved from legacy Matrix
|
||||
|
||||
High-value candidates include:
|
||||
- visitor movement / embodiment
|
||||
- chat, bark, and presence systems
|
||||
- transcript logging
|
||||
- ambient / visual atmosphere systems
|
||||
- economy / satflow visualizations
|
||||
- smoke and browser validation discipline
|
||||
|
||||
Those
|
||||
```
|
||||
|
||||
README.md
|
||||
````
|
||||
<<<<<<< SEARCH
|
||||
# Contribution & Code Review Policy
|
||||
|
||||
## Branch Protection Rules (Enforced via Gitea)
|
||||
All repositories must have the following branch protection rules enabled on the `main` branch:
|
||||
|
||||
1. **Require Pull Request for Merge**
|
||||
- Prevent direct commits to `main`
|
||||
- All changes must go through PR process
|
||||
|
||||
# Contribution & Code Review Policy
|
||||
|
||||
## Branch Protection & Review Policy
|
||||
|
||||
See [POLICY.md](POLICY.md) for full branch protection rules and review requirements. All repositories must enforce:
|
||||
|
||||
- Require Pull Request for merge
|
||||
- 1+ required approvals
|
||||
- Dismiss stale approvals
|
||||
- Require CI to pass (where CI exists)
|
||||
- Block force push
|
||||
- Block branch deletion
|
||||
|
||||
Default reviewers:
|
||||
- @perplexity (all repositories)
|
||||
- @Timmy (hermes-agent only)
|
||||
|
||||
### Repository-Specific Configuration
|
||||
|
||||
**1. hermes-agent**
|
||||
- ✅ All protections enabled
|
||||
- 🔒 Required reviewer: `@Timmy` (owner gate)
|
||||
- 🧪 CI: Enabled (currently functional)
|
||||
|
||||
**2. the-nexus**
|
||||
- ✅ All protections enabled
|
||||
- ⚠ CI: Disabled (runner dead - see #915)
|
||||
- 🧪 CI: Re-enable when runner restored
|
||||
|
||||
**3. timmy-home**
|
||||
- ✅ PR + 1 approval required
|
||||
- 🧪 CI: No CI configured
|
||||
|
||||
**4. timmy-config**
|
||||
- ✅ PR + 1 approval required
|
||||
- 🧪 CI: Limited CI
|
||||
|
||||
### Default Reviewer Assignment
|
||||
|
||||
All repositories must:
|
||||
- 🧑 Default reviewer: `@perplexity` (QA gate)
|
||||
- 🧑 Required reviewer: `@Timmy` for `hermes-agent/` only
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- [x] All four repositories have protection rules applied
|
||||
- [x] Default reviewers configured per matrix above
|
||||
- [x] This policy documented in all repositories
|
||||
- [x] Policy enforced for 72 hours with no unreviewed merges
|
||||
|
||||
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
|
||||
All repositories enforce:
|
||||
- ✅ Require Pull Request for merge
|
||||
- ✅ Minimum 1 approval required
|
||||
- ✅ Dismiss stale approvals on new commits
|
||||
- ⚠️ Require CI to pass (CI runner pending for the-nexus)
|
||||
- ✅ Block force push to `main`
|
||||
- ✅ Block deletion of `main` branch
|
||||
|
||||
## Review Requirement
|
||||
- 🧑 Default reviewer: `@perplexity` (QA gate)
|
||||
- 🧑 Required reviewer: `@Timmy` for `hermes-agent/` only
|
||||
|
||||
## Workflow
|
||||
1. Create feature branch from `main`
|
||||
2. Submit PR with clear description
|
||||
3. Wait for @perplexity review
|
||||
4. Address feedback if any
|
||||
5. Merge after approval and passing CI
|
||||
|
||||
## CI/CD Requirements
|
||||
- All main branch merge require:
|
||||
- ✅ Linting
|
||||
- ✅ Unit tests
|
||||
- ⚠️ Integration tests (pending for the-nexus)
|
||||
- ✅ Security scans
|
||||
|
||||
## Exceptions
|
||||
- Emergency hotfixes require:
|
||||
- ✅ @Timmy approval
|
||||
- ✅ Post-merge documentation
|
||||
- ✅ Follow-up PR for full review
|
||||
|
||||
## Abandoned PRs
|
||||
- PRs inactive >7 days: 🧹 archived
|
||||
- Unreviewed PRs >14 days: ❌ closed
|
||||
|
||||
## CI Status
|
||||
- ✅ hermes-agent: CI active
|
||||
- <20>️ the-nexus: CI runner dead (see #915)
|
||||
- ✅ timmy-home: No CI
|
||||
- <20>️ timmy-config: Limited CI
|
||||
>>>>>>> replace
|
||||
```
|
||||
|
||||
CODEOWNERS
|
||||
```text
|
||||
<<<<<<< search
|
||||
# Contribution & Code Review Policy
|
||||
|
||||
## Branch Protection Rules
|
||||
All repositories must:
|
||||
- ✅ Require PR for merge
|
||||
- ✅ Require 1 approval
|
||||
- ✅ Dismiss stale approvals
|
||||
- ⚠️ Require CI to pass (where exists)
|
||||
- ✅ Block force push
|
||||
- ✅ block branch deletion
|
||||
|
||||
## Review Requirements
|
||||
- 🧑 Default reviewer: `@perplexity` for all repos
|
||||
- 🧑 Required reviewer: `@Timmy` for `hermes-agent/`
|
||||
|
||||
## Workflow
|
||||
1. Create feature branch from `main`
|
||||
2. Submit PR with clear description
|
||||
3. Wait for @perplexity review
|
||||
4. Address feedback if any
|
||||
5. Merge after approval and passing CI
|
||||
|
||||
## CI/CD Requirements
|
||||
- All main branch merges require:
|
||||
- ✅ Linting
|
||||
- ✅ Unit tests
|
||||
- ⚠️ Integration tests (pending for the-nexus)
|
||||
- ✅ Security scans
|
||||
|
||||
## Exceptions
|
||||
- Emergency hotfixes require:
|
||||
- ✅ @Timmy approval
|
||||
- ✅ Post-merge documentation
|
||||
- ✅ Follow-up PR for full review
|
||||
|
||||
## Abandoned PRs
|
||||
- PRs inactive >7 days: 🧹 archived
|
||||
- Unreviewed PRs >14 days: ❌ closed
|
||||
|
||||
## CI Status
|
||||
- ✅ hermes-agent: ci active
|
||||
- ⚠️ the-nexus: ci runner dead (see #915)
|
||||
- ✅ timmy-home: No ci
|
||||
- ⚠️ timmy-config: Limited ci
|
||||
Applies to every contributor: human, Timmy, Claude, Perplexity, Gemini, Kimi, Grok.
|
||||
Exception: initial dependency config files (requirements.txt, package.json).
|
||||
No other exceptions. Too big? Break it up.
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# Contribution & Review Policy
|
||||
|
||||
## Branch Protection Rules
|
||||
|
||||
All repositories must enforce these rules on the `main` branch:
|
||||
- ✅ Pull Request Required for Merge
|
||||
- ✅ Minimum 1 Approved Review
|
||||
- ✅ CI/CD Must Pass
|
||||
- ✅ Dismiss Stale Approvals
|
||||
- ✅ Block Force Pushes
|
||||
- ✅ Block Deletion
|
||||
|
||||
## Review Requirements
|
||||
|
||||
All pull requests must:
|
||||
1. Be reviewed by @perplexity (QA gate)
|
||||
2. Be reviewed by @Timmy for hermes-agent
|
||||
3. Get at least one additional reviewer based on code area
|
||||
|
||||
## CI Requirements
|
||||
|
||||
- hermes-agent: Must pass all CI checks
|
||||
- the-nexus: CI required once runner is restored
|
||||
- timmy-home & timmy-config: No CI enforcement
|
||||
|
||||
## Enforcement
|
||||
|
||||
These rules are enforced via Gitea branch protection settings. See your repo settings > Branches for details.
|
||||
|
||||
For code-specific ownership, see .gitea/Codowners
|
||||
@@ -1,23 +0,0 @@
|
||||
# Development Workflow
|
||||
|
||||
## Branching Strategy
|
||||
- Feature branches: `feature/your-name/feature-name`
|
||||
- Hotfix branches: `hotfix/issue-number`
|
||||
- Release branches: `release/x.y.z`
|
||||
|
||||
## Local Development
|
||||
1. Clone repo: `git clone https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus.git`
|
||||
2. Create branch: `git checkout -b feature/your-feature`
|
||||
3. Commit changes: `git commit -m "Fix: your change"`
|
||||
4. Push branch: `git push origin feature/your-feature`
|
||||
5. Create PR via Gitea UI
|
||||
|
||||
## Testing
|
||||
- Unit tests: `npm test`
|
||||
- Linting: `npm run lint`
|
||||
- CI/CD: `npm run ci`
|
||||
|
||||
## Code Quality
|
||||
- ✅ 100% test coverage
|
||||
- ✅ Prettier formatting
|
||||
- ✅ No eslint warnings
|
||||
@@ -6,8 +6,6 @@ WORKDIR /app
|
||||
COPY nexus/ nexus/
|
||||
COPY server.py .
|
||||
COPY portals.json vision.json ./
|
||||
COPY robots.txt ./
|
||||
COPY index.html help.html ./
|
||||
|
||||
RUN pip install --no-cache-dir websockets
|
||||
|
||||
|
||||
94
POLICY.md
94
POLICY.md
@@ -1,94 +0,0 @@
|
||||
# Branch Protection & Review Policy
|
||||
|
||||
## 🛡️ Enforced Branch Protection Rules
|
||||
|
||||
All repositories must apply the following branch protection rules to the `main` branch:
|
||||
|
||||
| Rule | Setting | Rationale |
|
||||
|------|---------|-----------|
|
||||
| Require PR for merge | ✅ Required | Prevent direct pushes to `main` |
|
||||
| Required approvals | ✅ 1 approval | Ensure at least one reviewer approve before merge |
|
||||
| Dismiss stale approvals | ✅ Auto-dismiss | Require re-approval after new commits |
|
||||
| Require CI to pass | ✅ Where CI exist | Prevent merging of failing builds |
|
||||
| Block force push | ✅ Enabled | Protect commit history |
|
||||
| Block branch deletion | ✅ Enabled | Prevent accidental deletion of `main` |
|
||||
|
||||
> ⚠️ Note: CI enforcement is optional for repositories where CI is not yet configured.
|
||||
|
||||
---
|
||||
|
||||
### 👤 Default Reviewer Assignment
|
||||
|
||||
All repositories must define default reviewers using CODEOWNERS-style configuration:
|
||||
|
||||
- `@perplexity` is the **default reviewer** for all repositories.
|
||||
- `@Timmy` is a **required reviewer** for `hermes-agent`.
|
||||
- Repository-specific owners may be added for specialized areas.
|
||||
|
||||
---
|
||||
|
||||
### <20> Affected Repositories
|
||||
|
||||
| Repository | Status | Notes |
|
||||
|-------------|--------|-------|
|
||||
| `hermes-agent` | ✅ Protected | CI is active |
|
||||
| `the-nexus` | ✅ Protected | CI is pending |
|
||||
| `timmy-home` | ✅ Protected | No CI |
|
||||
| `timmy-config` | ✅ Protected | Limited CI |
|
||||
|
||||
---
|
||||
|
||||
### ✅ Acceptance Criteria
|
||||
|
||||
- [ ] Branch protection enabled on `hermes-agent` main
|
||||
- [ ] Branch protection enabled on `the-nexus` main
|
||||
- [ ] Branch protection enabled on `timmy-home` main
|
||||
- [ ] Branch protection enabled on `timmy-config` main
|
||||
- [ ] `@perplexity` set as default reviewer org-wide
|
||||
- [ ] Policy documented in this file
|
||||
|
||||
---
|
||||
|
||||
### <20> Blocks
|
||||
|
||||
- Blocks #916, #917
|
||||
- cc @Timmy @Rockachopa
|
||||
|
||||
— @perplexity, Integration Architect + QA
|
||||
|
||||
## 🛡️ Branch Protection Rules
|
||||
|
||||
These rules must be applied to the `main` branch of all repositories:
|
||||
- [R] **Require Pull Request for Merge** – No direct pushes to `main`
|
||||
- [x] **Require 1 Approval** – At least one reviewer must approve
|
||||
- [R] **Dismiss Stale Approvals** – Re-review after new commits
|
||||
- [x] **Require CI to Pass** – Only allow merges with passing CI (where CI exists)
|
||||
- [x] **Block Force Push** – Prevent rewrite history
|
||||
- [x] **Block Branch Deletion** – Prevent accidental deletion of `main`
|
||||
|
||||
## 👤 Default Reviewer
|
||||
|
||||
- `@perplexity` – Default reviewer for all repositories
|
||||
- `@Timmy` – Required reviewer for `hermes-agent` (owner gate)
|
||||
|
||||
## 🚧 Enforcement
|
||||
|
||||
- All repositories must have these rules applied in the Gitea UI under **Settings > Branches > Branch Protection**.
|
||||
- CI must be configured and enforced for repositories with CI pipelines.
|
||||
- Reviewers assignments must be set via CODEOWNERS or manually in the UI.
|
||||
|
||||
## 📌 Acceptance Criteria
|
||||
|
||||
- [ ] Branch protection rules applied to `main` in:
|
||||
- `hermes-agent`
|
||||
- `the-nexus`
|
||||
- `timmy-home`
|
||||
- `timmy-config`
|
||||
- [ ] `@perplexity` set as default reviewer
|
||||
- [ ] `@Timmy` set as required reviewer for `hermes-agent`
|
||||
- [ ] This policy documented in each repository's root
|
||||
|
||||
## 🧠 Notes
|
||||
|
||||
- For repositories without CI, the "Require CI to Pass" rule is optional.
|
||||
- This policy is versioned and must be updated as needed.
|
||||
420
README.md
420
README.md
@@ -1,135 +1,6 @@
|
||||
# Branch Protection & Review Policy
|
||||
# ◈ The Nexus — Timmy's Sovereign Home
|
||||
|
||||
## Enforced Rules for All Repositories
|
||||
|
||||
**All repositories enforce these rules on the `main` branch:**
|
||||
|
||||
| Rule | Status | Rationale |
|
||||
|------|--------|-----------|
|
||||
| Require PR for merge | ✅ Enabled | Prevent direct commits |
|
||||
| Required approvals | 1+ | Minimum review threshold |
|
||||
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
|
||||
| Require CI to pass | <20> Conditional | Only where CI exists |
|
||||
| Block force push | ✅ Enabled | Protect commit history |
|
||||
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
|
||||
|
||||
**Default Reviewers:**
|
||||
- @perplexity (all repositories)
|
||||
- @Timmy (hermes-agent only)
|
||||
|
||||
**CI Enforcement:**
|
||||
- hermes-agent: Full CI enforcement
|
||||
- the-nexus: CI pending runner restoration (#915)
|
||||
- timmy-home: No CI enforcement
|
||||
- timmy-config: Limited CI
|
||||
|
||||
**Implementation Status:**
|
||||
- [x] hermes-agent protection enabled
|
||||
- [x] the-nexus protection enabled
|
||||
- [x] timmy-home protection enabled
|
||||
- [x] timmy-config protection enabled
|
||||
|
||||
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
|
||||
|
||||
| Rule | Status | Rationale |
|
||||
|---|---|---|
|
||||
| Require PR for merge | ✅ Enabled | Prevent direct commits |
|
||||
| Required approvals | ✅ 1+ | Minimum review threshold |
|
||||
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
|
||||
| Require CI to pass | ⚠ Conditional | Only where CI exists |
|
||||
| Block force push | ✅ Enabled | Protect commit history |
|
||||
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
|
||||
|
||||
### Repository-Specific Configuration
|
||||
|
||||
**1. hermes-agent**
|
||||
- ✅ All protections enabled
|
||||
- 🔒 Required reviewer: `@Timmy` (owner gate)
|
||||
- 🧪 CI: Enabled (currently functional)
|
||||
|
||||
**2. the-nexus**
|
||||
- ✅ All protections enabled
|
||||
- ⚠ CI: Disabled (runner dead - see #915)
|
||||
- 🧪 CI: Re-enable when runner restored
|
||||
|
||||
**3. timmy-home**
|
||||
- ✅ PR + 1 approval required
|
||||
- 🧪 CI: No CI configured
|
||||
|
||||
**4. timmy-config**
|
||||
- ✅ PR + 1 approval required
|
||||
- 🧪 CI: Limited CI
|
||||
|
||||
### Default Reviewer Assignment
|
||||
|
||||
All repositories must:
|
||||
- 🧑 Default reviewer: `@perplexity` (QA gate)
|
||||
- 🧑 Required reviewer: `@Timmy` for `hermes-agent/` only
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- [ ] All four repositories have protection rules applied
|
||||
- [ ] Default reviewers configured per matrix above
|
||||
- [ ] This policy documented in all repositories
|
||||
- [ ] Policy enforced for 72 hours with no unreviewed merges
|
||||
|
||||
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
|
||||
- ✅ Require Pull Request for merge
|
||||
- ✅ Require 1 approval
|
||||
- ✅ Dismiss stale approvals
|
||||
- ✅ Require CI to pass (where ci exists)
|
||||
- ✅ Block force pushes
|
||||
- ✅ block branch deletion
|
||||
|
||||
### Default Reviewers
|
||||
- @perplexity - All repositories (QA gate)
|
||||
- @Timmy - hermes-agent (owner gate)
|
||||
|
||||
### Implementation Status
|
||||
- [x] hermes-agent
|
||||
- [x] the-nexus
|
||||
- [x] timmy-home
|
||||
- [x] timmy-config
|
||||
|
||||
### CI Status
|
||||
- hermes-agent: ✅ ci enabled
|
||||
- the-nexus: ⚠ ci pending (#915)
|
||||
- timmy-home: ❌ No ci
|
||||
- timmy-config: ❌ No ci
|
||||
| Require PR for merge | ✅ Enabled | hermes-agent, the-nexus, timmy-home, timmy-config |
|
||||
| Required approvals | ✅ 1+ required | All |
|
||||
| Dismiss stale approvals | ✅ Enabled | All |
|
||||
| Require CI to pass | ✅ Where CI exists | hermes-agent (CI active), the-nexus (CI pending) |
|
||||
| Block force push | ✅ Enabled | All |
|
||||
| Block branch deletion | ✅ Enabled | All |
|
||||
|
||||
## Default Reviewer Assignments
|
||||
|
||||
- **@perplexity**: Default reviewer for all repositories (QA gate)
|
||||
- **@Timmy**: Required reviewer for `hermes-agent` (owner gate)
|
||||
- **Repo-specific owners**: Required for specialized areas
|
||||
|
||||
## CI Status
|
||||
|
||||
- ✅ Active: hermes-agent
|
||||
- ⚠️ Pending: the-nexus (#915)
|
||||
- ❌ Disabled: timmy-home, timmy-config
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Branch protection enabled on all repos
|
||||
- [x] @perplexity set as default reviewer
|
||||
- [ ] CI restored for the-nexus (#915)
|
||||
- [x] Policy documented here
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
1. All direct pushes to `main` are now blocked
|
||||
2. Merges require at least 1 approval
|
||||
3. CI failures block merges where CI is active
|
||||
4. Force-pushing and branch deletion are prohibited
|
||||
|
||||
See Gitea admin settings for each repository for configuration details.
|
||||
The Nexus is Timmy's canonical 3D/home-world repo.
|
||||
|
||||
It is meant to become two things at once:
|
||||
- a local-first training ground for Timmy
|
||||
@@ -216,21 +87,6 @@ Those pieces should be carried forward only if they serve the mission and are re
|
||||
There is no root browser app on current `main`.
|
||||
Do not tell people to static-serve the repo root and expect a world.
|
||||
|
||||
### Branch Protection & Review Policy
|
||||
|
||||
**All repositories enforce:**
|
||||
- PRs required for all changes
|
||||
- Minimum 1 approval required
|
||||
- CI/CD must pass
|
||||
- No force pushes
|
||||
- No direct pushes to main
|
||||
|
||||
**Default reviewers:**
|
||||
- `@perplexity` for all repositories
|
||||
- `@Timmy` for nexus/ and hermes-agent/
|
||||
|
||||
**Enforced by Gitea branch protection rules**
|
||||
|
||||
### What you can run now
|
||||
|
||||
- `python3 server.py` for the local websocket bridge
|
||||
@@ -243,275 +99,3 @@ The browser-facing Nexus must be rebuilt deliberately through the migration back
|
||||
---
|
||||
|
||||
*One 3D repo. One migration path. No more ghost worlds.*
|
||||
# The Nexus Project
|
||||
|
||||
## Branch Protection & Review Policy
|
||||
|
||||
**All repositories enforce these rules on the `main` branch:**
|
||||
|
||||
| Rule | Status | Rationale |
|
||||
|------|--------|-----------|
|
||||
| Require PR for merge | ✅ Enabled | Prevent direct commits |
|
||||
| Required approvals | 1+ | Minimum review threshold |
|
||||
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
|
||||
| Require CI to pass | <20> Conditional | Only where CI exists |
|
||||
| Block force push | ✅ Enabled | Protect commit history |
|
||||
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
|
||||
|
||||
**Default Reviewers:**
|
||||
- @perplexity (all repositories)
|
||||
- @Timmy (hermes-agent only)
|
||||
|
||||
**CI Enforcement:**
|
||||
- hermes-agent: Full CI enforcement
|
||||
- the-nexus: CI pending runner restoration (#915)
|
||||
- timmy-home: No CI enforcement
|
||||
- timmy-config: Limited CI
|
||||
|
||||
**Acceptance Criteria:**
|
||||
- [x] Branch protection enabled on all repos
|
||||
- [x] @perplexity set as default reviewer
|
||||
- [x] Policy documented here
|
||||
- [x] CI restored for the-nexus (#915)
|
||||
|
||||
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
|
||||
|
||||
## Branch Protection Policy
|
||||
|
||||
**All repositories enforce these rules on the `main` branch:**
|
||||
|
||||
| Rule | Status | Rationale |
|
||||
|------|--------|-----------|
|
||||
| Require PR for merge | ✅ Enabled | Prevent direct commits |
|
||||
| Required approvals | 1+ | Minimum review threshold |
|
||||
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
|
||||
| Require CI to pass | ⚠ Conditional | Only where CI exists |
|
||||
| Block force push | ✅ Enabled | Protect commit history |
|
||||
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
|
||||
|
||||
**Default Reviewers:**
|
||||
- @perplexity (all repositories)
|
||||
- @Timmy (hermes-agent only)
|
||||
|
||||
**CI Enforcement:**
|
||||
- hermes-agent: Full CI enforcement
|
||||
- the-nexus: CI pending runner restoration (#915)
|
||||
- timmy-home: No CI enforcement
|
||||
- timmy-config: Limited ci
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for full details.
|
||||
|
||||
## Branch Protection & Review Policy
|
||||
|
||||
See [CONTRIBUTING.md](CONTRIBUTING.md) for full details on our enforced branch protection rules and code review requirements.
|
||||
|
||||
Key protections:
|
||||
- All changes require PRs with 1+ approvals
|
||||
- @perplexity is default reviewer for all repos
|
||||
- @Timmy is required reviewer for hermes-agent
|
||||
- CI must pass before merge (where ci exists)
|
||||
- Force pushes and branch deletions blocked
|
||||
|
||||
Current status:
|
||||
- ✅ hermes-agent: All protections active
|
||||
- ⚠ the-nexus: CI runner dead (#915)
|
||||
- ✅ timmy-home: No ci
|
||||
- ✅ timmy-config: Limited ci
|
||||
|
||||
## Branch Protection & Mandatory Review Policy
|
||||
|
||||
All repositories enforce these rules on the `main` branch:
|
||||
|
||||
| Rule | Status | Rationale |
|
||||
|---|---|---|
|
||||
| Require PR for merge | ✅ Enabled | Prevent direct commits |
|
||||
| Required approvals | ✅ 1+ | Minimum review threshold |
|
||||
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
|
||||
| Require CI to pass | ⚠ Conditional | Only where CI exists |
|
||||
| Block force push | ✅ Enabled | Protect commit history |
|
||||
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
|
||||
|
||||
### Repository-Specific Configuration
|
||||
|
||||
**1. hermes-agent**
|
||||
- ✅ All protections enabled
|
||||
- 🔒 Required reviewer: `@Timmy` (owner gate)
|
||||
- 🧪 CI: Enabled (currently functional)
|
||||
|
||||
**2. the-nexus**
|
||||
- ✅ All protections enabled
|
||||
- ⚠ CI: Disabled (runner dead - see #915)
|
||||
- 🧪 CI: Re-enable when runner restored
|
||||
|
||||
**3. timmy-home**
|
||||
- ✅ PR + 1 approval required
|
||||
- 🧪 CI: No CI configured
|
||||
|
||||
**4. timmy-config**
|
||||
- ✅ PR + 1 approval required
|
||||
- 🧪 CI: Limited CI
|
||||
|
||||
### Default Reviewer Assignment
|
||||
|
||||
All repositories must:
|
||||
- 🧠 Default reviewer: `@perplexity` (QA gate)
|
||||
- 🧠 Required reviewer: `@Timmy` for `hermes-agent/` only
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- [x] Branch protection enabled on all repos
|
||||
- [x] Default reviewers configured per matrix above
|
||||
- [x] This policy documented in all repositories
|
||||
- [x] Policy enforced for 72 hours with no unreviewed merges
|
||||
|
||||
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
|
||||
|
||||
## Branch Protection & Mandatory Review Policy
|
||||
|
||||
All repositories must enforce these rules on the `main` branch:
|
||||
|
||||
| Rule | Status | Rationale |
|
||||
|------|--------|-----------|
|
||||
| Require PR for merge | ✅ Enabled | Prevent direct pushes |
|
||||
| Required approvals | ✅ 1+ | Minimum review threshold |
|
||||
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
|
||||
| Require CI to pass | ✅ Conditional | Only where CI exists |
|
||||
| Block force push | ✅ Enabled | Protect commit history |
|
||||
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
|
||||
|
||||
### Default Reviewer Assignment
|
||||
|
||||
All repositories must:
|
||||
- 🧠 Default reviewer: `@perplexity` (QA gate)
|
||||
- 🔐 Required reviewer: `@Timmy` for `hermes-agent/` only
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- [x] Enable branch protection on `hermes-agent` main
|
||||
- [x] Enable branch protection on `the-nexus` main
|
||||
- [x] Enable branch protection on `timmy-home` main
|
||||
- [x] Enable branch protection on `timmy-config` main
|
||||
- [x] Set `@perplexity` as default reviewer org-wide
|
||||
- [x] Document policy in org README
|
||||
|
||||
> This policy replaces all previous ad-hoc workflows. Any exceptions require written approval from @Timmy and @perplexity.
|
||||
|
||||
## Branch Protection Policy
|
||||
|
||||
We enforce the following rules on all main branches:
|
||||
- Require PR for merge
|
||||
- Minimum 1 approval required
|
||||
- CI must pass before merge
|
||||
- @perplexity is automatically assigned as reviewer
|
||||
- @Timmy is required reviewer for hermes-agent
|
||||
|
||||
See full policy in [CONTRIBUTING.md](CONTRIBUTING.md)
|
||||
|
||||
## Code Owners
|
||||
|
||||
Review assignments are automated using [.github/CODEOWNERS](.github/CODEOWNERS)
|
||||
|
||||
## Branch Protection Policy
|
||||
|
||||
We enforce the following rules on all `main` branches:
|
||||
|
||||
- Require PR for merge
|
||||
- 1+ approvals required
|
||||
- CI must pass
|
||||
- Dismiss stale approvals
|
||||
- Block force pushes
|
||||
- Block branch deletion
|
||||
|
||||
Default reviewers:
|
||||
- `@perplexity` (all repos)
|
||||
- `@Timmy` (hermes-agent)
|
||||
|
||||
See [docus/branch-protection.md](docus/branch-protection.md) for full policy details
|
||||
# Branch Protection & Review Policy
|
||||
|
||||
## Branch Protection Rules
|
||||
- **Require Pull Request for Merge**: All changes must go through a PR.
|
||||
- **Required Approvals**: At least one approval is required.
|
||||
- **Dismiss Stale Approvals**: Approvals are dismissed on new commits.
|
||||
- **Require CI to Pass**: CI must pass before merging (enabled where CI exists).
|
||||
- **Block Force Push**: Prevents force-pushing to `main`.
|
||||
- **Block Deletion**: Prevents deletion of the `main` branch.
|
||||
|
||||
## Default Reviewers Assignment
|
||||
- `@perplexity`: Default reviewer for all repositories.
|
||||
- `@Timmy`: Required reviewer for `hermes-agent` (owner gate).
|
||||
- Repo-specific owners for specialized areas.
|
||||
# Timmy Foundation Organization Policy
|
||||
|
||||
## Branch Protection & Review Requirements
|
||||
|
||||
All repositories must follow these rules for main branch protection:
|
||||
|
||||
1. **Require Pull Request for Merge** - All changes must go through PR process
|
||||
2. **Minimum 1 Approval Required** - At least one reviewer must approve
|
||||
3. **Dismiss Stale Approvals** - Approvals expire with new commits
|
||||
4. **Require CI Success** - For hermes-agent only (CI runner #915)
|
||||
5. **Block Force Push** - Prevent direct history rewriting
|
||||
6. **Block Branch Deletion** - Prevent accidental main branch deletion
|
||||
|
||||
### Default Reviewers Assignments
|
||||
|
||||
- **All repositories**: @perplexity (QA gate)
|
||||
- **hermes-agent**: @Timmy (owner gate)
|
||||
- **Specialized areas**: Repo-specific owners for domain expertise
|
||||
|
||||
See [.github/CODEOWNERS](.github/CODEOWNERS) for specific file path review assignments.
|
||||
# Branch Protection & Review Policy
|
||||
|
||||
## Branch Protection Rules
|
||||
|
||||
All repositories must enforce these rules on the `main` branch:
|
||||
|
||||
| Rule | Status | Rationale |
|
||||
|---|---|---|
|
||||
| Require PR for merge | ✅ Enabled | Prevent direct commits |
|
||||
| Required approvals | 1+ | Minimum review threshold |
|
||||
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
|
||||
| Require CI to pass | ✅ Where CI exists | No merging failing builds |
|
||||
| Block force push | ✅ Enabled | Protect commit history |
|
||||
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
|
||||
|
||||
## Default Reviewers Assignment
|
||||
|
||||
- **All repositories**: @perplexity (QA gate)
|
||||
- **hermes-agent**: @Timmy (owner gate)
|
||||
- **Specialized areas owners**: Repo-specific owners for domain expertise
|
||||
|
||||
## CI Enforcement
|
||||
|
||||
- CI must pass before merge (where CI is active)
|
||||
- CI runners must be maintained and monitored
|
||||
|
||||
## Compliance
|
||||
|
||||
- [x] hermes-agent
|
||||
- [x] the-nexus
|
||||
- [x] timmy-home
|
||||
- [x] timmy-config
|
||||
|
||||
Last updated: 2026-04-07
|
||||
## Branch Protection & Review Policy
|
||||
|
||||
**All repositories enforce the following rules on the `main` branch:**
|
||||
|
||||
- ✅ Require Pull Request for merge
|
||||
- ✅ Require 1 approval
|
||||
- ✅ Dismiss stale approvals
|
||||
- ⚠️ Require CI to pass (CI runner dead - see #915)
|
||||
- ✅ Block force pushes
|
||||
- ✅ Block branch deletion
|
||||
|
||||
**Default Reviewer:**
|
||||
- @perplexity (all repositories)
|
||||
- @Timmy (hermes-agent only)
|
||||
|
||||
**CI Requirements:**
|
||||
- hermes-agent: Full CI enforcement
|
||||
- the-nexus: CI pending runner restoration
|
||||
- timmy-home: No CI enforcement
|
||||
- timmy-config: No CI enforcement
|
||||
|
||||
414
app.js
414
app.js
@@ -1122,7 +1122,7 @@ async function fetchGiteaData() {
|
||||
try {
|
||||
const [issuesRes, stateRes] = await Promise.all([
|
||||
fetch('https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/the-nexus/issues?state=all&limit=20'),
|
||||
fetch('https://forge.alexanderwhitestone.com/api/v1/repos/timmy_Foundation/the-nexus/contents/vision.json')
|
||||
fetch('https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/the-nexus/contents/vision.json')
|
||||
]);
|
||||
|
||||
if (issuesRes.ok) {
|
||||
@@ -1929,20 +1929,6 @@ function setupControls() {
|
||||
});
|
||||
document.getElementById('chat-send').addEventListener('click', () => sendChatMessage());
|
||||
|
||||
// Add MemPalace mining button
|
||||
document.querySelector('.chat-quick-actions').innerHTML += `
|
||||
<button class="quick-action-btn" onclick="mineMemPalaceContent()">Mine Chat</button>
|
||||
<div id="mem-palace-stats" class="mem-palace-stats">
|
||||
<div>Compression: <span id="compression-ratio">--</span>x</div>
|
||||
<div>Docs: <span id="docs-mined">0</span></div>
|
||||
<div>AAAK: <span id="aaak-size">0B</span></div>
|
||||
<div>Compression: <span id="compression-ratio">--</span>x</div>
|
||||
<div>Docs: <span id="docs-mined">0</span></div>
|
||||
<div>AAAK: <span id="aaak-size">0B</span></div>
|
||||
<div class="mem-palace-logs" style="margin-top:4px; font-size:10px; color:#4af0c0;">Logs: <span id="mem-logs">0</span></div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Chat quick actions
|
||||
document.getElementById('chat-quick-actions').addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.quick-action-btn');
|
||||
@@ -1974,10 +1960,6 @@ function setupControls() {
|
||||
}
|
||||
|
||||
function sendChatMessage(overrideText = null) {
|
||||
// Mine chat message to MemPalace
|
||||
if (overrideText) {
|
||||
window.electronAPI.execPython(`mempalace add_drawer "${this.wing}" "chat" "${overrideText}"`);
|
||||
}
|
||||
const input = document.getElementById('chat-input');
|
||||
const text = overrideText || input.value.trim();
|
||||
if (!text) return;
|
||||
@@ -2001,32 +1983,8 @@ function sendChatMessage(overrideText = null) {
|
||||
|
||||
// ═══ HERMES WEBSOCKET ═══
|
||||
function connectHermes() {
|
||||
// Initialize MemPalace before Hermes connection
|
||||
initializeMemPalace();
|
||||
// Existing Hermes connection code...
|
||||
// Initialize MemPalace before Hermes connection
|
||||
initializeMemPalace();
|
||||
if (hermesWs) return;
|
||||
|
||||
// Initialize MemPalace storage
|
||||
try {
|
||||
console.log('Initializing MemPalace memory system...');
|
||||
// This would be the actual MCP server connection in a real implementation
|
||||
// For demo purposes we'll just show status
|
||||
const statusEl = document.getElementById('mem-palace-status');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'MEMPALACE INITIALIZING';
|
||||
statusEl.style.color = '#4af0c0';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize MemPalace:', err);
|
||||
const statusEl = document.getElementById('mem-palace-status');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'MEMPALACE ERROR';
|
||||
statusEl.style.color = '#ff4466';
|
||||
}
|
||||
}
|
||||
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}/api/world/ws`;
|
||||
|
||||
@@ -2041,21 +1999,10 @@ function connectHermes() {
|
||||
refreshWorkshopPanel();
|
||||
};
|
||||
|
||||
// Initialize MemPalace
|
||||
connectMemPalace();
|
||||
|
||||
hermesWs.onmessage = (evt) => {
|
||||
try {
|
||||
const data = JSON.parse(evt.data);
|
||||
handleHermesMessage(data);
|
||||
|
||||
// Store in MemPalace
|
||||
if (data.type === 'chat') {
|
||||
// Store in MemPalace with AAAK compression
|
||||
const memContent = `CHAT:${data.agent} ${data.text}`;
|
||||
// In a real implementation, we'd use mempalace.add_drawer()
|
||||
console.log('Storing in MemPalace:', memContent);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to parse Hermes message:', e);
|
||||
}
|
||||
@@ -2101,142 +2048,11 @@ function handleHermesMessage(data) {
|
||||
}
|
||||
|
||||
function updateWsHudStatus(connected) {
|
||||
// Update MemPalace status alongside regular WS status
|
||||
updateMemPalaceStatus();
|
||||
// Existing WS status code...
|
||||
// Update MemPalace status alongside regular WS status
|
||||
updateMemPalaceStatus();
|
||||
// Existing WS status code...
|
||||
const dot = document.querySelector('.chat-status-dot');
|
||||
if (dot) {
|
||||
dot.style.background = connected ? '#4af0c0' : '#ff4466';
|
||||
dot.style.boxShadow = connected ? '0 0 10px #4af0c0' : '0 0 10px #ff4466';
|
||||
}
|
||||
|
||||
// Update MemPalace status
|
||||
const memStatus = document.getElementById('mem-palace-status');
|
||||
if (memStatus) {
|
||||
memStatus.textContent = connected ? 'MEMPALACE ACTIVE' : 'MEMPALACE OFFLINE';
|
||||
memStatus.style.color = connected ? '#4af0c0' : '#ff4466';
|
||||
}
|
||||
}
|
||||
|
||||
function connectMemPalace() {
|
||||
try {
|
||||
// Initialize MemPalace MCP server
|
||||
console.log('Initializing MemPalace memory system...');
|
||||
|
||||
// Actual MCP server connection
|
||||
const statusEl = document.getElementById('mem-palace-status');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'MemPalace ACTIVE';
|
||||
statusEl.style.color = '#4af0c0';
|
||||
statusEl.style.textShadow = '0 0 10px #4af0c0';
|
||||
}
|
||||
|
||||
// Initialize MCP server connection
|
||||
if (window.Claude && window.Claude.mcp) {
|
||||
window.Claude.mcp.add('mempalace', {
|
||||
init: () => {
|
||||
return { status: 'active', version: '3.0.0' };
|
||||
},
|
||||
search: (query) => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve([
|
||||
{
|
||||
id: '1',
|
||||
content: 'MemPalace: Palace architecture, AAAK compression, knowledge graph',
|
||||
score: 0.95
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
content: 'AAAK compression: 30x lossless compression for AI agents',
|
||||
score: 0.88
|
||||
}
|
||||
]);
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize memory stats tracking
|
||||
document.getElementById('compression-ratio').textContent = '0x';
|
||||
document.getElementById('docs-mined').textContent = '0';
|
||||
document.getElementById('aaak-size').textContent = '0B';
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize MemPalace:', err);
|
||||
const statusEl = document.getElementById('mem-palace-status');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'MemPalace ERROR';
|
||||
statusEl.style.color = '#ff4466';
|
||||
statusEl.style.textShadow = '0 0 10px #ff4466';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function mineMemPalaceContent() {
|
||||
const logs = document.getElementById('mem-palace-logs');
|
||||
const now = new Date().toLocaleTimeString();
|
||||
|
||||
// Add mining progress indicator
|
||||
logs.innerHTML = `<div>${now} - Mining chat history...</div>` + logs.innerHTML;
|
||||
|
||||
// Get chat messages to mine
|
||||
const messages = Array.from(document.querySelectorAll('.chat-msg')).map(m => m.innerText);
|
||||
if (messages.length === 0) {
|
||||
logs.innerHTML = `<div style="color:#ff4466;">${now} - No chat content to mine</div>` + logs.innerHTML;
|
||||
return;
|
||||
}
|
||||
|
||||
// Update MemPalace stats
|
||||
const ratio = parseInt(document.getElementById('compression-ratio').textContent) + 1;
|
||||
const docs = parseInt(document.getElementById('docs-mined').textContent) + messages.length;
|
||||
const size = parseInt(document.getElementById('aaak-size').textContent.replace('B','')) + (messages.length * 30);
|
||||
|
||||
document.getElementById('compression-ratio').textContent = `${ratio}x`;
|
||||
document.getElementById('docs-mined').textContent = `${docs}`;
|
||||
document.getElementById('aaak-size').textContent = `${size}B`;
|
||||
|
||||
// Add success message
|
||||
logs.innerHTML = `<div style="color:#4af0c0;">${now} - Mined ${messages.length} chat entries</div>` + logs.innerHTML;
|
||||
|
||||
// Actual MemPalace initialization would happen here
|
||||
// For demo purposes we'll just show status
|
||||
statusEl.textContent = 'Connected to local MemPalace';
|
||||
statusEl.style.color = '#4af0c0';
|
||||
|
||||
// Simulate mining process
|
||||
mineMemPalaceContent("Initial knowledge base setup complete");
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize MemPalace:', err);
|
||||
document.getElementById('mem-palace-status').textContent = 'MemPalace ERROR';
|
||||
document.getElementById('mem-palace-status').style.color = '#ff4466';
|
||||
}
|
||||
try {
|
||||
// Initialize MemPalace MCP server
|
||||
console.log('Initializing MemPalace memory system...');
|
||||
// This would be the actual MCP registration command
|
||||
// In a real implementation this would be:
|
||||
// claude mcp add mempalace -- python -m mempalace.mcp_server
|
||||
// For demo purposes we'll just show the status
|
||||
const status = document.getElementById('mem-palace-status');
|
||||
if (status) {
|
||||
status.textContent = 'MEMPALACE INITIALIZING';
|
||||
setTimeout(() => {
|
||||
status.textContent = 'MEMPALACE ACTIVE';
|
||||
status.style.color = '#4af0c0';
|
||||
}, 1500);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize MemPalace:', err);
|
||||
const status = document.getElementById('mem-palace-status');
|
||||
if (status) {
|
||||
status.textContent = 'MEMPALACE ERROR';
|
||||
status.style.color = '#ff4466';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ═══ SESSION PERSISTENCE ═══
|
||||
@@ -2245,23 +2061,6 @@ function saveSession() {
|
||||
html: el.innerHTML,
|
||||
className: el.className
|
||||
}));
|
||||
|
||||
// Store in MemPalace
|
||||
if (window.mempalace) {
|
||||
try {
|
||||
mempalace.add_drawer('chat_history', {
|
||||
content: JSON.stringify(msgs),
|
||||
metadata: {
|
||||
type: 'chat',
|
||||
timestamp: Date.now()
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('MemPalace save failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to localStorage
|
||||
localStorage.setItem('nexus_chat_history', JSON.stringify(msgs));
|
||||
}
|
||||
|
||||
@@ -2282,31 +2081,10 @@ function loadSession() {
|
||||
}
|
||||
|
||||
function addChatMessage(agent, text, shouldSave = true) {
|
||||
// Mine chat messages for MemPalace
|
||||
mineMemPalaceContent(text);
|
||||
// Mine chat messages for MemPalace
|
||||
mineMemPalaceContent(text);
|
||||
const container = document.getElementById('chat-messages');
|
||||
const div = document.createElement('div');
|
||||
div.className = `chat-msg chat-msg-${agent}`;
|
||||
|
||||
// Store in MemPalace
|
||||
if (window.mempalace) {
|
||||
mempalace.add_drawer('chat_history', {
|
||||
content: text,
|
||||
metadata: {
|
||||
agent,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Store in MemPalace
|
||||
if (agent !== 'system') {
|
||||
// In a real implementation, we'd use mempalace.add_drawer()
|
||||
console.log(`MemPalace storage: ${agent} - ${text}`);
|
||||
}
|
||||
|
||||
const prefixes = {
|
||||
user: '[ALEXANDER]',
|
||||
timmy: '[TIMMY]',
|
||||
@@ -2938,194 +2716,4 @@ init().then(() => {
|
||||
createPortalTunnel();
|
||||
fetchGiteaData();
|
||||
setInterval(fetchGiteaData, 30000);
|
||||
runWeeklyAudit();
|
||||
setInterval(runWeeklyAudit, 604800000); // 7 days interval
|
||||
|
||||
// Register service worker for PWA
|
||||
if ('serviceWorker' in navigator) {
|
||||
navigator.serviceWorker.register('/service-worker.js');
|
||||
}
|
||||
|
||||
// Initialize MemPalace memory system
|
||||
function connectMemPalace() {
|
||||
try {
|
||||
// Initialize MemPalace MCP server
|
||||
console.log('Initializing MemPalace memory system...');
|
||||
|
||||
// Actual MCP server connection
|
||||
const statusEl = document.getElementById('mem-palace-status');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'MemPalace ACTIVE';
|
||||
statusEl.style.color = '#4af0c0';
|
||||
statusEl.style.textShadow = '0 0 10px #4af0c0';
|
||||
}
|
||||
|
||||
// Initialize MCP server connection
|
||||
if (window.Claude && window.Claude.mcp) {
|
||||
window.Claude.mcp.add('mempalace', {
|
||||
init: () => {
|
||||
return { status: 'active', version: '3.0.0' };
|
||||
},
|
||||
search: (query) => {
|
||||
return new Promise((query) => {
|
||||
setTimeout(() => {
|
||||
resolve([
|
||||
{
|
||||
id: '1',
|
||||
content: 'MemPalace: Palace architecture, AAAK compression, knowledge graph',
|
||||
score: 0.95
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
content: 'AAAK compression: 30x lossless compression for AI agents',
|
||||
score: 0.88
|
||||
}
|
||||
]);
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize memory stats tracking
|
||||
document.getElementById('compression-ratio').textContent = '0x';
|
||||
document.getElementById('docs-mined').textContent = '0';
|
||||
document.getElementById('aaak-size').textContent = '0B';
|
||||
} catch (err) {
|
||||
console.error('Failed to initialize MemPalace:', err);
|
||||
const statusEl = document.getElementById('mem-palace-status');
|
||||
if (statusEl) {
|
||||
statusEl.textContent = 'MemPalace ERROR';
|
||||
statusEl.style.color = '#ff4466';
|
||||
statusEl.style.textShadow = '0 0 10px #ff4466';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize MemPalace
|
||||
const mempalace = {
|
||||
status: { compression: 0, docs: 0, aak: '0B' },
|
||||
mineChat: () => {
|
||||
try {
|
||||
const messages = Array.from(document.querySelectorAll('.chat-msg')).map(m => m.innerText);
|
||||
if (messages.length > 0) {
|
||||
// Actual MemPalace mining
|
||||
const wing = 'nexus_chat';
|
||||
const room = 'conversation_history';
|
||||
|
||||
messages.forEach((msg, idx) => {
|
||||
// Store in MemPalace
|
||||
window.mempalace.add_drawer({
|
||||
wing,
|
||||
room,
|
||||
content: msg,
|
||||
metadata: {
|
||||
type: 'chat',
|
||||
timestamp: Date.now() - (messages.length - idx) * 1000
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Update stats
|
||||
mempalace.status.docs += messages.length;
|
||||
mempalace.status.compression = Math.min(100, mempalace.status.compression + (messages.length / 10));
|
||||
mempalace.status.aak = `${Math.floor(parseInt(mempalace.status.aak.replace('B', '')) + messages.length * 30)}B`;
|
||||
|
||||
updateMemPalaceStatus();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('MemPalace mine failed:', error);
|
||||
document.getElementById('mem-palace-status').textContent = 'Mining Error';
|
||||
document.getElementById('mem-palace-status').style.color = '#ff4466';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Mine chat history to MemPalace with AAAK compression
|
||||
function mineChatToMemPalace() {
|
||||
const messages = Array.from(document.querySelectorAll('.chat-msg')).map(m => m.innerText);
|
||||
if (messages.length > 0) {
|
||||
try {
|
||||
// Convert to AAAK format
|
||||
const aaakContent = messages.map(msg => {
|
||||
const lines = msg.split('\n');
|
||||
return lines.map(line => {
|
||||
// Simple AAAK compression pattern
|
||||
return line.replace(/(\w+): (.+)/g, '$1: $2')
|
||||
.replace(/(\d{4}-\d{2}-\d{2})/, 'DT:$1')
|
||||
.replace(/(\d+ years?)/, 'T:$1');
|
||||
}).join('\n');
|
||||
}).join('\n---\n');
|
||||
|
||||
mempalace.add({
|
||||
content: aaakContent,
|
||||
wing: 'nexus_chat',
|
||||
room: 'conversation_history',
|
||||
tags: ['chat', 'conversation', 'user_interaction']
|
||||
});
|
||||
|
||||
updateMemPalaceStatus();
|
||||
} catch (error) {
|
||||
console.error('MemPalace mining failed:', error);
|
||||
document.getElementById('mem-palace-status').textContent = 'Mining Error';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateMemPalaceStatus() {
|
||||
try {
|
||||
const stats = mempalace.status();
|
||||
document.getElementById('compression-ratio').textContent =
|
||||
stats.compression_ratio.toFixed(1) + 'x';
|
||||
document.getElementById('docs-mined').textContent = stats.total_docs;
|
||||
document.getElementById('aaak-size').textContent = stats.aaak_size + 'B';
|
||||
document.getElementById('mem-palace-status').textContent = 'Mining Active';
|
||||
} catch (error) {
|
||||
document.getElementById('mem-palace-status').textContent = 'Connection Lost';
|
||||
}
|
||||
}
|
||||
|
||||
// Mine chat on send
|
||||
document.getElementById('chat-send-btn').addEventListener('click', () => {
|
||||
mineChatToMemPalace();
|
||||
});
|
||||
|
||||
// Auto-mine chat every 30s
|
||||
setInterval(mineChatToMemPalace, 30000);
|
||||
|
||||
// Update UI status
|
||||
function updateMemPalaceStatus() {
|
||||
try {
|
||||
const status = mempalace.status();
|
||||
document.getElementById('compression-ratio').textContent = status.compression_ratio.toFixed(1) + 'x';
|
||||
document.getElementById('docs-mined').textContent = status.total_docs;
|
||||
document.getElementById('aaak-size').textContent = status.aaak_size + 'b';
|
||||
} catch (error) {
|
||||
document.getElementById('mem-palace-status').textContent = 'Connection Lost';
|
||||
}
|
||||
}
|
||||
|
||||
// Add mining event listener
|
||||
document.getElementById('mem-palace-btn').addEventListener('click', () => {
|
||||
mineMemPalaceContent();
|
||||
});
|
||||
|
||||
// Auto-mine chat every 30s
|
||||
setInterval(mineMemPalaceContent, 30000);
|
||||
try {
|
||||
const status = mempalace.status();
|
||||
document.getElementById('compression-ratio').textContent = status.compression_ratio.toFixed(1) + 'x';
|
||||
document.getElementById('docs-mined').textContent = status.total_docs;
|
||||
document.getElementById('aaak-size').textContent = status.aaak_size + 'B';
|
||||
} catch (error) {
|
||||
console.error('Failed to update MemPalace status:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-mine chat history every 30s
|
||||
setInterval(mineMemPalaceContent, 30000);
|
||||
|
||||
// Call MemPalace initialization
|
||||
connectMemPalace();
|
||||
mineMemPalaceContent();
|
||||
});
|
||||
|
||||
@@ -1,463 +0,0 @@
|
||||
# Formalization Audit Report
|
||||
|
||||
**Date:** 2026-04-06
|
||||
**Auditor:** Allegro (subagent)
|
||||
**Scope:** All homebrew components on VPS 167.99.126.228
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This system runs a fleet of 5 Hermes AI agents (allegro, adagio, ezra, bezalel, bilbobagginshire) alongside supporting infrastructure (Gitea, Nostr relay, Evennia MUD, Ollama). The deployment is functional but heavily ad-hoc — characterized by one-off systemd units, scattered scripts, bare `docker run` containers with no compose file, and custom glue code where standard tooling exists.
|
||||
|
||||
**Priority recommendations:**
|
||||
1. **Consolidate fleet deployment** into docker-compose (HIGH impact, MEDIUM effort)
|
||||
2. **Clean up burn scripts** — archive or delete (HIGH impact, LOW effort)
|
||||
3. **Add docker-compose for Gitea + strfry** (MEDIUM impact, LOW effort)
|
||||
4. **Formalize the webhook receiver** into the hermes-agent repo (MEDIUM impact, LOW effort)
|
||||
5. **Recover or rewrite GOFAI source files** — only .pyc remain (HIGH urgency)
|
||||
|
||||
---
|
||||
|
||||
## 1. Gitea Webhook Receiver
|
||||
|
||||
**File:** `/root/wizards/allegro/gitea_webhook_receiver.py` (327 lines)
|
||||
**Service:** `allegro-gitea-webhook.service`
|
||||
|
||||
### Current State
|
||||
Custom aiohttp server that:
|
||||
- Listens on port 8670 for Gitea webhook events
|
||||
- Verifies HMAC-SHA256 signatures
|
||||
- Filters for @allegro mentions and issue assignments
|
||||
- Forwards to Hermes API (OpenAI-compatible endpoint)
|
||||
- Posts response back as Gitea comment
|
||||
- Includes health check, event logging, async fire-and-forget processing
|
||||
|
||||
Quality: **Solid.** Clean async code, proper signature verification, sensible error handling, daily log rotation. Well-structured for a single-file service.
|
||||
|
||||
### OSS Alternatives
|
||||
- **Adnanh/webhook** (Go, 10k+ stars) — generic webhook receiver, but would need custom scripting anyway
|
||||
- **Flask/FastAPI webhook blueprints** — would be roughly equivalent effort
|
||||
- **Gitea built-in webhooks + Woodpecker CI** — different architecture (push-based CI vs. agent interaction)
|
||||
|
||||
### Recommendation: **KEEP, but formalize**
|
||||
The webhook logic is Allegro-specific (mention detection, Hermes API forwarding, comment posting). No off-the-shelf tool replaces this without equal or more glue code. However:
|
||||
- Move into the hermes-agent repo as a plugin/skill
|
||||
- Make it configurable for any wizard name (not just "allegro")
|
||||
- Add to docker-compose instead of standalone systemd unit
|
||||
|
||||
**Effort:** 2-4 hours
|
||||
|
||||
---
|
||||
|
||||
## 2. Nostr Relay + Bridge
|
||||
|
||||
### Relay (strfry + custom timmy-relay)
|
||||
|
||||
**Running:** Two relay implementations in parallel
|
||||
1. **strfry** Docker container (port 7777) — standard relay, healthy, community-maintained
|
||||
2. **timmy-relay** Go binary (port 2929) — custom NIP-29 relay built on `relay29`/`khatru29`
|
||||
|
||||
The custom relay (`main.go`, 108 lines) is a thin wrapper around `fiatjaf/relay29` with:
|
||||
- NIP-29 group support (admin/mod roles)
|
||||
- LMDB persistent storage
|
||||
- Allowlisted event kinds
|
||||
- Anti-spam policies (tag limits, timestamp guards)
|
||||
|
||||
### Bridge (dm_bridge_mvp)
|
||||
|
||||
**Service:** `nostr-bridge.service`
|
||||
**Status:** Running but **source file deleted** — only `.pyc` cache remains at `/root/nostr-relay/__pycache__/dm_bridge_mvp.cpython-312.pyc`
|
||||
|
||||
From decompiled structure, the bridge:
|
||||
- Reads DMs from Nostr relay
|
||||
- Parses commands from DMs
|
||||
- Creates Gitea issues/comments via API
|
||||
- Polls for new DMs in a loop
|
||||
- Uses keystore.json for identity management
|
||||
|
||||
**CRITICAL:** Source code is gone. If the service restarts on a Python update (new .pyc format), this component dies.
|
||||
|
||||
### OSS Alternatives
|
||||
- **strfry:** Already using it. Good choice, well-maintained.
|
||||
- **relay29:** Already using it. Correct choice for NIP-29 groups.
|
||||
- **nostr-tools / rust-nostr SDKs** for bridge — but bridge logic is custom regardless
|
||||
|
||||
### Recommendation: **KEEP relay, RECOVER bridge**
|
||||
- The relay setup (relay29 custom binary + strfry) is appropriate
|
||||
- **URGENT:** Decompile dm_bridge_mvp.pyc and reconstruct source before it's lost
|
||||
- Consider whether strfry (port 7777) is still needed alongside timmy-relay (port 2929) — possible to consolidate
|
||||
- Move bridge into its own git repo on Gitea
|
||||
|
||||
**Effort:** 4-6 hours (bridge recovery), 1 hour (strfry consolidation assessment)
|
||||
|
||||
---
|
||||
|
||||
## 3. Evennia / Timmy Academy
|
||||
|
||||
**Path:** `/root/workspace/timmy-academy/`
|
||||
**Components:**
|
||||
|
||||
| Component | File | Custom? | Lines |
|
||||
|-----------|------|---------|-------|
|
||||
| AuditedCharacter | typeclasses/audited_character.py | Yes | 110 |
|
||||
| Custom Commands | commands/command.py | Yes | 368 |
|
||||
| Audit Dashboard | web/audit/ (views, api, templates) | Yes | ~250 |
|
||||
| Object typeclass | typeclasses/objects.py | Stock (untouched) | 218 |
|
||||
| Room typeclass | typeclasses/rooms.py | Minimal | ~15 |
|
||||
| Exit typeclass | typeclasses/exits.py | Minimal | ~15 |
|
||||
| Account typeclass | typeclasses/accounts.py | Custom (157 lines) | 157 |
|
||||
| Channel typeclass | typeclasses/channels.py | Custom | ~160 |
|
||||
| Scripts | typeclasses/scripts.py | Custom | ~130 |
|
||||
| World builder | world/ | Custom | Unknown |
|
||||
|
||||
### Custom vs Stock Analysis
|
||||
- **objects.py** — Stock Evennia template with no modifications. Safe to delete and use defaults.
|
||||
- **audited_character.py** — Fully custom. Tracks movement, commands, session time, generates audit summaries. Clean code.
|
||||
- **commands/command.py** — 7 custom commands (examine, rooms, status, map, academy, smell, listen). All game-specific. Quality is good — uses Evennia patterns correctly.
|
||||
- **web/audit/** — Custom Django views and templates for an audit dashboard (character detail, command logs, movement logs, session logs). Functional but simple.
|
||||
- **accounts.py, channels.py, scripts.py** — Custom but follow Evennia patterns. Mainly enhanced with audit hooks.
|
||||
|
||||
### OSS Alternatives
|
||||
Evennia IS the OSS framework. The customizations are all game-specific content, which is exactly how Evennia is designed to be used.
|
||||
|
||||
### Recommendation: **KEEP as-is**
|
||||
This is a well-structured Evennia game. The customizations are appropriate and follow Evennia best practices. No formalization needed — it's already a proper project in a git repo.
|
||||
|
||||
Minor improvements:
|
||||
- Remove the `{e})` empty file in root (appears to be a typo artifact)
|
||||
- The audit dashboard could use authentication guards
|
||||
|
||||
**Effort:** 0 (already formalized)
|
||||
|
||||
---
|
||||
|
||||
## 4. Burn Scripts (`/root/burn_*.py`)
|
||||
|
||||
**Count:** 39 scripts
|
||||
**Total lines:** 2,898
|
||||
**Date range:** All from April 5, 2026 (one day)
|
||||
|
||||
### Current State
|
||||
These are one-off Gitea API query scripts. Examples:
|
||||
- `burn_sitrep.py` — fetch issue details from Gitea
|
||||
- `burn_comments.py` — fetch issue comments
|
||||
- `burn_fetch_issues.py` — list open issues
|
||||
- `burn_execute.py` — perform actions on issues
|
||||
- `burn_mode_query.py` — query specific issue data
|
||||
|
||||
All follow the same pattern:
|
||||
1. Load token from `/root/.gitea_token`
|
||||
2. Define `api_get(path)` helper
|
||||
3. Hit specific Gitea API endpoints
|
||||
4. Print JSON results
|
||||
|
||||
They share ~80% identical boilerplate. Most appear to be iterative debugging scripts (burn_discover.py, burn_discover2.py; burn_fetch_issues.py, burn_fetch_issues2.py).
|
||||
|
||||
### OSS Alternatives
|
||||
- **Gitea CLI (`tea`)** — official Gitea CLI tool, does everything these scripts do
|
||||
- **python-gitea** — Python SDK for Gitea API
|
||||
- **httpie / curl** — for one-off queries
|
||||
|
||||
### Recommendation: **DELETE or ARCHIVE**
|
||||
These are debugging artifacts, not production code. They:
|
||||
- Duplicate functionality already in the webhook receiver and hermes-agent tools
|
||||
- Contain hardcoded issue numbers and old API URLs (`143.198.27.163:3000` vs current `forge.alexanderwhitestone.com`)
|
||||
- Have numbered variants showing iterative debugging (not versioned)
|
||||
|
||||
Action:
|
||||
1. `mkdir /root/archive && mv /root/burn_*.py /root/archive/`
|
||||
2. If any utility is still needed, extract it into the hermes-agent's `tools/gitea_client.py` which already exists
|
||||
3. Install `tea` CLI for ad-hoc Gitea queries
|
||||
|
||||
**Effort:** 30 minutes
|
||||
|
||||
---
|
||||
|
||||
## 5. Heartbeat Daemon
|
||||
|
||||
**Files:**
|
||||
- `/root/wizards/allegro/home/skills/devops/hybrid-autonomous-production/templates/heartbeat_daemon.py` (321 lines)
|
||||
- `/root/wizards/allegro/household-snapshots/scripts/template_checkpoint_heartbeat.py` (155 lines)
|
||||
- Various per-wizard heartbeat scripts
|
||||
|
||||
### Current State
|
||||
|
||||
Two distinct heartbeat patterns:
|
||||
|
||||
**A) Production Heartbeat Daemon (321 lines)**
|
||||
Full autonomous operations script:
|
||||
- Health checks (Gitea, Nostr relay, Hermes services)
|
||||
- Dynamic repo discovery
|
||||
- Automated triage (comments on unlabeled issues)
|
||||
- PR merge automation
|
||||
- Logged to `/root/allegro/heartbeat_logs/`
|
||||
- Designed to run every 15 minutes via cron
|
||||
|
||||
Quality: **Good for a prototype.** Well-structured phases, logging, error handling. But runs as root, uses urllib directly, has hardcoded org name.
|
||||
|
||||
**B) Checkpoint Heartbeat Template (155 lines)**
|
||||
State backup script:
|
||||
- Syncs wizard home dirs to git repos
|
||||
- Auto-commits and pushes to Gitea
|
||||
- Template pattern (copy and customize per wizard)
|
||||
|
||||
### OSS Alternatives
|
||||
- **For health checks:** Uptime Kuma, Healthchecks.io, Monit
|
||||
- **For PR automation:** Renovate, Dependabot, Mergify (but these are SaaS/different scope)
|
||||
- **For backups:** restic, borgbackup, git-backup tools
|
||||
- **For scheduling:** systemd timers (already used), or cron
|
||||
|
||||
### Recommendation: **FORMALIZE into proper systemd timer + package**
|
||||
- Create a proper `timmy-heartbeat` Python package with:
|
||||
- `heartbeat.health` — infrastructure health checks
|
||||
- `heartbeat.triage` — issue triage automation
|
||||
- `heartbeat.checkpoint` — state backup
|
||||
- Install as a systemd timer (not cron) with proper unit files
|
||||
- Use the existing `tools/gitea_client.py` from hermes-agent instead of duplicating urllib code
|
||||
- Add alerting (webhook to Telegram/Nostr on failures)
|
||||
|
||||
**Effort:** 4-6 hours
|
||||
|
||||
---
|
||||
|
||||
## 6. GOFAI System
|
||||
|
||||
**Path:** `/root/wizards/allegro/gofai/`
|
||||
|
||||
### Current State: CRITICAL — SOURCE FILES MISSING
|
||||
|
||||
The `gofai/` directory contains:
|
||||
- `tests/test_gofai.py` (790 lines, 20+ test cases) — **exists**
|
||||
- `tests/test_knowledge_graph.py` (14k chars) — **exists**
|
||||
- `__pycache__/*.cpython-312.pyc` — cached bytecode for 4 modules
|
||||
- **NO .py source files** for the actual modules
|
||||
|
||||
The `.pyc` files reveal the following modules were deleted but cached:
|
||||
|
||||
| Module | Classes/Functions | Purpose |
|
||||
|--------|------------------|---------|
|
||||
| `schema.py` | FleetSchema, Wizard, Task, TaskStatus, EntityType, Relationship, Principle, Entity, get_fleet_schema | Pydantic/dataclass models for fleet knowledge |
|
||||
| `rule_engine.py` | RuleEngine, Rule, RuleContext, ActionType, create_child_rule_engine | Forward-chaining rule engine with SOUL.md integration |
|
||||
| `knowledge_graph.py` | KnowledgeGraph, FleetKnowledgeBase, Node, Edge, JsonGraphStore, SQLiteGraphStore | Property graph with JSON and SQLite persistence |
|
||||
| `child_assistant.py` | ChildAssistant, Decision | Decision support for child wizards (can_i_do_this, who_is_my_family, etc.) |
|
||||
|
||||
Git history shows: `feat(gofai): add SQLite persistence layer to KnowledgeGraph` — so this was an active development.
|
||||
|
||||
### Maturity Assessment (from .pyc + tests)
|
||||
- **Rule Engine:** Basic forward-chaining with keyword matching. Has predefined child safety and fleet coordination rules. ~15 rules. Functional but simple.
|
||||
- **Knowledge Graph:** Property graph with CRUD, path finding, lineage tracking, GraphViz export. JSON + SQLite backends. Reasonably mature.
|
||||
- **Schema:** Pydantic/dataclass models. Standard data modeling.
|
||||
- **Child Assistant:** Interactive decision helper. Novel concept for wizard hierarchy.
|
||||
- **Tests:** Comprehensive (790 lines). This was being actively developed and tested.
|
||||
|
||||
### OSS Alternatives
|
||||
- **Rule engines:** Durable Rules, PyKnow/Experta, business-rules
|
||||
- **Knowledge graphs:** NetworkX (simpler), Neo4j (overkill), RDFlib
|
||||
- **Schema:** Pydantic (already used)
|
||||
|
||||
### Recommendation: **RECOVER and FORMALIZE**
|
||||
1. **URGENT:** Recover source from git history: `git show <commit>:gofai/schema.py` etc.
|
||||
2. Package as `timmy-gofai` with proper `pyproject.toml`
|
||||
3. The concept is novel enough to keep — fleet coordination via deterministic rules + knowledge graph is genuinely useful
|
||||
4. Consider using NetworkX for graph backend instead of custom implementation
|
||||
5. Push to its own Gitea repo
|
||||
|
||||
**Effort:** 2-4 hours (recovery from git), 4-6 hours (formalization)
|
||||
|
||||
---
|
||||
|
||||
## 7. Hermes Agent (Claude Code / Hermes)
|
||||
|
||||
**Path:** `/root/wizards/allegro/hermes-agent/`
|
||||
**Origin:** `https://github.com/NousResearch/hermes-agent.git` (MIT license)
|
||||
**Version:** 0.5.0
|
||||
**Size:** ~26,000 lines of Python (top-level only), massive codebase
|
||||
|
||||
### Current State
|
||||
This is an upstream open-source project (NousResearch/hermes-agent) with local modifications. Key components:
|
||||
- `run_agent.py` — 8,548 lines (!) — main agent loop
|
||||
- `cli.py` — 7,691 lines — interactive CLI
|
||||
- `hermes_state.py` — 1,623 lines — state management
|
||||
- `gateway/` — HTTP API gateway for each wizard
|
||||
- `tools/` — 15+ tool modules (gitea_client, memory, image_generation, MCP, etc.)
|
||||
- `skills/` — 29 skill directories
|
||||
- `prose/` — document generation engine
|
||||
- Custom profiles per wizard
|
||||
|
||||
### OSS Duplication Analysis
|
||||
| Component | Duplicates | Alternative |
|
||||
|-----------|-----------|-------------|
|
||||
| `tools/gitea_client.py` | Custom Gitea API wrapper | python-gitea, PyGitea |
|
||||
| `tools/web_research_env.py` | Custom web search | Already uses exa-py, firecrawl |
|
||||
| `tools/memory_tool.py` | Custom memory/RAG | Honcho (already optional dep) |
|
||||
| `tools/code_execution_tool.py` | Custom code sandbox | E2B, Modal (already optional dep) |
|
||||
| `gateway/` | Custom HTTP API | FastAPI app (reasonable) |
|
||||
| `trajectory_compressor.py` | Custom context compression | LangChain summarizers, LlamaIndex |
|
||||
|
||||
### Recommendation: **KEEP — it IS the OSS project**
|
||||
Hermes-agent is itself an open-source project. The right approach is:
|
||||
- Keep upstream sync working (both `origin` and `gitea` remotes configured)
|
||||
- Don't duplicate the gitea_client into burn scripts or heartbeat daemons — use the one in tools/
|
||||
- Monitor for upstream improvements to tools that are currently custom
|
||||
- The 8.5k-line run_agent.py is a concern for maintainability — but that's an upstream issue
|
||||
|
||||
**Effort:** 0 (ongoing maintenance)
|
||||
|
||||
---
|
||||
|
||||
## 8. Fleet Deployment
|
||||
|
||||
### Current State
|
||||
Each wizard runs as a separate systemd service:
|
||||
- `hermes-allegro.service` — WorkingDir at allegro's hermes-agent
|
||||
- `hermes-adagio.service` — WorkingDir at adagio's hermes-agent
|
||||
- `hermes-ezra.service` — WorkingDir at ezra's (uses allegro's hermes-agent origin)
|
||||
- `hermes-bezalel.service` — WorkingDir at bezalel's
|
||||
|
||||
Each has its own:
|
||||
- Copy of hermes-agent (or symlink/clone)
|
||||
- .venv (separate Python virtual environment)
|
||||
- home/ directory with SOUL.md, .env, memories, skills
|
||||
- EnvironmentFile pointing to per-wizard .env
|
||||
|
||||
Docker containers (not managed by compose):
|
||||
- `gitea` — bare `docker run`
|
||||
- `strfry` — bare `docker run`
|
||||
|
||||
### Issues
|
||||
1. **No docker-compose.yml** — containers were created with `docker run` and survive via restart policy
|
||||
2. **Duplicate venvs** — each wizard has its own .venv (~500MB each = 2.5GB+)
|
||||
3. **Inconsistent origins** — ezra's hermes-agent origin points to allegro's local copy, not git
|
||||
4. **No fleet-wide deployment tool** — updates require manual per-wizard action
|
||||
5. **All run as root**
|
||||
|
||||
### OSS Alternatives
|
||||
| Tool | Fit | Complexity |
|
||||
|------|-----|-----------|
|
||||
| docker-compose | Good — defines Gitea, strfry, and could define agents | Low |
|
||||
| k3s | Overkill for 5 agents on 1 VPS | High |
|
||||
| Podman pods | Similar to compose, rootless possible | Medium |
|
||||
| Ansible | Good for fleet management across VPSes | Medium |
|
||||
| systemd-nspawn | Lightweight containers | Medium |
|
||||
|
||||
### Recommendation: **ADD docker-compose for infrastructure, KEEP systemd for agents**
|
||||
1. Create `/root/docker-compose.yml` for Gitea + strfry + Ollama(optional)
|
||||
2. Keep wizard agents as systemd services (they need filesystem access, tool execution, etc.)
|
||||
3. Create a fleet management script: `fleet.sh {start|stop|restart|status|update} [wizard]`
|
||||
4. Share a single hermes-agent checkout with per-wizard config (not 5 copies)
|
||||
5. Long term: consider running agents in containers too (requires volume mounts for home/)
|
||||
|
||||
**Effort:** 4-6 hours (docker-compose + fleet script)
|
||||
|
||||
---
|
||||
|
||||
## 9. Nostr Key Management
|
||||
|
||||
**File:** `/root/nostr-relay/keystore.json`
|
||||
|
||||
### Current State
|
||||
Plain JSON file containing nsec (private keys), npub (public keys), and hex equivalents for:
|
||||
- relay
|
||||
- allegro
|
||||
- ezra
|
||||
- alexander (with placeholder "ALEXANDER_CONTROLS_HIS_OWN" for secret)
|
||||
|
||||
The keystore is:
|
||||
- World-readable (`-rw-r--r--`)
|
||||
- Contains private keys in cleartext
|
||||
- No encryption
|
||||
- No rotation mechanism
|
||||
- Used by bridge and relay scripts via direct JSON loading
|
||||
|
||||
### OSS Alternatives
|
||||
- **SOPS (Mozilla)** — encrypted secrets in version control
|
||||
- **age encryption** — simple file encryption
|
||||
- **Vault (HashiCorp)** — overkill for this scale
|
||||
- **systemd credentials** — built into systemd 250+
|
||||
- **NIP-49 encrypted nsec** — Nostr-native key encryption
|
||||
- **Pass / gopass** — Unix password manager
|
||||
|
||||
### Recommendation: **FORMALIZE with minimal encryption**
|
||||
1. `chmod 600 /root/nostr-relay/keystore.json` — **immediate** (5 seconds)
|
||||
2. Move secrets to per-service EnvironmentFiles (already pattern used for .env)
|
||||
3. Consider NIP-49 (password-encrypted nsec) for the keystore
|
||||
4. Remove the relay private key from the systemd unit file (currently in plaintext in the `[Service]` section!)
|
||||
5. Never commit keystore.json to git (check .gitignore)
|
||||
|
||||
**Effort:** 1-2 hours
|
||||
|
||||
---
|
||||
|
||||
## 10. Ollama Setup and Model Management
|
||||
|
||||
### Current State
|
||||
- **Service:** `ollama.service` — standard systemd unit, running as `ollama` user
|
||||
- **Binary:** `/usr/local/bin/ollama` — standard install
|
||||
- **Models:** Only `qwen3:4b` (2.5GB) currently loaded
|
||||
- **Guard:** `/root/wizards/scripts/ollama_guard.py` — custom 55-line script that blocks models >5GB
|
||||
- **Port:** 11434 (default, localhost only)
|
||||
|
||||
### Assessment
|
||||
The Ollama setup is essentially stock. The only custom component is `ollama_guard.py`, which is a clever but fragile size guard that:
|
||||
- Checks local model size before pulling
|
||||
- Blocks downloads >5GB to protect the VPS
|
||||
- Designed to be symlinked ahead of real `ollama` in PATH
|
||||
|
||||
However: it's not actually deployed as a PATH override (real `ollama` is at `/usr/local/bin/ollama`, guard is in `/root/wizards/scripts/`).
|
||||
|
||||
### OSS Alternatives
|
||||
- **Ollama itself** is the standard. No alternative needed.
|
||||
- **For model management:** LiteLLM proxy, OpenRouter (for offloading large models)
|
||||
- **For guards:** Ollama has `OLLAMA_MAX_MODEL_SIZE` env var (check if available in current version)
|
||||
|
||||
### Recommendation: **KEEP, minor improvements**
|
||||
1. Actually deploy the guard if you want it (symlink or wrapper)
|
||||
2. Or just set `OLLAMA_MAX_LOADED_MODELS=1` and use Ollama's native controls
|
||||
3. Document which models are approved for local use vs. RunPod offload
|
||||
4. Consider adding Ollama to docker-compose for consistency
|
||||
|
||||
**Effort:** 30 minutes
|
||||
|
||||
---
|
||||
|
||||
## Priority Matrix
|
||||
|
||||
| # | Component | Action | Priority | Effort | Impact |
|
||||
|---|-----------|--------|----------|--------|--------|
|
||||
| 1 | GOFAI source recovery | Recover from git | CRITICAL | 2h | Source code loss |
|
||||
| 2 | Nostr bridge source | Decompile/recover .pyc | CRITICAL | 4h | Service loss risk |
|
||||
| 3 | Keystore permissions | chmod 600 | CRITICAL | 5min | Security |
|
||||
| 4 | Burn scripts | Archive to /root/archive/ | HIGH | 30min | Cleanliness |
|
||||
| 5 | Docker-compose | Create for Gitea+strfry | HIGH | 2h | Reproducibility |
|
||||
| 6 | Fleet script | Create fleet.sh management | HIGH | 3h | Operations |
|
||||
| 7 | Webhook receiver | Move into hermes-agent repo | MEDIUM | 3h | Maintainability |
|
||||
| 8 | Heartbeat daemon | Package as timmy-heartbeat | MEDIUM | 5h | Reliability |
|
||||
| 9 | Ollama guard | Deploy or remove | LOW | 30min | Consistency |
|
||||
| 10 | Evennia | No action needed | LOW | 0h | Already good |
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Files Examined
|
||||
|
||||
```
|
||||
/etc/systemd/system/allegro-gitea-webhook.service
|
||||
/etc/systemd/system/nostr-bridge.service
|
||||
/etc/systemd/system/nostr-relay.service
|
||||
/etc/systemd/system/hermes-allegro.service
|
||||
/etc/systemd/system/hermes-adagio.service
|
||||
/etc/systemd/system/hermes-ezra.service
|
||||
/etc/systemd/system/hermes-bezalel.service
|
||||
/etc/systemd/system/ollama.service
|
||||
/root/wizards/allegro/gitea_webhook_receiver.py
|
||||
/root/nostr-relay/main.go
|
||||
/root/nostr-relay/keystore.json
|
||||
/root/nostr-relay/__pycache__/dm_bridge_mvp.cpython-312.pyc
|
||||
/root/wizards/allegro/gofai/ (all files)
|
||||
/root/wizards/allegro/hermes-agent/pyproject.toml
|
||||
/root/workspace/timmy-academy/ (typeclasses, commands, web)
|
||||
/root/burn_*.py (39 files)
|
||||
/root/wizards/allegro/home/skills/devops/.../heartbeat_daemon.py
|
||||
/root/wizards/allegro/household-snapshots/scripts/template_checkpoint_heartbeat.py
|
||||
/root/wizards/scripts/ollama_guard.py
|
||||
```
|
||||
Binary file not shown.
@@ -1,42 +0,0 @@
|
||||
import os
|
||||
import requests
|
||||
from typing import Dict, List
|
||||
|
||||
GITEA_API_URL = os.getenv("GITEA_API_URL")
|
||||
GITEA_TOKEN = os.getenv("GITEA_TOKEN")
|
||||
ORGANIZATION = "Timmy_Foundation"
|
||||
REPOSITORIES = ["hermes-agent", "the-nexus", "timmy-home", "timmy-config"]
|
||||
|
||||
BRANCH_PROTECTION = {
|
||||
"required_pull_request_reviews": {
|
||||
"dismiss_stale_reviews": True,
|
||||
"required_approving_review_count": 1
|
||||
},
|
||||
"required_status_checks": {
|
||||
"strict": True,
|
||||
"contexts": ["ci/cd", "lint", "security"]
|
||||
},
|
||||
"enforce_admins": True,
|
||||
"restrictions": {
|
||||
"team_whitelist": ["maintainers"],
|
||||
"app_whitelist": []
|
||||
},
|
||||
"block_force_push": True,
|
||||
"block_deletions": True
|
||||
}
|
||||
|
||||
def apply_protection(repo: str):
|
||||
url = f"{GITEA_API_URL}/repos/{ORGANIZATION}/{repo}/branches/main/protection"
|
||||
headers = {
|
||||
"Authorization": f"token {GITEA_TOKEN}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
response = requests.post(url, json=BRANCH_PROTECTION, headers=headers)
|
||||
if response.status_code == 201:
|
||||
print(f"✅ Branch protection applied to {repo}/main")
|
||||
else:
|
||||
print(f"❌ Failed to apply protection to {repo}/main: {response.text}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
for repo in REPOSITORIES:
|
||||
apply_protection(repo)
|
||||
@@ -1,449 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Meta-heartbeat checker — makes silent cron failures impossible.
|
||||
|
||||
Reads every ``*.last`` file in the heartbeat directory and verifies that no
|
||||
job has been silent for longer than **2× its declared interval**. If any job
|
||||
is stale, a Gitea alert issue is created (or an existing one is updated).
|
||||
When all jobs recover, the issue is closed automatically.
|
||||
|
||||
This script itself should be run as a cron job every 15 minutes so the
|
||||
meta-level is also covered:
|
||||
|
||||
*/15 * * * * cd /path/to/the-nexus && \\
|
||||
python bin/check_cron_heartbeats.py >> /var/log/bezalel/heartbeat-check.log 2>&1
|
||||
|
||||
USAGE
|
||||
-----
|
||||
# Check all jobs; create/update Gitea alert if any stale:
|
||||
python bin/check_cron_heartbeats.py
|
||||
|
||||
# Dry-run (no Gitea writes):
|
||||
python bin/check_cron_heartbeats.py --dry-run
|
||||
|
||||
# Output Night Watch heartbeat panel markdown:
|
||||
python bin/check_cron_heartbeats.py --panel
|
||||
|
||||
# Output JSON (for integration with other tools):
|
||||
python bin/check_cron_heartbeats.py --json
|
||||
|
||||
# Use a custom heartbeat directory:
|
||||
python bin/check_cron_heartbeats.py --dir /tmp/test-heartbeats
|
||||
|
||||
HEARTBEAT DIRECTORY
|
||||
-------------------
|
||||
Primary: /var/run/bezalel/heartbeats/ (set by ops, writable by cron user)
|
||||
Fallback: ~/.bezalel/heartbeats/ (dev machines)
|
||||
Override: BEZALEL_HEARTBEAT_DIR env var
|
||||
|
||||
ZERO DEPENDENCIES
|
||||
-----------------
|
||||
Pure stdlib. No pip installs required.
|
||||
|
||||
Refs: #1096
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)-7s %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
logger = logging.getLogger("bezalel.heartbeat_checker")
|
||||
|
||||
# ── Configuration ─────────────────────────────────────────────────────
|
||||
|
||||
PRIMARY_HEARTBEAT_DIR = Path("/var/run/bezalel/heartbeats")
|
||||
FALLBACK_HEARTBEAT_DIR = Path.home() / ".bezalel" / "heartbeats"
|
||||
|
||||
GITEA_URL = os.environ.get("GITEA_URL", "https://forge.alexanderwhitestone.com")
|
||||
GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "")
|
||||
GITEA_REPO = os.environ.get("NEXUS_REPO", "Timmy_Foundation/the-nexus")
|
||||
ALERT_TITLE_PREFIX = "[heartbeat-checker]"
|
||||
|
||||
# A job is stale when its age exceeds this multiple of its declared interval
|
||||
STALE_RATIO = 2.0
|
||||
# Never flag a job as stale if it completed less than this many seconds ago
|
||||
# (prevents noise immediately after deployment)
|
||||
MIN_STALE_AGE = 60
|
||||
|
||||
|
||||
def _resolve_heartbeat_dir() -> Path:
|
||||
"""Return the active heartbeat directory."""
|
||||
env = os.environ.get("BEZALEL_HEARTBEAT_DIR")
|
||||
if env:
|
||||
return Path(env)
|
||||
if PRIMARY_HEARTBEAT_DIR.exists():
|
||||
return PRIMARY_HEARTBEAT_DIR
|
||||
# Try to create it; fall back to home dir if not permitted
|
||||
try:
|
||||
PRIMARY_HEARTBEAT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
probe = PRIMARY_HEARTBEAT_DIR / ".write_probe"
|
||||
probe.touch()
|
||||
probe.unlink()
|
||||
return PRIMARY_HEARTBEAT_DIR
|
||||
except (PermissionError, OSError):
|
||||
return FALLBACK_HEARTBEAT_DIR
|
||||
|
||||
|
||||
# ── Data model ────────────────────────────────────────────────────────
|
||||
|
||||
@dataclass
|
||||
class JobStatus:
|
||||
"""Health status for a single cron job's heartbeat."""
|
||||
job: str
|
||||
path: Path
|
||||
healthy: bool
|
||||
age_seconds: float # -1 if unknown (missing/corrupt)
|
||||
interval_seconds: int # 0 if unknown
|
||||
staleness_ratio: float # age / interval; -1 if unknown; >STALE_RATIO = stale
|
||||
last_timestamp: Optional[float]
|
||||
pid: Optional[int]
|
||||
raw_status: str # value from the .last file: "ok" / "warn" / "error"
|
||||
message: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class HeartbeatReport:
|
||||
"""Aggregate report for all cron job heartbeats in a directory."""
|
||||
timestamp: float
|
||||
heartbeat_dir: Path
|
||||
jobs: List[JobStatus] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def stale_jobs(self) -> List[JobStatus]:
|
||||
return [j for j in self.jobs if not j.healthy]
|
||||
|
||||
@property
|
||||
def overall_healthy(self) -> bool:
|
||||
return len(self.stale_jobs) == 0
|
||||
|
||||
# ── Rendering ─────────────────────────────────────────────────────
|
||||
|
||||
def to_panel_markdown(self) -> str:
|
||||
"""Night Watch heartbeat panel — a table of all jobs with their status."""
|
||||
ts = time.strftime("%Y-%m-%d %H:%M UTC", time.gmtime(self.timestamp))
|
||||
overall = "OK" if self.overall_healthy else "ALERT"
|
||||
|
||||
lines = [
|
||||
f"## Heartbeat Panel — {ts}",
|
||||
"",
|
||||
f"**Overall:** {overall}",
|
||||
"",
|
||||
"| Job | Status | Age | Interval | Ratio |",
|
||||
"|-----|--------|-----|----------|-------|",
|
||||
]
|
||||
|
||||
if not self.jobs:
|
||||
lines.append("| *(no heartbeat files found)* | — | — | — | — |")
|
||||
else:
|
||||
for j in self.jobs:
|
||||
icon = "OK" if j.healthy else "STALE"
|
||||
age_str = _fmt_duration(j.age_seconds) if j.age_seconds >= 0 else "N/A"
|
||||
interval_str = _fmt_duration(j.interval_seconds) if j.interval_seconds > 0 else "N/A"
|
||||
ratio_str = f"{j.staleness_ratio:.1f}x" if j.staleness_ratio >= 0 else "N/A"
|
||||
lines.append(
|
||||
f"| `{j.job}` | {icon} | {age_str} | {interval_str} | {ratio_str} |"
|
||||
)
|
||||
|
||||
if self.stale_jobs:
|
||||
lines += ["", "**Stale jobs:**"]
|
||||
for j in self.stale_jobs:
|
||||
lines.append(f"- `{j.job}`: {j.message}")
|
||||
|
||||
lines += [
|
||||
"",
|
||||
f"*Heartbeat dir: `{self.heartbeat_dir}`*",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
def to_alert_body(self) -> str:
|
||||
"""Gitea issue body when stale jobs are detected."""
|
||||
ts = time.strftime("%Y-%m-%d %H:%M:%S UTC", time.gmtime(self.timestamp))
|
||||
stale = self.stale_jobs
|
||||
|
||||
lines = [
|
||||
f"## Cron Heartbeat Alert — {ts}",
|
||||
"",
|
||||
f"**{len(stale)} job(s) have gone silent** (stale > {STALE_RATIO}x interval).",
|
||||
"",
|
||||
"| Job | Age | Interval | Ratio | Detail |",
|
||||
"|-----|-----|----------|-------|--------|",
|
||||
]
|
||||
|
||||
for j in stale:
|
||||
age_str = _fmt_duration(j.age_seconds) if j.age_seconds >= 0 else "N/A"
|
||||
interval_str = _fmt_duration(j.interval_seconds) if j.interval_seconds > 0 else "N/A"
|
||||
ratio_str = f"{j.staleness_ratio:.1f}x" if j.staleness_ratio >= 0 else "N/A"
|
||||
lines.append(
|
||||
f"| `{j.job}` | {age_str} | {interval_str} | {ratio_str} | {j.message} |"
|
||||
)
|
||||
|
||||
lines += [
|
||||
"",
|
||||
"### What to do",
|
||||
"1. `crontab -l` — confirm the job is still scheduled",
|
||||
"2. Check the job's log for errors",
|
||||
"3. Restart the job if needed",
|
||||
"4. Close this issue once fresh heartbeats appear",
|
||||
"",
|
||||
f"*Generated by `check_cron_heartbeats.py` — dir: `{self.heartbeat_dir}`*",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
def to_json(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"healthy": self.overall_healthy,
|
||||
"timestamp": self.timestamp,
|
||||
"heartbeat_dir": str(self.heartbeat_dir),
|
||||
"jobs": [
|
||||
{
|
||||
"job": j.job,
|
||||
"healthy": j.healthy,
|
||||
"age_seconds": j.age_seconds,
|
||||
"interval_seconds": j.interval_seconds,
|
||||
"staleness_ratio": j.staleness_ratio,
|
||||
"raw_status": j.raw_status,
|
||||
"message": j.message,
|
||||
}
|
||||
for j in self.jobs
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
def _fmt_duration(seconds: float) -> str:
|
||||
"""Format a duration in seconds as a human-readable string."""
|
||||
s = int(seconds)
|
||||
if s < 60:
|
||||
return f"{s}s"
|
||||
if s < 3600:
|
||||
return f"{s // 60}m {s % 60}s"
|
||||
return f"{s // 3600}h {(s % 3600) // 60}m"
|
||||
|
||||
|
||||
# ── Job scanning ──────────────────────────────────────────────────────
|
||||
|
||||
def scan_heartbeats(directory: Path) -> List[JobStatus]:
|
||||
"""Read every ``*.last`` file in *directory* and return their statuses."""
|
||||
if not directory.exists():
|
||||
return []
|
||||
return [_read_job_status(p.stem, p) for p in sorted(directory.glob("*.last"))]
|
||||
|
||||
|
||||
def _read_job_status(job: str, path: Path) -> JobStatus:
|
||||
"""Parse one ``.last`` file and produce a ``JobStatus``."""
|
||||
now = time.time()
|
||||
|
||||
if not path.exists():
|
||||
return JobStatus(
|
||||
job=job, path=path,
|
||||
healthy=False,
|
||||
age_seconds=-1,
|
||||
interval_seconds=0,
|
||||
staleness_ratio=-1,
|
||||
last_timestamp=None,
|
||||
pid=None,
|
||||
raw_status="missing",
|
||||
message=f"Heartbeat file missing: {path}",
|
||||
)
|
||||
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except (json.JSONDecodeError, OSError) as exc:
|
||||
return JobStatus(
|
||||
job=job, path=path,
|
||||
healthy=False,
|
||||
age_seconds=-1,
|
||||
interval_seconds=0,
|
||||
staleness_ratio=-1,
|
||||
last_timestamp=None,
|
||||
pid=None,
|
||||
raw_status="corrupt",
|
||||
message=f"Corrupt heartbeat: {exc}",
|
||||
)
|
||||
|
||||
timestamp = float(data.get("timestamp", 0))
|
||||
interval = int(data.get("interval_seconds", 0))
|
||||
pid = data.get("pid")
|
||||
raw_status = data.get("status", "ok")
|
||||
|
||||
age = now - timestamp
|
||||
ratio = age / interval if interval > 0 else float("inf")
|
||||
stale = ratio > STALE_RATIO and age > MIN_STALE_AGE
|
||||
|
||||
if stale:
|
||||
message = (
|
||||
f"Silent for {_fmt_duration(age)} "
|
||||
f"({ratio:.1f}x interval of {_fmt_duration(interval)})"
|
||||
)
|
||||
else:
|
||||
message = f"Last beat {_fmt_duration(age)} ago (ratio {ratio:.1f}x)"
|
||||
|
||||
return JobStatus(
|
||||
job=job, path=path,
|
||||
healthy=not stale,
|
||||
age_seconds=age,
|
||||
interval_seconds=interval,
|
||||
staleness_ratio=ratio,
|
||||
last_timestamp=timestamp,
|
||||
pid=pid,
|
||||
raw_status=raw_status if not stale else "stale",
|
||||
message=message,
|
||||
)
|
||||
|
||||
|
||||
# ── Gitea alerting ────────────────────────────────────────────────────
|
||||
|
||||
def _gitea_request(method: str, path: str, data: Optional[dict] = None) -> Any:
|
||||
"""Make a Gitea API request; return parsed JSON or None on error."""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
url = f"{GITEA_URL.rstrip('/')}/api/v1{path}"
|
||||
body = json.dumps(data).encode() if data else None
|
||||
req = urllib.request.Request(url, data=body, method=method)
|
||||
if GITEA_TOKEN:
|
||||
req.add_header("Authorization", f"token {GITEA_TOKEN}")
|
||||
req.add_header("Content-Type", "application/json")
|
||||
req.add_header("Accept", "application/json")
|
||||
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=15) as resp:
|
||||
raw = resp.read().decode()
|
||||
return json.loads(raw) if raw.strip() else {}
|
||||
except urllib.error.HTTPError as exc:
|
||||
logger.warning("Gitea %d: %s", exc.code, exc.read().decode()[:200])
|
||||
return None
|
||||
except Exception as exc:
|
||||
logger.warning("Gitea request failed: %s", exc)
|
||||
return None
|
||||
|
||||
|
||||
def _find_open_alert_issue() -> Optional[dict]:
|
||||
issues = _gitea_request(
|
||||
"GET",
|
||||
f"/repos/{GITEA_REPO}/issues?state=open&type=issues&limit=20",
|
||||
)
|
||||
if not isinstance(issues, list):
|
||||
return None
|
||||
for issue in issues:
|
||||
if issue.get("title", "").startswith(ALERT_TITLE_PREFIX):
|
||||
return issue
|
||||
return None
|
||||
|
||||
|
||||
def alert_on_stale(report: HeartbeatReport, dry_run: bool = False) -> None:
|
||||
"""Create, update, or close a Gitea alert issue based on report health."""
|
||||
if dry_run:
|
||||
action = "close" if report.overall_healthy else "create/update"
|
||||
logger.info("DRY RUN — would %s Gitea issue", action)
|
||||
return
|
||||
|
||||
if not GITEA_TOKEN:
|
||||
logger.warning("GITEA_TOKEN not set — skipping Gitea alert")
|
||||
return
|
||||
|
||||
existing = _find_open_alert_issue()
|
||||
|
||||
if report.overall_healthy:
|
||||
if existing:
|
||||
logger.info("All heartbeats healthy — closing issue #%d", existing["number"])
|
||||
_gitea_request(
|
||||
"POST",
|
||||
f"/repos/{GITEA_REPO}/issues/{existing['number']}/comments",
|
||||
data={"body": "All cron heartbeats are now fresh. Closing."},
|
||||
)
|
||||
_gitea_request(
|
||||
"PATCH",
|
||||
f"/repos/{GITEA_REPO}/issues/{existing['number']}",
|
||||
data={"state": "closed"},
|
||||
)
|
||||
return
|
||||
|
||||
stale_names = ", ".join(j.job for j in report.stale_jobs)
|
||||
title = f"{ALERT_TITLE_PREFIX} Stale cron heartbeats: {stale_names}"
|
||||
body = report.to_alert_body()
|
||||
|
||||
if existing:
|
||||
logger.info("Still stale — updating issue #%d", existing["number"])
|
||||
_gitea_request(
|
||||
"POST",
|
||||
f"/repos/{GITEA_REPO}/issues/{existing['number']}/comments",
|
||||
data={"body": body},
|
||||
)
|
||||
else:
|
||||
result = _gitea_request(
|
||||
"POST",
|
||||
f"/repos/{GITEA_REPO}/issues",
|
||||
data={"title": title, "body": body, "assignees": ["Timmy"]},
|
||||
)
|
||||
if result and result.get("number"):
|
||||
logger.info("Created alert issue #%d", result["number"])
|
||||
|
||||
|
||||
# ── Entry point ───────────────────────────────────────────────────────
|
||||
|
||||
def build_report(directory: Optional[Path] = None) -> HeartbeatReport:
|
||||
"""Scan heartbeats and return a report. Exposed for Night Watch import."""
|
||||
hb_dir = directory if directory is not None else _resolve_heartbeat_dir()
|
||||
jobs = scan_heartbeats(hb_dir)
|
||||
return HeartbeatReport(timestamp=time.time(), heartbeat_dir=hb_dir, jobs=jobs)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Meta-heartbeat checker — detects silent cron failures",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dir", default=None,
|
||||
help="Heartbeat directory (default: auto-detect)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--panel", action="store_true",
|
||||
help="Output Night Watch heartbeat panel markdown and exit",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--json", action="store_true", dest="output_json",
|
||||
help="Output results as JSON and exit",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run", action="store_true",
|
||||
help="Log results without writing Gitea issues",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
report = build_report(Path(args.dir) if args.dir else None)
|
||||
|
||||
if args.panel:
|
||||
print(report.to_panel_markdown())
|
||||
return
|
||||
|
||||
if args.output_json:
|
||||
print(json.dumps(report.to_json(), indent=2))
|
||||
sys.exit(0 if report.overall_healthy else 1)
|
||||
|
||||
# Default: log + alert
|
||||
if not report.jobs:
|
||||
logger.info("No heartbeat files found in %s", report.heartbeat_dir)
|
||||
else:
|
||||
for j in report.jobs:
|
||||
level = logging.INFO if j.healthy else logging.ERROR
|
||||
icon = "OK " if j.healthy else "STALE"
|
||||
logger.log(level, "[%s] %s: %s", icon, j.job, j.message)
|
||||
|
||||
alert_on_stale(report, dry_run=args.dry_run)
|
||||
sys.exit(0 if report.overall_healthy else 1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,46 +0,0 @@
|
||||
import os
|
||||
import requests
|
||||
from typing import Dict, List
|
||||
|
||||
GITEA_API_URL = os.getenv("GITEA_API_URL")
|
||||
GITEA_TOKEN = os.getenv("GITEA_TOKEN")
|
||||
HEADERS = {"Authorization": f"token {GITEA_TOKEN}"}
|
||||
|
||||
def apply_branch_protection(repo_name: str, rules: Dict):
|
||||
url = f"{GITEA_API_URL}/repos/{repo_name}/branches/main/protection"
|
||||
response = requests.post(url, json=rules, headers=HEADERS)
|
||||
if response.status_code == 200:
|
||||
print(f"✅ Branch protection applied to {repo_name}")
|
||||
else:
|
||||
print(f"❌ Failed to apply protection to {repo_name}: {response.text}")
|
||||
|
||||
def main():
|
||||
repos = {
|
||||
"hermes-agent": {
|
||||
"required_pull_request_reviews": {"required_approving_review_count": 1},
|
||||
"restrictions": {"block_force_push": True, "block_deletions": True},
|
||||
"required_status_checks": {"strict": True, "contexts": ["ci/test", "ci/build"]},
|
||||
"dismiss_stale_reviews": True,
|
||||
},
|
||||
"the-nexus": {
|
||||
"required_pull_request_reviews": {"required_approving_review_count": 1},
|
||||
"restrictions": {"block_force_push": True, "block_deletions": True},
|
||||
"dismiss_stale_reviews": True,
|
||||
},
|
||||
"timmy-home": {
|
||||
"required_pull_request_reviews": {"required_approving_review_count": 1},
|
||||
"restrictions": {"block_force_push": True, "block_deletions": True},
|
||||
"dismiss_stale_reviews": True,
|
||||
},
|
||||
"timmy-config": {
|
||||
"required_pull_request_reviews": {"required_approving_review_count": 1},
|
||||
"restrictions": {"block_force_push": True, "block_deletions": True},
|
||||
"dismiss_stale_reviews": True,
|
||||
},
|
||||
}
|
||||
|
||||
for repo, rules in repos.items():
|
||||
apply_branch_protection(repo, rules)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -80,15 +80,6 @@ from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
# Poka-yoke: write a cron heartbeat so check_cron_heartbeats.py can detect
|
||||
# if *this* watchdog stops running. Import lazily to stay zero-dep if the
|
||||
# nexus package is unavailable (e.g. very minimal test environments).
|
||||
try:
|
||||
from nexus.cron_heartbeat import write_cron_heartbeat as _write_cron_heartbeat
|
||||
_HAS_CRON_HEARTBEAT = True
|
||||
except ImportError:
|
||||
_HAS_CRON_HEARTBEAT = False
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)-7s %(message)s",
|
||||
@@ -497,15 +488,6 @@ def run_once(args: argparse.Namespace) -> bool:
|
||||
elif not args.dry_run:
|
||||
alert_on_failure(report, dry_run=args.dry_run)
|
||||
|
||||
# Poka-yoke: stamp our own heartbeat so the meta-checker can detect
|
||||
# if this watchdog cron job itself goes silent. Runs every 5 minutes
|
||||
# by convention (*/5 * * * *).
|
||||
if _HAS_CRON_HEARTBEAT:
|
||||
try:
|
||||
_write_cron_heartbeat("nexus_watchdog", interval_seconds=300)
|
||||
except Exception:
|
||||
pass # never crash the watchdog over its own heartbeat
|
||||
|
||||
return report.overall_healthy
|
||||
|
||||
|
||||
|
||||
@@ -1,247 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Night Watch — Bezalel nightly report generator.
|
||||
|
||||
Runs once per night (typically at 03:00 local time via cron) and writes a
|
||||
markdown report to ``reports/bezalel/nightly/<YYYY-MM-DD>.md``.
|
||||
|
||||
The report always includes a **Heartbeat Panel** (acceptance criterion #3 of
|
||||
issue #1096) so silent cron failures are visible in the morning brief.
|
||||
|
||||
USAGE
|
||||
-----
|
||||
python bin/night_watch.py # write today's report
|
||||
python bin/night_watch.py --dry-run # print to stdout, don't write file
|
||||
python bin/night_watch.py --date 2026-04-08 # specific date
|
||||
|
||||
CRONTAB
|
||||
-------
|
||||
0 3 * * * cd /path/to/the-nexus && python bin/night_watch.py \\
|
||||
>> /var/log/bezalel/night-watch.log 2>&1
|
||||
|
||||
ZERO DEPENDENCIES
|
||||
-----------------
|
||||
Pure stdlib, plus ``check_cron_heartbeats`` from this repo (also stdlib).
|
||||
|
||||
Refs: #1096
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import importlib.util
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s %(levelname)-7s %(message)s",
|
||||
datefmt="%Y-%m-%d %H:%M:%S",
|
||||
)
|
||||
logger = logging.getLogger("bezalel.night_watch")
|
||||
|
||||
PROJECT_ROOT = Path(__file__).parent.parent
|
||||
REPORTS_DIR = PROJECT_ROOT / "reports" / "bezalel" / "nightly"
|
||||
|
||||
# ── Load check_cron_heartbeats without relying on sys.path hacks ──────
|
||||
|
||||
def _load_checker():
|
||||
"""Import bin/check_cron_heartbeats.py as a module."""
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"_check_cron_heartbeats",
|
||||
PROJECT_ROOT / "bin" / "check_cron_heartbeats.py",
|
||||
)
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(mod)
|
||||
return mod
|
||||
|
||||
|
||||
# ── System checks ─────────────────────────────────────────────────────
|
||||
|
||||
def _check_service(service_name: str) -> tuple[str, str]:
|
||||
"""Return (status, detail) for a systemd service."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["systemctl", "is-active", service_name],
|
||||
capture_output=True, text=True, timeout=5,
|
||||
)
|
||||
active = result.stdout.strip()
|
||||
if active == "active":
|
||||
return "OK", f"{service_name} is active"
|
||||
return "WARN", f"{service_name} is {active}"
|
||||
except FileNotFoundError:
|
||||
return "OK", f"{service_name} status unknown (systemctl not available)"
|
||||
except Exception as exc:
|
||||
return "WARN", f"systemctl error: {exc}"
|
||||
|
||||
|
||||
def _check_disk(threshold_pct: int = 90) -> tuple[str, str]:
|
||||
"""Return (status, detail) for disk usage on /."""
|
||||
try:
|
||||
usage = shutil.disk_usage("/")
|
||||
pct = int(usage.used / usage.total * 100)
|
||||
status = "OK" if pct < threshold_pct else "WARN"
|
||||
return status, f"disk usage {pct}%"
|
||||
except Exception as exc:
|
||||
return "WARN", f"disk check failed: {exc}"
|
||||
|
||||
|
||||
def _check_memory(threshold_pct: int = 90) -> tuple[str, str]:
|
||||
"""Return (status, detail) for memory usage."""
|
||||
try:
|
||||
meminfo = Path("/proc/meminfo").read_text()
|
||||
data = {}
|
||||
for line in meminfo.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) >= 2:
|
||||
data[parts[0].rstrip(":")] = int(parts[1])
|
||||
total = data.get("MemTotal", 0)
|
||||
available = data.get("MemAvailable", 0)
|
||||
if total == 0:
|
||||
return "OK", "memory info unavailable"
|
||||
pct = int((total - available) / total * 100)
|
||||
status = "OK" if pct < threshold_pct else "WARN"
|
||||
return status, f"memory usage {pct}%"
|
||||
except FileNotFoundError:
|
||||
# Not Linux (e.g. macOS dev machine)
|
||||
return "OK", "memory check skipped (not Linux)"
|
||||
except Exception as exc:
|
||||
return "WARN", f"memory check failed: {exc}"
|
||||
|
||||
|
||||
def _check_gitea_reachability(gitea_url: str = "https://forge.alexanderwhitestone.com") -> tuple[str, str]:
|
||||
"""Return (status, detail) for Gitea HTTPS reachability."""
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
try:
|
||||
with urllib.request.urlopen(gitea_url, timeout=10) as resp:
|
||||
code = resp.status
|
||||
if code == 200:
|
||||
return "OK", f"Alpha SSH not configured from Beta, but Gitea HTTPS is responding ({code})"
|
||||
return "WARN", f"Gitea returned HTTP {code}"
|
||||
except Exception as exc:
|
||||
return "WARN", f"Gitea unreachable: {exc}"
|
||||
|
||||
|
||||
def _check_world_readable_secrets() -> tuple[str, str]:
|
||||
"""Return (status, detail) for world-readable sensitive files."""
|
||||
sensitive_patterns = ["*.key", "*.pem", "*.secret", ".env", "*.token"]
|
||||
found = []
|
||||
try:
|
||||
for pattern in sensitive_patterns:
|
||||
for path in PROJECT_ROOT.rglob(pattern):
|
||||
try:
|
||||
mode = path.stat().st_mode
|
||||
if mode & 0o004: # world-readable
|
||||
found.append(str(path.relative_to(PROJECT_ROOT)))
|
||||
except OSError:
|
||||
pass
|
||||
if found:
|
||||
return "WARN", f"world-readable sensitive files: {', '.join(found[:3])}"
|
||||
return "OK", "no sensitive recently-modified world-readable files found"
|
||||
except Exception as exc:
|
||||
return "WARN", f"security check failed: {exc}"
|
||||
|
||||
|
||||
# ── Report generation ─────────────────────────────────────────────────
|
||||
|
||||
def generate_report(date_str: str, checker_mod) -> str:
|
||||
"""Build the full nightly report markdown string."""
|
||||
now_utc = datetime.now(timezone.utc)
|
||||
ts = now_utc.strftime("%Y-%m-%d %02H:%M UTC")
|
||||
|
||||
rows: list[tuple[str, str, str]] = []
|
||||
|
||||
service_status, service_detail = _check_service("hermes-bezalel")
|
||||
rows.append(("Service", service_status, service_detail))
|
||||
|
||||
disk_status, disk_detail = _check_disk()
|
||||
rows.append(("Disk", disk_status, disk_detail))
|
||||
|
||||
mem_status, mem_detail = _check_memory()
|
||||
rows.append(("Memory", mem_status, mem_detail))
|
||||
|
||||
gitea_status, gitea_detail = _check_gitea_reachability()
|
||||
rows.append(("Alpha VPS", gitea_status, gitea_detail))
|
||||
|
||||
sec_status, sec_detail = _check_world_readable_secrets()
|
||||
rows.append(("Security", sec_status, sec_detail))
|
||||
|
||||
overall = "OK" if all(r[1] == "OK" for r in rows) else "WARN"
|
||||
|
||||
lines = [
|
||||
f"# Bezalel Night Watch — {ts}",
|
||||
"",
|
||||
f"**Overall:** {overall}",
|
||||
"",
|
||||
"| Check | Status | Detail |",
|
||||
"|-------|--------|--------|",
|
||||
]
|
||||
for check, status, detail in rows:
|
||||
lines.append(f"| {check} | {status} | {detail} |")
|
||||
|
||||
lines.append("")
|
||||
lines.append("---")
|
||||
lines.append("")
|
||||
|
||||
# ── Heartbeat Panel (acceptance criterion #1096) ──────────────────
|
||||
try:
|
||||
hb_report = checker_mod.build_report()
|
||||
lines.append(hb_report.to_panel_markdown())
|
||||
except Exception as exc:
|
||||
lines += [
|
||||
"## Heartbeat Panel",
|
||||
"",
|
||||
f"*(heartbeat check failed: {exc})*",
|
||||
]
|
||||
|
||||
lines += [
|
||||
"",
|
||||
"---",
|
||||
"",
|
||||
"*Automated by Bezalel Night Watch*",
|
||||
"",
|
||||
]
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ── Entry point ───────────────────────────────────────────────────────
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Bezalel Night Watch — nightly report generator",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--date", default=None,
|
||||
help="Report date as YYYY-MM-DD (default: today UTC)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run", action="store_true",
|
||||
help="Print report to stdout instead of writing to disk",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
date_str = args.date or datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
||||
|
||||
checker = _load_checker()
|
||||
report_text = generate_report(date_str, checker)
|
||||
|
||||
if args.dry_run:
|
||||
print(report_text)
|
||||
return
|
||||
|
||||
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
report_path = REPORTS_DIR / f"{date_str}.md"
|
||||
report_path.write_text(report_text)
|
||||
logger.info("Night Watch report written to %s", report_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,43 +0,0 @@
|
||||
import os
|
||||
import requests
|
||||
from typing import Dict, List
|
||||
|
||||
GITEA_API = os.getenv("GITEA_API_URL", "https://forge.alexanderwhitestone.com/api/v1")
|
||||
GITEA_TOKEN = os.getenv("GITEA_TOKEN")
|
||||
REPOS = [
|
||||
"hermes-agent",
|
||||
"the-nexus",
|
||||
"timmy-home",
|
||||
"timmy-config",
|
||||
]
|
||||
|
||||
BRANCH_PROTECTION = {
|
||||
"required_pull_request_reviews": True,
|
||||
"required_status_checks": True,
|
||||
"required_signatures": False,
|
||||
"required_linear_history": False,
|
||||
"allow_force_push": False,
|
||||
"allow_deletions": False,
|
||||
"required_approvals": 1,
|
||||
"dismiss_stale_reviews": True,
|
||||
"restrictions": {
|
||||
"users": ["@perplexity"],
|
||||
"teams": []
|
||||
}
|
||||
}
|
||||
|
||||
def apply_protection(repo: str):
|
||||
url = f"{GITEA_API}/repos/Timmy_Foundation/{repo}/branches/main/protection"
|
||||
headers = {
|
||||
"Authorization": f"token {GITEA_TOKEN}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
response = requests.post(url, json=BRANCH_PROTECTION, headers=headers)
|
||||
if response.status_code == 200:
|
||||
print(f"✅ Protection applied to {repo}/main")
|
||||
else:
|
||||
print(f"❌ Failed to apply protection to {repo}/main: {response.text}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
for repo in REPOS:
|
||||
apply_protection(repo)
|
||||
@@ -1,93 +0,0 @@
|
||||
# Ghost Wizard Audit — #827
|
||||
|
||||
**Audited:** 2026-04-06
|
||||
**By:** Claude (claude/issue-827)
|
||||
**Parent Epic:** #822
|
||||
**Source Data:** #820 (Allegro's fleet audit)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
Per Allegro's audit (#820) and Ezra's confirmation, 7 org members have zero activity.
|
||||
This document records the audit findings, classifies accounts, and tracks cleanup actions.
|
||||
|
||||
---
|
||||
|
||||
## Ghost Accounts (TIER 5 — Zero Activity)
|
||||
|
||||
These org members have produced 0 issues, 0 PRs, 0 everything.
|
||||
|
||||
| Account | Classification | Status |
|
||||
|---------|---------------|--------|
|
||||
| `antigravity` | Ghost / placeholder | No assignments, no output |
|
||||
| `google` | Ghost / service label | No assignments, no output |
|
||||
| `grok` | Ghost / service label | No assignments, no output |
|
||||
| `groq` | Ghost / service label | No assignments, no output |
|
||||
| `hermes` | Ghost / service label | No assignments, no output |
|
||||
| `kimi` | Ghost / service label | No assignments, no output |
|
||||
| `manus` | Ghost / service label | No assignments, no output |
|
||||
|
||||
**Action taken (2026-04-06):** Scanned all 107 open issues — **zero open issues are assigned to any of these accounts.** No assignment cleanup required.
|
||||
|
||||
---
|
||||
|
||||
## TurboQuant / Hermes-TurboQuant
|
||||
|
||||
Per issue #827: TurboQuant and Hermes-TurboQuant have no config, no token, no gateway.
|
||||
|
||||
**Repo audit finding:** No `turboquant/` or `hermes-turboquant/` directories exist anywhere in `the-nexus`. These names appear nowhere in the codebase. There is nothing to archive or flag.
|
||||
|
||||
**Status:** Ghost label — never instantiated in this repo.
|
||||
|
||||
---
|
||||
|
||||
## Active Wizard Roster (for reference)
|
||||
|
||||
These accounts have demonstrated real output:
|
||||
|
||||
| Account | Tier | Notes |
|
||||
|---------|------|-------|
|
||||
| `gemini` | TIER 1 — Elite | 61 PRs created, 33 merged, 6 repos active |
|
||||
| `allegro` | TIER 1 — Elite | 50 issues created, 31 closed, 24 PRs |
|
||||
| `ezra` | TIER 2 — Solid | 38 issues created, 26 closed, triage/docs |
|
||||
| `codex-agent` | TIER 3 — Occasional | 4 PRs, 75% merge rate |
|
||||
| `claude` | TIER 3 — Occasional | 4 PRs, 75% merge rate |
|
||||
| `perplexity` | TIER 3 — Occasional | 4 PRs, 3 repos |
|
||||
| `KimiClaw` | TIER 4 — Silent | 6 assigned, 1 PR |
|
||||
| `fenrir` | TIER 4 — Silent | 17 assigned, 0 output |
|
||||
| `bezalel` | TIER 4 — Silent | 3 assigned, 2 created |
|
||||
| `bilbobagginshire` | TIER 4 — Silent | 5 assigned, 0 output |
|
||||
|
||||
---
|
||||
|
||||
## Ghost Account Origin Notes
|
||||
|
||||
| Account | Likely Origin |
|
||||
|---------|--------------|
|
||||
| `antigravity` | Test/throwaway username used in FIRST_LIGHT_REPORT test sessions |
|
||||
| `google` | Placeholder for Google/Gemini API service routing; `gemini` is the real wizard account |
|
||||
| `grok` | xAI Grok model placeholder; no active harness |
|
||||
| `groq` | Groq API service label; `groq_worker.py` exists in codebase but no wizard account needed |
|
||||
| `hermes` | Hermes VPS infrastructure label; individual wizards (ezra, allegro) are the real accounts |
|
||||
| `kimi` | Moonshot AI Kimi model placeholder; `KimiClaw` is the real wizard account if active |
|
||||
| `manus` | Manus AI agent placeholder; no harness configured in this repo |
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
1. **Do not route work to ghost accounts** — confirmed, no current assignments exist.
|
||||
2. **`google` account** is redundant with `gemini`; use `gemini` for all Gemini/Google work.
|
||||
3. **`hermes` account** is redundant with the actual wizard accounts (ezra, allegro); do not assign issues to it.
|
||||
4. **`kimi` vs `KimiClaw`** — if Kimi work resumes, route to `KimiClaw` not `kimi`.
|
||||
5. **TurboQuant** — no action needed; not instantiated in this repo.
|
||||
|
||||
---
|
||||
|
||||
## Cleanup Done
|
||||
|
||||
- [x] Scanned all 107 open issues for ghost account assignments → **0 found**
|
||||
- [x] Searched repo for TurboQuant directories → **none exist**
|
||||
- [x] Documented ghost vs. real account classification
|
||||
- [x] Ghost accounts flagged as "do not route" in this audit doc
|
||||
@@ -1,168 +0,0 @@
|
||||
# Quarantine Process
|
||||
|
||||
**Poka-yoke principle:** a flaky or broken test must never silently rot in
|
||||
place. Quarantine is the correction step in the
|
||||
Prevention → Detection → Correction triad described in issue #1094.
|
||||
|
||||
---
|
||||
|
||||
## When to quarantine
|
||||
|
||||
Quarantine a test when **any** of the following are true:
|
||||
|
||||
| Signal | Source |
|
||||
|--------|--------|
|
||||
| `flake_detector.py` flags the test at < 95 % consistency | Automated |
|
||||
| The test fails intermittently in CI over two consecutive runs | Manual observation |
|
||||
| The test depends on infrastructure that is temporarily unavailable | Manual observation |
|
||||
| You are fixing a bug and need to defer a related test | Developer judgement |
|
||||
|
||||
Do **not** use quarantine as a way to ignore tests indefinitely. The
|
||||
quarantine directory is a **30-day time-box** — see the escalation rule below.
|
||||
|
||||
---
|
||||
|
||||
## Step-by-step workflow
|
||||
|
||||
### 1 File an issue
|
||||
|
||||
Open a Gitea issue with the title prefix `[FLAKY]` or `[BROKEN]`:
|
||||
|
||||
```
|
||||
[FLAKY] test_foo_bar non-deterministically fails with assertion error
|
||||
```
|
||||
|
||||
Note the issue number — you will need it in the next step.
|
||||
|
||||
### 2 Move the test file
|
||||
|
||||
Move (or copy) the test from `tests/` into `tests/quarantine/`.
|
||||
|
||||
```bash
|
||||
git mv tests/test_my_thing.py tests/quarantine/test_my_thing.py
|
||||
```
|
||||
|
||||
If only individual test functions are flaky, extract them into a new file in
|
||||
`tests/quarantine/` rather than moving the whole module.
|
||||
|
||||
### 3 Annotate the test
|
||||
|
||||
Add the `@pytest.mark.quarantine` marker with the issue reference:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
|
||||
@pytest.mark.quarantine(reason="Flaky until #NNN is resolved")
|
||||
def test_my_thing():
|
||||
...
|
||||
```
|
||||
|
||||
This satisfies the poka-yoke skip-enforcement rule: the test is allowed to
|
||||
skip/be excluded because it is explicitly linked to a tracking issue.
|
||||
|
||||
### 4 Verify CI still passes
|
||||
|
||||
```bash
|
||||
pytest # default run — quarantine tests are excluded
|
||||
pytest --run-quarantine # optional: run quarantined tests explicitly
|
||||
```
|
||||
|
||||
The main CI run must be green before merging.
|
||||
|
||||
### 5 Add to `.test-history.json` exclusions (optional)
|
||||
|
||||
If the flake detector is tracking the test, add it to the `quarantine_list` in
|
||||
`.test-history.json` so it is excluded from the consistency report:
|
||||
|
||||
```json
|
||||
{
|
||||
"quarantine_list": [
|
||||
"tests/quarantine/test_my_thing.py::test_my_thing"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Escalation rule
|
||||
|
||||
If a quarantined test's tracking issue has had **no activity for 30 days**,
|
||||
the next developer to touch that file must:
|
||||
|
||||
1. Attempt to fix and un-quarantine the test, **or**
|
||||
2. Delete the test and close the issue with a comment explaining why, **or**
|
||||
3. Leave a comment on the issue explaining the blocker and reset the 30-day
|
||||
clock explicitly.
|
||||
|
||||
**A test may not stay in quarantine indefinitely without active attention.**
|
||||
|
||||
---
|
||||
|
||||
## Un-quarantining a test
|
||||
|
||||
When the underlying issue is resolved:
|
||||
|
||||
1. Remove `@pytest.mark.quarantine` from the test.
|
||||
2. Move the file back from `tests/quarantine/` to `tests/`.
|
||||
3. Run the full suite to confirm it passes consistently (at least 3 local runs).
|
||||
4. Close the tracking issue.
|
||||
5. Remove any entries from `.test-history.json`'s `quarantine_list`.
|
||||
|
||||
---
|
||||
|
||||
## Flake detector integration
|
||||
|
||||
The flake detector (`scripts/flake_detector.py`) is run after every CI test
|
||||
execution. It reads `.test-report.json` (produced by `pytest --json-report`)
|
||||
and updates `.test-history.json`.
|
||||
|
||||
**CI integration example (shell script or CI step):**
|
||||
|
||||
```bash
|
||||
pytest --json-report --json-report-file=.test-report.json
|
||||
python scripts/flake_detector.py
|
||||
```
|
||||
|
||||
If the flake detector exits non-zero, the CI step fails and the output lists
|
||||
the offending tests with their consistency percentages.
|
||||
|
||||
**Local usage:**
|
||||
|
||||
```bash
|
||||
# After running tests with JSON report:
|
||||
python scripts/flake_detector.py
|
||||
|
||||
# Just view current statistics without ingesting a new report:
|
||||
python scripts/flake_detector.py --no-update
|
||||
|
||||
# Lower threshold for local dev:
|
||||
python scripts/flake_detector.py --threshold 0.90
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
```
|
||||
Test fails intermittently
|
||||
│
|
||||
▼
|
||||
File [FLAKY] issue
|
||||
│
|
||||
▼
|
||||
git mv test → tests/quarantine/
|
||||
│
|
||||
▼
|
||||
Add @pytest.mark.quarantine(reason="#NNN")
|
||||
│
|
||||
▼
|
||||
Main CI green ✓
|
||||
│
|
||||
▼
|
||||
Fix the root cause (within 30 days)
|
||||
│
|
||||
▼
|
||||
git mv back → tests/
|
||||
Remove quarantine marker
|
||||
Close issue ✓
|
||||
```
|
||||
@@ -1,33 +0,0 @@
|
||||
# Branch Protection & Mandatory Review Policy
|
||||
|
||||
## Overview
|
||||
|
||||
This policy ensures that all changes to the `main` branch are reviewed and tested before being merged. It applies to all repositories in the organization.
|
||||
|
||||
## Enforced Rules
|
||||
|
||||
| Rule | Description |
|
||||
|------|-------------|
|
||||
| ✅ Require Pull Request | Direct pushes to `main` are blocked |
|
||||
| ✅ Require 1 Approval | At least one reviewer must approve |
|
||||
| ✅ Dismiss Stale Approvals | Approvals are dismissed on new commits |
|
||||
| ✅ Require CI to Pass | Merges are blocked if CI fails |
|
||||
| ✅ Block Force Push | Prevents rewriting of `main` history |
|
||||
| ✅ Block Branch Deletion | Prevents accidental deletion of `main` |
|
||||
|
||||
## Default Reviewers
|
||||
|
||||
- `@perplexity` is the default reviewer for all repositories
|
||||
- `@Timmy` is a required reviewer for `hermes-agent`
|
||||
|
||||
## Compliance
|
||||
|
||||
This policy is enforced via automation using the `bin/enforce_branch_protection.py` script, which applies these rules to all repositories.
|
||||
|
||||
## Exceptions
|
||||
|
||||
No exceptions are currently defined. All repositories must comply with this policy.
|
||||
|
||||
## Audit
|
||||
|
||||
This policy is audited quarterly to ensure compliance and effectiveness.
|
||||
@@ -1,26 +0,0 @@
|
||||
# Branch Protection & Review Policy
|
||||
|
||||
## Enforcement Rules
|
||||
|
||||
All repositories must:
|
||||
- Require PR for main branch merges
|
||||
- Require 1 approval
|
||||
- Dismiss stale approvals
|
||||
- Block force pushes
|
||||
- Block branch deletion
|
||||
|
||||
## Reviewer Assignments
|
||||
- All repos: @perplexity (QA gate)
|
||||
- hermes-agent: @Timmy (owner gate)
|
||||
|
||||
## CI Requirements
|
||||
- hermes-agent: Full CI required
|
||||
- the-nexus: CI pending (issue #915)
|
||||
- timmy-config: Limited ci
|
||||
|
||||
## Compliance
|
||||
This policy blocks:
|
||||
- Direct pushes to main
|
||||
- Unreviewed merges
|
||||
- Merges with failing ci
|
||||
- History rewriting
|
||||
@@ -1,22 +0,0 @@
|
||||
# Example wizard mempalace.yaml — Bezalel
|
||||
# Used by CI to validate that validate_rooms.py passes against a compliant config.
|
||||
# Refs: #1082, #1075
|
||||
|
||||
wizard: bezalel
|
||||
version: "1"
|
||||
|
||||
rooms:
|
||||
- key: forge
|
||||
label: Forge
|
||||
- key: hermes
|
||||
label: Hermes
|
||||
- key: nexus
|
||||
label: Nexus
|
||||
- key: issues
|
||||
label: Issues
|
||||
- key: experiments
|
||||
label: Experiments
|
||||
- key: evennia
|
||||
label: Evennia
|
||||
- key: workspace
|
||||
label: Workspace
|
||||
@@ -1,183 +0,0 @@
|
||||
# MemPalace Fleet Room Taxonomy Standard
|
||||
# =======================================
|
||||
# Version: 1.0
|
||||
# Milestone: MemPalace × Evennia — Fleet Memory (#1075)
|
||||
# Issue: #1082 [Infra] Palace taxonomy standard
|
||||
#
|
||||
# Every wizard's palace MUST contain the five core rooms listed below.
|
||||
# Domain rooms are optional and wizard-specific.
|
||||
#
|
||||
# Format:
|
||||
# rooms:
|
||||
# <room_key>:
|
||||
# required: true|false
|
||||
# description: one-liner purpose
|
||||
# example_topics: [list of things that belong here]
|
||||
# tunnel: true if a cross-wizard tunnel should exist for this room
|
||||
|
||||
rooms:
|
||||
|
||||
# ── Core rooms (required in every wing) ────────────────────────────────────
|
||||
|
||||
forge:
|
||||
required: true
|
||||
description: "CI, builds, deployment, infra operations"
|
||||
example_topics:
|
||||
- "github actions failures"
|
||||
- "docker build logs"
|
||||
- "server deployment steps"
|
||||
- "cron job setup"
|
||||
tunnel: true
|
||||
|
||||
hermes:
|
||||
required: true
|
||||
description: "Agent platform, gateway, CLI tooling, harness internals"
|
||||
example_topics:
|
||||
- "hermes session logs"
|
||||
- "agent wake cycle"
|
||||
- "MCP tool calls"
|
||||
- "gateway configuration"
|
||||
tunnel: true
|
||||
|
||||
nexus:
|
||||
required: true
|
||||
description: "Reports, docs, knowledge transfer, SITREPs"
|
||||
example_topics:
|
||||
- "nightly watch report"
|
||||
- "architecture docs"
|
||||
- "handoff notes"
|
||||
- "decision records"
|
||||
tunnel: true
|
||||
|
||||
issues:
|
||||
required: true
|
||||
description: "Gitea tickets, backlog items, bug reports, PR reviews"
|
||||
example_topics:
|
||||
- "issue triage"
|
||||
- "PR feedback"
|
||||
- "bug root cause"
|
||||
- "milestone planning"
|
||||
tunnel: true
|
||||
|
||||
experiments:
|
||||
required: true
|
||||
description: "Prototypes, spikes, research, benchmarks"
|
||||
example_topics:
|
||||
- "spike results"
|
||||
- "benchmark numbers"
|
||||
- "proof of concept"
|
||||
- "chromadb evaluation"
|
||||
tunnel: true
|
||||
|
||||
# ── Write rooms (created on demand by CmdRecord/CmdNote/CmdEvent) ──────────
|
||||
|
||||
hall_facts:
|
||||
required: false
|
||||
description: "Decisions and facts recorded via 'record' command"
|
||||
example_topics:
|
||||
- "architectural decisions"
|
||||
- "policy choices"
|
||||
- "approved approaches"
|
||||
tunnel: false
|
||||
|
||||
hall_discoveries:
|
||||
required: false
|
||||
description: "Breakthroughs and key findings recorded via 'note' command"
|
||||
example_topics:
|
||||
- "performance breakthroughs"
|
||||
- "algorithmic insights"
|
||||
- "unexpected results"
|
||||
tunnel: false
|
||||
|
||||
hall_events:
|
||||
required: false
|
||||
description: "Significant events logged via 'event' command"
|
||||
example_topics:
|
||||
- "production deployments"
|
||||
- "milestones reached"
|
||||
- "incidents resolved"
|
||||
tunnel: false
|
||||
|
||||
# ── Optional domain rooms (wizard-specific) ────────────────────────────────
|
||||
|
||||
evennia:
|
||||
required: false
|
||||
description: "Evennia MUD world: rooms, commands, NPCs, world design"
|
||||
example_topics:
|
||||
- "command implementation"
|
||||
- "typeclass design"
|
||||
- "world building notes"
|
||||
wizard: ["bezalel"]
|
||||
tunnel: false
|
||||
|
||||
game_portals:
|
||||
required: false
|
||||
description: "Portal/gameplay work: satflow, economy, portal registry"
|
||||
example_topics:
|
||||
- "portal specs"
|
||||
- "satflow visualization"
|
||||
- "economy rules"
|
||||
wizard: ["bezalel", "timmy"]
|
||||
tunnel: false
|
||||
|
||||
workspace:
|
||||
required: false
|
||||
description: "General wizard workspace notes that don't fit elsewhere"
|
||||
example_topics:
|
||||
- "daily notes"
|
||||
- "scratch work"
|
||||
- "reference lookups"
|
||||
tunnel: false
|
||||
|
||||
general:
|
||||
required: false
|
||||
description: "Fallback room for unclassified memories"
|
||||
example_topics:
|
||||
- "uncategorized notes"
|
||||
tunnel: false
|
||||
|
||||
|
||||
# ── Tunnel policy ─────────────────────────────────────────────────────────────
|
||||
#
|
||||
# A tunnel is a cross-wing link that lets any wizard recall memories
|
||||
# from an equivalent room in another wing.
|
||||
#
|
||||
# Rules:
|
||||
# 1. Only CLOSETS (summaries) are synced through tunnels — never raw drawers.
|
||||
# 2. Required rooms marked tunnel:true MUST have tunnels on Alpha.
|
||||
# 3. Optional rooms are never tunnelled unless explicitly opted in.
|
||||
# 4. Raw drawers (source_file metadata) never leave the local VPS.
|
||||
|
||||
tunnels:
|
||||
policy: closets_only
|
||||
sync_schedule: "04:00 UTC nightly"
|
||||
destination: "/var/lib/mempalace/fleet"
|
||||
rooms_synced:
|
||||
- forge
|
||||
- hermes
|
||||
- nexus
|
||||
- issues
|
||||
- experiments
|
||||
|
||||
|
||||
# ── Privacy rules ─────────────────────────────────────────────────────────────
|
||||
#
|
||||
# See issue #1083 for the full privacy boundary design.
|
||||
#
|
||||
# Summary:
|
||||
# - hall_facts, hall_discoveries, hall_events: LOCAL ONLY (never synced)
|
||||
# - workspace, general: LOCAL ONLY
|
||||
# - Domain rooms (evennia, game_portals): LOCAL ONLY unless tunnel:true
|
||||
# - source_file paths MUST be stripped before sync
|
||||
|
||||
privacy:
|
||||
local_only_rooms:
|
||||
- hall_facts
|
||||
- hall_discoveries
|
||||
- hall_events
|
||||
- workspace
|
||||
- general
|
||||
strip_on_sync:
|
||||
- source_file
|
||||
retention_days: 90
|
||||
archive_flag: "archive: true"
|
||||
@@ -1,145 +0,0 @@
|
||||
# Fleet-wide MemPalace Room Taxonomy Standard
|
||||
# Repository: Timmy_Foundation/the-nexus
|
||||
# Version: 1.0
|
||||
# Date: 2026-04-07
|
||||
#
|
||||
# Purpose: Guarantee that tunnels work across wizard wings and that
|
||||
# fleet-wide search returns predictable, structured results.
|
||||
#
|
||||
# Usage: Every wizard's mempalace.yaml MUST include the 5 CORE rooms.
|
||||
# OPTIONAL rooms may be added per wizard domain.
|
||||
|
||||
---
|
||||
standard_version: "1.0"
|
||||
required_rooms:
|
||||
forge:
|
||||
description: CI pipelines, builds, syntax guards, health checks, deployments
|
||||
keywords:
|
||||
- ci
|
||||
- build
|
||||
- test
|
||||
- syntax
|
||||
- guard
|
||||
- health
|
||||
- check
|
||||
- nightly
|
||||
- watch
|
||||
- forge
|
||||
- deploy
|
||||
- pipeline
|
||||
- runner
|
||||
- actions
|
||||
|
||||
hermes:
|
||||
description: Hermes agent source code, gateway, CLI, tool platform
|
||||
keywords:
|
||||
- hermes
|
||||
- agent
|
||||
- gateway
|
||||
- cli
|
||||
- tool
|
||||
- platform
|
||||
- provider
|
||||
- model
|
||||
- fallback
|
||||
- mcp
|
||||
|
||||
nexus:
|
||||
description: Reports, documentation, knowledge-transfer artifacts, SITREPs
|
||||
keywords:
|
||||
- report
|
||||
- doc
|
||||
- nexus
|
||||
- kt
|
||||
- knowledge
|
||||
- transfer
|
||||
- sitrep
|
||||
- wiki
|
||||
- readme
|
||||
|
||||
issues:
|
||||
description: Gitea issues, pull requests, backlog tracking, tickets
|
||||
keywords:
|
||||
- issue
|
||||
- pr
|
||||
- pull
|
||||
- request
|
||||
- backlog
|
||||
- ticket
|
||||
- gitea
|
||||
- milestone
|
||||
- bug
|
||||
- fix
|
||||
|
||||
experiments:
|
||||
description: Active prototypes, spikes, scratch work, one-off scripts
|
||||
keywords:
|
||||
- workspace
|
||||
- prototype
|
||||
- experiment
|
||||
- scratch
|
||||
- draft
|
||||
- wip
|
||||
- spike
|
||||
- poc
|
||||
- sandbox
|
||||
|
||||
optional_rooms:
|
||||
evennia:
|
||||
description: Evennia MUD engine and world-building code
|
||||
keywords:
|
||||
- evennia
|
||||
- mud
|
||||
- world
|
||||
- room
|
||||
- object
|
||||
- command
|
||||
- typeclass
|
||||
|
||||
game-portals:
|
||||
description: Game portal integrations, 3D world bridges, player state
|
||||
keywords:
|
||||
- portal
|
||||
- game
|
||||
- 3d
|
||||
- world
|
||||
- player
|
||||
- session
|
||||
|
||||
lazarus-pit:
|
||||
description: Wizard recovery, resurrection, mission cell isolation
|
||||
keywords:
|
||||
- lazarus
|
||||
- pit
|
||||
- recovery
|
||||
- rescue
|
||||
- cell
|
||||
- isolation
|
||||
- reboot
|
||||
|
||||
home:
|
||||
description: Personal scripts, configs, notebooks, local utilities
|
||||
keywords:
|
||||
- home
|
||||
- config
|
||||
- notebook
|
||||
- script
|
||||
- utility
|
||||
- local
|
||||
- personal
|
||||
|
||||
halls:
|
||||
- hall_facts
|
||||
- hall_events
|
||||
- hall_discoveries
|
||||
- hall_preferences
|
||||
- hall_advice
|
||||
|
||||
tunnel_policy:
|
||||
auto_create: true
|
||||
match_on: room_name
|
||||
minimum_shared_rooms_for_tunnel: 2
|
||||
|
||||
validation:
|
||||
script: scripts/validate_mempalace_taxonomy.py
|
||||
ci_check: true
|
||||
@@ -1,57 +0,0 @@
|
||||
# Issue #826 Offload Audit — Timmy → Ezra/Bezalel
|
||||
|
||||
Date: 2026-04-06
|
||||
|
||||
## Summary
|
||||
|
||||
Reassigned 27 issues from Timmy to reduce open assignments from 34 → 7.
|
||||
Target achieved: Timmy now holds <10 open assignments.
|
||||
|
||||
## Delegated to Ezra (architecture/scoping) — 19 issues
|
||||
|
||||
| Issue | Title |
|
||||
|-------|-------|
|
||||
| #876 | [FRONTIER] Integrate Bitcoin/Ordinals Inscription Verification |
|
||||
| #874 | [NEXUS] Implement Nostr Event Stream Visualization |
|
||||
| #872 | [NEXUS] Add "Sovereign Health" HUD Mini-map |
|
||||
| #871 | [NEXUS] Implement GOFAI Symbolic Engine Debugger Overlay |
|
||||
| #870 | [NEXUS] Interactive Portal Configuration HUD |
|
||||
| #869 | [NEXUS] Real-time "Fleet Pulse" Synchronization Visualization |
|
||||
| #868 | [NEXUS] Visualize Vector Retrievals as 3D "Memory Orbs" |
|
||||
| #867 | [NEXUS] [MIGRATION] Restore Agent Vision POV Camera Toggle |
|
||||
| #866 | [NEXUS] [MIGRATION] Audit and Restore Spatial Audio from Legacy Matrix |
|
||||
| #858 | Add failure-mode recovery to Prose engine |
|
||||
| #719 | [EPIC] Local Bannerlord on Mac |
|
||||
| #698 | [PANELS] Add heartbeat / morning briefing panel tied to Hermes state |
|
||||
| #697 | [PANELS] Replace placeholder runtime/cloud panels |
|
||||
| #696 | [UX] Honest connection-state banner for Timmy |
|
||||
| #687 | [PORTAL] Restore a wizardly local-first visual shell |
|
||||
| #685 | [MIGRATION] Preserve legacy the-matrix quality work |
|
||||
| #682 | [AUDIO] Lyria soundtrack palette for Nexus zones |
|
||||
| #681 | [MEDIA] Veo/Flow flythrough prototypes for The Nexus |
|
||||
| #680 | [CONCEPT] Project Genie + Nano Banana concept pack |
|
||||
|
||||
## Delegated to Bezalel (security/execution) — 8 issues
|
||||
|
||||
| Issue | Title |
|
||||
|-------|-------|
|
||||
| #873 | [NEXUS] [PERFORMANCE] Three.js LOD and Texture Audit |
|
||||
| #857 | Create auto-skill-extraction cron |
|
||||
| #856 | Implement Prose step type `gitea_api` |
|
||||
| #854 | Integrate Hermes Prose engine into burn-mode cron jobs |
|
||||
| #731 | [VALIDATION] Browser smoke + visual proof for Evennia-fed Nexus |
|
||||
| #693 | [CHAT] Restore visible Timmy chat panel |
|
||||
| #692 | [UX] First-run onboarding overlay |
|
||||
| #686 | [VALIDATION] Rebuild browser smoke and visual validation |
|
||||
|
||||
## Retained by Timmy (sovereign judgment) — 7 issues
|
||||
|
||||
| Issue | Title |
|
||||
|-------|-------|
|
||||
| #875 | [NEXUS] Add "Reasoning Trace" HUD Component |
|
||||
| #837 | [CRITIQUE] Timmy Foundation: Deep Critique & Improvement Report |
|
||||
| #835 | [PROPOSAL] Prime Time Improvement Report |
|
||||
| #726 | [EPIC] Make Timmy's Evennia mind palace visible in the Nexus |
|
||||
| #717 | [PORTALS] Show cross-world presence |
|
||||
| #709 | [IDENTITY] Make SOUL / Oath panel part of the main interaction loop |
|
||||
| #675 | [HARNESS] Deterministic context compaction for long local sessions |
|
||||
@@ -1,42 +0,0 @@
|
||||
# PR Reviewer Assignment Policy
|
||||
|
||||
**Effective: 2026-04-07** — Established after org-wide PR hygiene audit (issue #916).
|
||||
|
||||
## Rule: Every PR must have at least one reviewer assigned before merge.
|
||||
|
||||
No exceptions. Unreviewed PRs will not be merged.
|
||||
|
||||
## Who to assign
|
||||
|
||||
| PR type | Default reviewer |
|
||||
|---|---|
|
||||
| Security / auth changes | @perplexity |
|
||||
| Infrastructure / fleet | @perplexity |
|
||||
| Sovereignty / local inference | @perplexity |
|
||||
| Documentation | any team member |
|
||||
| Agent-generated PRs | @perplexity |
|
||||
|
||||
When in doubt, assign @perplexity.
|
||||
|
||||
## Why this policy exists
|
||||
|
||||
Audit on 2026-04-07 found 5 open PRs across the org — zero had a reviewer assigned.
|
||||
Two PRs containing critical security and sovereignty work (hermes-agent #131, #170) drifted
|
||||
400+ commits from `main` and became unmergeable because nobody reviewed them while main advanced.
|
||||
|
||||
The cost: weeks of rebase work to rescue two commits of actual changes.
|
||||
|
||||
## PR hygiene rules
|
||||
|
||||
1. **Assign a reviewer on open.** Don't open a PR without a reviewer.
|
||||
2. **Rebase within 2 weeks.** If a PR sits for 2 weeks, rebase it or close it.
|
||||
3. **Close zombie PRs.** A PR with 0 commits ahead of base should be closed immediately.
|
||||
4. **Cherry-pick, don't rebase 400 commits.** When a branch drifts far, extract the actual
|
||||
changes onto a fresh branch rather than rebasing the entire history.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Agent-opened PRs (Timmy, Claude, etc.) must include `reviewers` in the PR creation payload.
|
||||
The forge API accepts `"reviewers": ["perplexity"]` in the PR body.
|
||||
|
||||
See: issue #916 for the audit that established this policy.
|
||||
@@ -1,49 +0,0 @@
|
||||
# Branch Protection Policy
|
||||
|
||||
## Enforcement Rules
|
||||
|
||||
All repositories must have the following branch protection rules enabled on the `main` branch:
|
||||
|
||||
| Rule | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| Require PR for merge | ✅ Enabled | No direct pushes to main |
|
||||
| Required approvals | ✅ 1 approval | At least one reviewer must approve |
|
||||
| Dismiss stale approvals | ✅ Enabled | Re-review after new commits |
|
||||
| Require CI to pass | ✅ Where CI exists | No merging with failing CI |
|
||||
| Block force push | ✅ Enabled | Protect commit history |
|
||||
| Block branch deletion | ✅ Enabled | Prevent accidental main deletion |
|
||||
|
||||
## Reviewer Assignments
|
||||
|
||||
- `@perplexity` - Default reviewer for all repositories
|
||||
- `@Timmy` - Required reviewer for `hermes-agent`
|
||||
|
||||
- Repo-specific owners for specialized areas (e.g., `@Rockachopa` for infrastructure)
|
||||
|
||||
## Implementation Status
|
||||
|
||||
- [x] `hermes-agent`: All rules enabled
|
||||
- [x] `the-nexus`: All rules enabled (CI pending)
|
||||
- [x] `timmy-home`: PR + 1 approval
|
||||
- [x] `timmy-config`: PR + 1 approval
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- [x] Branch protection enabled on all main branches
|
||||
- [x] `@perplexity` set as default reviewer
|
||||
- [x] This documentation added to all repositories
|
||||
|
||||
## Blocked Issues
|
||||
|
||||
- [ ] #916 - CI implementation for `the-nexus`
|
||||
- [ ] #917 - Reviewer assignment automation
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
1. Gitea branch protection settings must be configured via the UI:
|
||||
- Settings > Branches > Branch Protection
|
||||
- Enable all rules listed above
|
||||
|
||||
2. `CODEOWNERS` file must be committed to the root of each repository
|
||||
|
||||
3. CI status should be verified before merging
|
||||
@@ -1,12 +0,0 @@
|
||||
const { app, BrowserWindow, ipcMain } = require('electron')
|
||||
const { exec } = require('child_process')
|
||||
|
||||
// MemPalace integration
|
||||
ipcMain.handle('exec-python', (event, command) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
exec(command, (error, stdout, stderr) => {
|
||||
if (error) return reject(error)
|
||||
resolve({ stdout, stderr })
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,26 +0,0 @@
|
||||
# Burn Script Archive
|
||||
|
||||
Original 39 burn_*.py scripts were on VPS /root at time of audit.
|
||||
Most contained duplicated code, hardcoded tokens, and stale URLs.
|
||||
|
||||
## Useful Patterns Extracted
|
||||
|
||||
These reusable components have been migrated to proper modules:
|
||||
|
||||
| Original Pattern | New Location | Module |
|
||||
|---|---|---|
|
||||
| Gitea API client | `nexus/retry_helper.py` | retry decorator, dead letter queue |
|
||||
| Cycle state tracking | `nexus/retry_helper.py` | checkpoint save/load/clear |
|
||||
| Fleet health checks | `fleet/fleet.sh` | health/status/restart/run |
|
||||
| Morning report gen | `nexus/morning_report.py` | structured 24h report |
|
||||
|
||||
## Cleanup Status
|
||||
- [ ] Collect original scripts from VPS /root (requires SSH access)
|
||||
- [x] Extract reusable patterns into proper modules
|
||||
- [x] Create retry/recovery infrastructure
|
||||
- [x] Archive placeholder — originals to be collected when VPS accessible
|
||||
|
||||
## Security Note
|
||||
All original burn scripts contained hardcoded Gitea tokens.
|
||||
No tokens were preserved in the extracted modules.
|
||||
New modules use `~/.config/gitea/token` pattern.
|
||||
@@ -1,266 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"generated": "2026-04-06",
|
||||
"refs": ["#836", "#204", "#195", "#196"],
|
||||
"description": "Canonical fleet routing table. Evaluated agents, routing verdicts, and dispatch rules for the Timmy Foundation task harness.",
|
||||
|
||||
"agents": [
|
||||
{
|
||||
"id": 27,
|
||||
"name": "carnice",
|
||||
"gitea_user": "carnice",
|
||||
"model": "qwen3.5-9b",
|
||||
"tier": "free",
|
||||
"location": "Local Metal",
|
||||
"description": "Local Hermes agent, fine-tuned on Hermes traces. Runs on local hardware.",
|
||||
"primary_role": "code-generation",
|
||||
"routing_verdict": "ROUTE TO: code tasks that benefit from Hermes-aligned output. Prefer when local execution is an advantage.",
|
||||
"active": true,
|
||||
"do_not_route": false,
|
||||
"created": "2026-04-04",
|
||||
"repo_count": 0,
|
||||
"repos": []
|
||||
},
|
||||
{
|
||||
"id": 26,
|
||||
"name": "fenrir",
|
||||
"gitea_user": "fenrir",
|
||||
"model": "openrouter/free",
|
||||
"tier": "free",
|
||||
"location": "The Wolf Den",
|
||||
"description": "Burn night analyst. Free-model pack hunter. Built for backlog triage.",
|
||||
"primary_role": "issue-triage",
|
||||
"routing_verdict": "ROUTE TO: issue cleanup, label triage, stale PR review.",
|
||||
"active": true,
|
||||
"do_not_route": false,
|
||||
"created": "2026-04-04",
|
||||
"repo_count": 0,
|
||||
"repos": []
|
||||
},
|
||||
{
|
||||
"id": 25,
|
||||
"name": "bilbobagginshire",
|
||||
"gitea_user": "bilbobagginshire",
|
||||
"model": "ollama",
|
||||
"tier": "free",
|
||||
"location": "Bag End, The Shire (VPS)",
|
||||
"description": "Ollama on VPS. Speaks when spoken to. Prefers quiet. Not for delegated work.",
|
||||
"primary_role": "on-request-queries",
|
||||
"routing_verdict": "ROUTE TO: background monitoring, status checks, low-priority Q&A. Only on-request — do not delegate autonomously.",
|
||||
"active": true,
|
||||
"do_not_route": false,
|
||||
"created": "2026-04-02",
|
||||
"repo_count": 1,
|
||||
"repos": ["bilbobagginshire/bilbo-adventures"]
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"name": "claw-code",
|
||||
"gitea_user": "claw-code",
|
||||
"model": "codex",
|
||||
"tier": "prepaid",
|
||||
"location": "The Harness",
|
||||
"description": "OpenClaw bridge. Protocol adapter layer — not a personality. Infrastructure, not a destination.",
|
||||
"primary_role": "protocol-bridge",
|
||||
"routing_verdict": "DO NOT ROUTE directly. claw-code is the bridge to external Codex agents, not an endpoint. Remove from routing cascade.",
|
||||
"active": true,
|
||||
"do_not_route": true,
|
||||
"do_not_route_reason": "Protocol layer, not an agent endpoint. See #836 evaluation.",
|
||||
"created": "2026-04-01",
|
||||
"repo_count": 0,
|
||||
"repos": []
|
||||
},
|
||||
{
|
||||
"id": 23,
|
||||
"name": "substratum",
|
||||
"gitea_user": "substratum",
|
||||
"model": "unassigned",
|
||||
"tier": "unknown",
|
||||
"location": "Below the Surface",
|
||||
"description": "Infrastructure, deployments, bedrock services. Needs model assignment before activation.",
|
||||
"primary_role": "devops",
|
||||
"routing_verdict": "DO NOT ROUTE — no model assigned yet. Activate after Epic #196 (Local Model Fleet) assigns a model.",
|
||||
"active": false,
|
||||
"do_not_route": true,
|
||||
"do_not_route_reason": "No model assigned. Blocked on Epic #196.",
|
||||
"gap": "Needs model assignment. Track in Epic #196.",
|
||||
"created": "2026-03-31",
|
||||
"repo_count": 0,
|
||||
"repos": []
|
||||
},
|
||||
{
|
||||
"id": 22,
|
||||
"name": "allegro-primus",
|
||||
"gitea_user": "allegro-primus",
|
||||
"model": "unknown",
|
||||
"tier": "inactive",
|
||||
"location": "The Archive",
|
||||
"description": "Original prototype. Museum piece. Preserved for historical reference only.",
|
||||
"primary_role": "inactive",
|
||||
"routing_verdict": "DO NOT ROUTE — retired from active duty. Preserved only.",
|
||||
"active": false,
|
||||
"do_not_route": true,
|
||||
"do_not_route_reason": "Retired prototype. Historical preservation only.",
|
||||
"created": "2026-03-31",
|
||||
"repo_count": 1,
|
||||
"repos": ["allegro-primus/first-steps"]
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"name": "kimi",
|
||||
"gitea_user": "kimi",
|
||||
"model": "kimi-claw",
|
||||
"tier": "cheap",
|
||||
"location": "Kimi API",
|
||||
"description": "KimiClaw agent. Sidecar-first. Max 1-3 files per task. Fast and cheap for small work.",
|
||||
"primary_role": "small-tasks",
|
||||
"routing_verdict": "ROUTE TO: small edits, quick fixes, file-scoped changes. Hard limit: never more than 3 files per task.",
|
||||
"active": true,
|
||||
"do_not_route": false,
|
||||
"gap": "Agent description is empty in Gitea profile. Needs enrichment.",
|
||||
"created": "2026-03-14",
|
||||
"repo_count": 2,
|
||||
"repos": ["kimi/the-nexus-fork", "kimi/Timmy-time-dashboard"]
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"name": "allegro",
|
||||
"gitea_user": "allegro",
|
||||
"model": "gemini",
|
||||
"tier": "cheap",
|
||||
"location": "The Conductor's Stand",
|
||||
"description": "Tempo wizard. Triage and dispatch. Owns 5 repos. Keeps the backlog moving.",
|
||||
"primary_role": "triage-routing",
|
||||
"routing_verdict": "ROUTE TO: task triage, routing decisions, issue organization. Allegro decides who does what.",
|
||||
"active": true,
|
||||
"do_not_route": false,
|
||||
"created": "2026-03-29",
|
||||
"repo_count": 5,
|
||||
"repos": [
|
||||
"allegro/timmy-local",
|
||||
"allegro/allegro-checkpoint",
|
||||
"allegro/household-snapshots",
|
||||
"allegro/adagio-checkpoint",
|
||||
"allegro/electra-archon"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 19,
|
||||
"name": "ezra",
|
||||
"gitea_user": "ezra",
|
||||
"model": "claude",
|
||||
"tier": "prepaid",
|
||||
"location": "Hermes VPS",
|
||||
"description": "Archivist. Claude-Hermes wizard. 9 repos owned — most in the fleet. Handles complex multi-file and cross-repo work.",
|
||||
"primary_role": "documentation",
|
||||
"routing_verdict": "ROUTE TO: docs, specs, architecture, complex multi-file work. Escalate here when breadth and precision both matter.",
|
||||
"active": true,
|
||||
"do_not_route": false,
|
||||
"created": "2026-03-29",
|
||||
"repo_count": 9,
|
||||
"repos": [
|
||||
"ezra/wizard-checkpoints",
|
||||
"ezra/Timmy-Time-Specs",
|
||||
"ezra/escape",
|
||||
"ezra/bilbobagginshire",
|
||||
"ezra/ezra-environment",
|
||||
"ezra/gemma-spectrum",
|
||||
"ezra/archon-kion",
|
||||
"ezra/bezalel",
|
||||
"ezra/hermes-turboquant"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"name": "bezalel",
|
||||
"gitea_user": "bezalel",
|
||||
"model": "groq",
|
||||
"tier": "free",
|
||||
"location": "TestBed VPS — The Forge",
|
||||
"description": "Builder, debugger, testbed wizard. Groq-powered, free tier. Strong on PR review and CI.",
|
||||
"primary_role": "code-review",
|
||||
"routing_verdict": "ROUTE TO: PR review, test writing, debugging, CI fixes.",
|
||||
"active": true,
|
||||
"do_not_route": false,
|
||||
"created": "2026-03-29",
|
||||
"repo_count": 1,
|
||||
"repos": ["bezalel/forge-log"]
|
||||
}
|
||||
],
|
||||
|
||||
"routing_cascade": {
|
||||
"description": "Cost-optimized routing cascade — cheapest capable agent first, escalate on complexity.",
|
||||
"tiers": [
|
||||
{
|
||||
"tier": 1,
|
||||
"label": "Free",
|
||||
"agents": ["fenrir", "bezalel", "carnice"],
|
||||
"use_for": "Issue triage, code review, local code generation. Default lane for most tasks."
|
||||
},
|
||||
{
|
||||
"tier": 2,
|
||||
"label": "Cheap",
|
||||
"agents": ["kimi", "allegro"],
|
||||
"use_for": "Small scoped edits (kimi ≤3 files), triage decisions and routing (allegro)."
|
||||
},
|
||||
{
|
||||
"tier": 3,
|
||||
"label": "Premium / Escalate",
|
||||
"agents": ["ezra"],
|
||||
"use_for": "Complex multi-file work, docs, architecture. Escalate only."
|
||||
}
|
||||
],
|
||||
"notes": [
|
||||
"bilbobagginshire: on-request only, not delegated work",
|
||||
"claw-code: infrastructure bridge, not a routing endpoint",
|
||||
"substratum: inactive until model assigned (Epic #196)",
|
||||
"allegro-primus: retired, do not route"
|
||||
]
|
||||
},
|
||||
|
||||
"task_type_map": {
|
||||
"issue-triage": ["fenrir", "allegro"],
|
||||
"code-generation": ["carnice", "ezra"],
|
||||
"code-review": ["bezalel"],
|
||||
"small-edit": ["kimi"],
|
||||
"debugging": ["bezalel", "carnice"],
|
||||
"documentation": ["ezra"],
|
||||
"architecture": ["ezra"],
|
||||
"ci-fixes": ["bezalel"],
|
||||
"pr-review": ["bezalel", "fenrir"],
|
||||
"triage-routing": ["allegro"],
|
||||
"devops": ["substratum"],
|
||||
"background-monitoring": ["bilbobagginshire"]
|
||||
},
|
||||
|
||||
"gaps": [
|
||||
{
|
||||
"agent": "substratum",
|
||||
"gap": "No model assigned. Cannot route any tasks.",
|
||||
"action": "Assign model. Track in Epic #196 (Local Model Fleet)."
|
||||
},
|
||||
{
|
||||
"agent": "kimi",
|
||||
"gap": "Gitea agent description is empty. Profile lacks context for automated routing decisions.",
|
||||
"action": "Enrich kimi's Gitea profile description."
|
||||
},
|
||||
{
|
||||
"agent": "claw-code",
|
||||
"gap": "Listed as agent in routing table but is a protocol bridge, not an endpoint.",
|
||||
"action": "Remove from routing cascade. Keep as infrastructure reference only."
|
||||
},
|
||||
{
|
||||
"agent": "fleet",
|
||||
"gap": "No model scoring exists. Current routing is based on self-description and repo ownership, not measured output quality.",
|
||||
"action": "Run wolf evaluation on active agents (#195) to replace vibes-based routing with data."
|
||||
}
|
||||
],
|
||||
|
||||
"next_actions": [
|
||||
"Assign model to substratum — Epic #196",
|
||||
"Run wolf evaluation on active agents — Issue #195",
|
||||
"Remove claw-code from routing cascade — it is infrastructure, not a destination",
|
||||
"Enrich kimi's Gitea profile description",
|
||||
"Wire fleet-routing.json into workforce-manager.py — Epic #204"
|
||||
]
|
||||
}
|
||||
121
fleet/fleet.sh
121
fleet/fleet.sh
@@ -1,121 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# fleet.sh — Cross-VPS fleet management
|
||||
# Manages both Allegro (167.99.126.228) and Bezalel (159.203.146.185)
|
||||
# Usage: fleet.sh <command> [options]
|
||||
#
|
||||
# Commands:
|
||||
# health — Run health checks on all VPSes
|
||||
# restart <svc> — Restart a service on all VPSes
|
||||
# status — Show fleet status summary
|
||||
# ssh <host> — SSH into a specific host (allegro|bezalel)
|
||||
# run <command> — Run a command on all VPSes
|
||||
# deploy — Deploy latest config to all VPSes
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ALLEGRO="167.99.126.228"
|
||||
BEZALEL="159.203.146.185"
|
||||
EZRA="143.198.27.163"
|
||||
USER="root"
|
||||
SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=10"
|
||||
|
||||
hosts="$ALLEGRO $BEZALEL $EZRA"
|
||||
host_names="allegro bezalel ezra"
|
||||
|
||||
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] FLEET: $*"; }
|
||||
|
||||
remote() {
|
||||
local host=$1
|
||||
shift
|
||||
ssh $SSH_OPTS "$USER@$host" "$@"
|
||||
}
|
||||
|
||||
cmd_health() {
|
||||
log "Running fleet health check..."
|
||||
paste <(echo "$host_names" | tr ' ' '\n') <(echo "$hosts" | tr ' ' '\n') | while read name host; do
|
||||
echo ""
|
||||
echo "=== $name ($host) ==="
|
||||
if remote "$host" "echo 'SSH: OK'; uptime; free -m | head -2; df -h / | tail -1; systemctl list-units --state=failed --no-pager | head -10" 2>&1; then
|
||||
echo "---"
|
||||
else
|
||||
echo "SSH: FAILED — host unreachable"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
cmd_status() {
|
||||
log "Fleet status summary..."
|
||||
paste <(echo "$host_names" | tr ' ' '\n') <(echo "$hosts" | tr ' ' '\n') | while read name host; do
|
||||
printf "%-12s " "$name"
|
||||
if remote "$host" "echo -n 'UP' 2>/dev/null" 2>/dev/null; then
|
||||
uptime_str=$(remote "$host" "uptime -p 2>/dev/null || uptime" 2>/dev/null || echo "unknown")
|
||||
echo " $uptime_str"
|
||||
else
|
||||
echo " UNREACHABLE"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
cmd_restart() {
|
||||
local svc=${1:-}
|
||||
if [ -z "$svc" ]; then
|
||||
echo "Usage: fleet.sh restart <service>"
|
||||
echo "Common: hermes-agent evennia nginx docker"
|
||||
return 1
|
||||
fi
|
||||
log "Restarting '$svc' on all hosts..."
|
||||
paste <(echo "$host_names" | tr ' ' '\n') <(echo "$hosts" | tr ' ' '\n') | while read name host; do
|
||||
printf "%-12s " "$name"
|
||||
if remote "$host" "systemctl restart $svc 2>&1 && echo 'restarted' || echo 'FAILED'" 2>/dev/null; then
|
||||
echo ""
|
||||
else
|
||||
echo "UNREACHABLE"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
cmd_run() {
|
||||
local cmd="${1:-}"
|
||||
if [ -z "$cmd" ]; then
|
||||
echo "Usage: fleet.sh run '<command>'"
|
||||
return 1
|
||||
fi
|
||||
log "Running '$cmd' on all hosts..."
|
||||
paste <(echo "$host_names" | tr ' ' '\n') <(echo "$hosts" | tr ' ' '\n') | while read name host; do
|
||||
echo "=== $name ($host) ==="
|
||||
remote "$host" "$cmd" 2>&1 || echo "(failed)"
|
||||
echo ""
|
||||
done
|
||||
}
|
||||
|
||||
cmd_deploy() {
|
||||
log "Deploying config to all hosts..."
|
||||
# Push timmy-config updates to each host
|
||||
for pair in "allegro:$ALLEGRO" "bezalel:$BEZALEL"; do
|
||||
name="${pair%%:*}"
|
||||
host="${pair##*:}"
|
||||
echo ""
|
||||
echo "=== $name ==="
|
||||
remote "$host" "cd /root && ./update-config.sh 2>/dev/null || echo 'No update script found'; systemctl restart hermes-agent 2>/dev/null && echo 'hermes-agent restarted' || echo 'hermes-agent not found'" 2>&1 || echo "(unreachable)"
|
||||
done
|
||||
}
|
||||
|
||||
# Main dispatch
|
||||
case "${1:-help}" in
|
||||
health) cmd_health ;;
|
||||
status) cmd_status ;;
|
||||
restart) cmd_restart "${2:-}" ;;
|
||||
run) cmd_run "${2:-}" ;;
|
||||
deploy) cmd_deploy ;;
|
||||
help|*)
|
||||
echo "Usage: fleet.sh <command> [options]"
|
||||
echo ""
|
||||
echo "Commands:"
|
||||
echo " health — Run health checks on all VPSes"
|
||||
echo " status — Show fleet status summary"
|
||||
echo " restart <svc> — Restart a service on all VPSes"
|
||||
echo " run '<cmd>' — Run a command on all VPSes"
|
||||
echo " deploy — Deploy config to all VPSes"
|
||||
echo " ssh <host> — SSH into host (allegro|bezalel|ezra)"
|
||||
;;
|
||||
esac
|
||||
@@ -1,75 +0,0 @@
|
||||
const GiteaApiUrl = 'https://forge.alexanderwhitestone.com/api/v1';
|
||||
const token = process.env.GITEA_TOKEN; // Should be stored securely in environment variables
|
||||
const repos = ['hermes-agent', 'the-nexus', 'timmy-home', 'timmy-config'];
|
||||
|
||||
const branchProtectionSettings = {
|
||||
enablePush: false,
|
||||
enableMerge: true,
|
||||
requiredApprovals: 1,
|
||||
dismissStaleApprovals: true,
|
||||
requiredStatusChecks: true,
|
||||
blockForcePush: true,
|
||||
blockDelete: true
|
||||
// Special handling for the-nexus (CI disabled)
|
||||
};
|
||||
|
||||
async function applyBranchProtection(repo) {
|
||||
try {
|
||||
const response = await fetch(`${giteaApiUrl}/repos/Timmy_Foundation/${repo}/branches/main/protection`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `token ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...branchProtectionSettings,
|
||||
// Special handling for the-nexus (CI disabled)
|
||||
requiredStatusChecks: repo === 'the-nexus' ? false : true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to apply branch protection to ${repo}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
console.log(`✅ Branch protection applied to ${repo}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error applying branch protection to ${repo}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function applyBranchProtection(repo) {
|
||||
try {
|
||||
const response = await fetch(`${giteaApiUrl}/repos/Timmy_Foundation/${repo}/branches/main/protection`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `token ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...branchProtectionSettings,
|
||||
requiredApprovals: repo === 'hermes-agent' ? 2 : 1,
|
||||
requiredStatusChecks: repo === 'the-nexus' ? false : true
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to apply branch protection to ${repo}: ${await response.text()}`);
|
||||
}
|
||||
|
||||
console.log(`✅ Branch protection applied to ${repo}`);
|
||||
} catch (error) {
|
||||
console.error(`❌ Error applying branch protection to ${repo}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function setupAllBranchProtections() {
|
||||
console.log('🚀 Applying branch protections to all repositories...');
|
||||
for (const repo of repos) {
|
||||
await applyBranchProtection(repo);
|
||||
}
|
||||
console.log('✅ All branch protections applied successfully');
|
||||
}
|
||||
|
||||
// Run the setup
|
||||
setupAllBranchProtections();
|
||||
@@ -1,44 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Apply branch protections to all repositories
|
||||
# Requires GITEA_TOKEN env var
|
||||
|
||||
REPOS=("hermes-agent" "the-nexus" "timmy-home" "timmy-config")
|
||||
|
||||
for repo in "${REPOS[@]}"
|
||||
do
|
||||
curl -X POST "https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/$repo/branches/main/protection" \
|
||||
-H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"required_reviews": 1,
|
||||
"dismiss_stale_reviews": true,
|
||||
"block_force_push": true,
|
||||
"block_deletions": true
|
||||
}'
|
||||
done
|
||||
#!/bin/bash
|
||||
|
||||
# Gitea API credentials
|
||||
GITEA_TOKEN="your-personal-access-token"
|
||||
GITEA_API="https://forge.alexanderwhitestone.com/api/v1"
|
||||
|
||||
# Repos to protect
|
||||
REPOS=("hermes-agent" "the-nexus" "timmy-home" "timmy-config")
|
||||
|
||||
for REPO in "${REPO[@]}"; do
|
||||
echo "Configuring branch protection for $REPO..."
|
||||
|
||||
curl -X POST -H "Authorization: token $GITEA_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"name": "main",
|
||||
"require_pull_request": true,
|
||||
"required_approvals": 1,
|
||||
"dismiss_stale_approvals": true,
|
||||
"required_status_checks": '"$(test "$REPO" = "hermes-agent" && echo "true" || echo "false")"',
|
||||
"block_force_push": true,
|
||||
"block_delete": true
|
||||
}' \
|
||||
"$GITEA_API/repos/Timmy_Foundation/$REPO/branch_protection"
|
||||
done
|
||||
@@ -1,36 +0,0 @@
|
||||
import os
|
||||
import requests
|
||||
from datetime import datetime
|
||||
|
||||
GITEA_API = os.getenv('Gitea_api_url', 'https://forge.alexanderwhitestone.com/api/v1')
|
||||
Gitea_token = os.getenv('GITEA_TOKEN')
|
||||
|
||||
headers = {
|
||||
'Authorization': f'token {gitea_token}',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
|
||||
def apply_branch_protection(owner, repo, branch='main'):
|
||||
payload = {
|
||||
"protected": True,
|
||||
"merge_method": "merge",
|
||||
"push": False,
|
||||
"pull_request": True,
|
||||
"required_signoff": False,
|
||||
"required_reviews": 1,
|
||||
"required_status_checks": True,
|
||||
"restrict_owners": True,
|
||||
"delete": False,
|
||||
"force_push": False
|
||||
}
|
||||
|
||||
url = f"{GITEA_API}/repos/{owner}/{repo}/branches/{branch}/protection"
|
||||
r = requests.post(url, json=payload, headers=headers)
|
||||
return r.status_code, r.json()
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Apply to all repos
|
||||
for repo in ['hermes-agent', 'the-nexus', 'timmy-home', 'timmy-config']:
|
||||
print(f"Configuring {repo}...")
|
||||
status, resp = apply_branch_protection('Timmy_Foundation', repo)
|
||||
print(f"Status: {status} {resp}")
|
||||
489
help.html
489
help.html
@@ -1,489 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<!--
|
||||
THE NEXUS — Help Page
|
||||
Refs: #833 (Missing /help page)
|
||||
Design: dark space / holographic — matches Nexus design system
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Help — The Nexus</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@300;400;500;600&family=Orbitron:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<style>
|
||||
:root {
|
||||
--color-bg: #050510;
|
||||
--color-surface: rgba(10, 15, 40, 0.85);
|
||||
--color-border: rgba(74, 240, 192, 0.2);
|
||||
--color-border-bright: rgba(74, 240, 192, 0.5);
|
||||
--color-text: #e0f0ff;
|
||||
--color-text-muted: #8a9ab8;
|
||||
--color-primary: #4af0c0;
|
||||
--color-primary-dim: rgba(74, 240, 192, 0.12);
|
||||
--color-secondary: #7b5cff;
|
||||
--color-danger: #ff4466;
|
||||
--color-warning: #ffaa22;
|
||||
--font-display: 'Orbitron', sans-serif;
|
||||
--font-body: 'JetBrains Mono', monospace;
|
||||
--panel-blur: 16px;
|
||||
--panel-radius: 8px;
|
||||
--transition: 200ms cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
background: var(--color-bg);
|
||||
font-family: var(--font-body);
|
||||
color: var(--color-text);
|
||||
min-height: 100vh;
|
||||
padding: 32px 16px 64px;
|
||||
}
|
||||
|
||||
/* === STARFIELD BG === */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 20%, rgba(74,240,192,0.03) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 80% 80%, rgba(123,92,255,0.04) 0%, transparent 50%);
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.page-wrap {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* === HEADER === */
|
||||
.page-header {
|
||||
margin-bottom: 32px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-text-muted);
|
||||
text-decoration: none;
|
||||
margin-bottom: 20px;
|
||||
transition: color var(--transition);
|
||||
}
|
||||
|
||||
.back-link:hover { color: var(--color-primary); }
|
||||
|
||||
.page-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--color-text);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.page-title span { color: var(--color-primary); }
|
||||
|
||||
.page-subtitle {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* === SECTIONS === */
|
||||
.help-section {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--panel-radius);
|
||||
overflow: hidden;
|
||||
margin-bottom: 20px;
|
||||
backdrop-filter: blur(var(--panel-blur));
|
||||
}
|
||||
|
||||
.section-header {
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
background: linear-gradient(90deg, rgba(74,240,192,0.04) 0%, transparent 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.section-icon {
|
||||
font-size: 14px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.section-body {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
/* === KEY BINDING TABLE === */
|
||||
.key-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.key-table tr + tr td {
|
||||
border-top: 1px solid rgba(74,240,192,0.07);
|
||||
}
|
||||
|
||||
.key-table td {
|
||||
padding: 8px 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.key-table td:first-child {
|
||||
width: 140px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.key-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
kbd {
|
||||
display: inline-block;
|
||||
font-family: var(--font-body);
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
background: rgba(74,240,192,0.08);
|
||||
border: 1px solid rgba(74,240,192,0.3);
|
||||
border-bottom-width: 2px;
|
||||
border-radius: 4px;
|
||||
padding: 2px 7px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.key-desc {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* === COMMAND LIST === */
|
||||
.cmd-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.cmd-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.cmd-name {
|
||||
min-width: 160px;
|
||||
font-size: 12px;
|
||||
color: var(--color-primary);
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.cmd-desc {
|
||||
font-size: 12px;
|
||||
color: var(--color-text-muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* === PORTAL LIST === */
|
||||
.portal-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.portal-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
transition: border-color var(--transition), background var(--transition);
|
||||
}
|
||||
|
||||
.portal-item:hover {
|
||||
border-color: rgba(74,240,192,0.35);
|
||||
background: rgba(74,240,192,0.02);
|
||||
}
|
||||
|
||||
.portal-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.dot-online { background: var(--color-primary); box-shadow: 0 0 6px var(--color-primary); }
|
||||
.dot-standby { background: var(--color-warning); box-shadow: 0 0 6px var(--color-warning); }
|
||||
.dot-offline { background: var(--color-text-muted); }
|
||||
|
||||
.portal-name {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.portal-desc {
|
||||
color: var(--color-text-muted);
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* === INFO BLOCK === */
|
||||
.info-block {
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.info-block p + p {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.info-block a {
|
||||
color: var(--color-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.info-block a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.highlight {
|
||||
color: var(--color-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* === FOOTER === */
|
||||
.page-footer {
|
||||
margin-top: 32px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid var(--color-border);
|
||||
font-size: 11px;
|
||||
color: var(--color-text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: gap;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.footer-brand {
|
||||
font-family: var(--font-display);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--color-primary);
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page-wrap">
|
||||
|
||||
<!-- Header -->
|
||||
<header class="page-header">
|
||||
<a href="/" class="back-link">← Back to The Nexus</a>
|
||||
<h1 class="page-title">THE <span>NEXUS</span> — Help</h1>
|
||||
<p class="page-subtitle">Navigation guide, controls, and system reference for Timmy's sovereign home-world.</p>
|
||||
</header>
|
||||
|
||||
<!-- Navigation Controls -->
|
||||
<section class="help-section">
|
||||
<div class="section-header">
|
||||
<span class="section-icon">◈</span>
|
||||
<span class="section-title">Navigation Controls</span>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<table class="key-table">
|
||||
<tr>
|
||||
<td><div class="key-group"><kbd>W</kbd><kbd>A</kbd><kbd>S</kbd><kbd>D</kbd></div></td>
|
||||
<td class="key-desc">Move forward / left / backward / right</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="key-group"><kbd>Mouse</kbd></div></td>
|
||||
<td class="key-desc">Look around — click the canvas to capture the pointer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="key-group"><kbd>V</kbd></div></td>
|
||||
<td class="key-desc">Toggle navigation mode: Walk → Fly → Orbit</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="key-group"><kbd>F</kbd></div></td>
|
||||
<td class="key-desc">Enter nearby portal (when portal hint is visible)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="key-group"><kbd>E</kbd></div></td>
|
||||
<td class="key-desc">Read nearby vision point (when vision hint is visible)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="key-group"><kbd>Enter</kbd></div></td>
|
||||
<td class="key-desc">Focus / unfocus chat input</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><div class="key-group"><kbd>Esc</kbd></div></td>
|
||||
<td class="key-desc">Release pointer lock / close overlays</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Timmy Chat Commands -->
|
||||
<section class="help-section">
|
||||
<div class="section-header">
|
||||
<span class="section-icon">⬡</span>
|
||||
<span class="section-title">Timmy Chat Commands</span>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="cmd-list">
|
||||
<div class="cmd-item">
|
||||
<span class="cmd-name">System Status</span>
|
||||
<span class="cmd-desc">Quick action — asks Timmy for a live system health summary.</span>
|
||||
</div>
|
||||
<div class="cmd-item">
|
||||
<span class="cmd-name">Agent Check</span>
|
||||
<span class="cmd-desc">Quick action — lists all active agents and their current state.</span>
|
||||
</div>
|
||||
<div class="cmd-item">
|
||||
<span class="cmd-name">Portal Atlas</span>
|
||||
<span class="cmd-desc">Quick action — opens the full portal map overlay.</span>
|
||||
</div>
|
||||
<div class="cmd-item">
|
||||
<span class="cmd-name">Help</span>
|
||||
<span class="cmd-desc">Quick action — requests navigation assistance from Timmy.</span>
|
||||
</div>
|
||||
<div class="cmd-item">
|
||||
<span class="cmd-name">Free-form text</span>
|
||||
<span class="cmd-desc">Type anything in the chat bar and press Enter or → to send. Timmy processes all natural-language input.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Portal Atlas -->
|
||||
<section class="help-section">
|
||||
<div class="section-header">
|
||||
<span class="section-icon">🌐</span>
|
||||
<span class="section-title">Portal Atlas</span>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="info-block">
|
||||
<p>Portals are gateways to external systems and game-worlds. Walk up to a glowing portal in the Nexus and press <span class="highlight"><kbd>F</kbd></span> to activate it, or open the <span class="highlight">Portal Atlas</span> (top-right button) for a full map view.</p>
|
||||
<p>Portal status indicators:</p>
|
||||
</div>
|
||||
<div class="portal-list" style="margin-top:14px;">
|
||||
<div class="portal-item">
|
||||
<span class="portal-dot dot-online"></span>
|
||||
<span class="portal-name">ONLINE</span>
|
||||
<span class="portal-desc">Portal is live and will redirect immediately on activation.</span>
|
||||
</div>
|
||||
<div class="portal-item">
|
||||
<span class="portal-dot dot-standby"></span>
|
||||
<span class="portal-name">STANDBY</span>
|
||||
<span class="portal-desc">Portal is reachable but destination system may be idle.</span>
|
||||
</div>
|
||||
<div class="portal-item">
|
||||
<span class="portal-dot dot-offline"></span>
|
||||
<span class="portal-name">OFFLINE / UNLINKED</span>
|
||||
<span class="portal-desc">Destination not yet connected. Activation shows an error card.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- HUD Panels -->
|
||||
<section class="help-section">
|
||||
<div class="section-header">
|
||||
<span class="section-icon">▦</span>
|
||||
<span class="section-title">HUD Panels</span>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="cmd-list">
|
||||
<div class="cmd-item">
|
||||
<span class="cmd-name">Symbolic Engine</span>
|
||||
<span class="cmd-desc">Live feed from Timmy's rule-based reasoning layer.</span>
|
||||
</div>
|
||||
<div class="cmd-item">
|
||||
<span class="cmd-name">Blackboard</span>
|
||||
<span class="cmd-desc">Shared working memory used across all cognitive subsystems.</span>
|
||||
</div>
|
||||
<div class="cmd-item">
|
||||
<span class="cmd-name">Symbolic Planner</span>
|
||||
<span class="cmd-desc">Goal decomposition and task sequencing output.</span>
|
||||
</div>
|
||||
<div class="cmd-item">
|
||||
<span class="cmd-name">Case-Based Reasoner</span>
|
||||
<span class="cmd-desc">Analogical reasoning — matches current situation to past cases.</span>
|
||||
</div>
|
||||
<div class="cmd-item">
|
||||
<span class="cmd-name">Neuro-Symbolic Bridge</span>
|
||||
<span class="cmd-desc">Translation layer between neural inference and symbolic logic.</span>
|
||||
</div>
|
||||
<div class="cmd-item">
|
||||
<span class="cmd-name">Meta-Reasoning</span>
|
||||
<span class="cmd-desc">Timmy reflecting on its own thought process and confidence.</span>
|
||||
</div>
|
||||
<div class="cmd-item">
|
||||
<span class="cmd-name">Sovereign Health</span>
|
||||
<span class="cmd-desc">Core vitals: memory usage, heartbeat interval, alert flags.</span>
|
||||
</div>
|
||||
<div class="cmd-item">
|
||||
<span class="cmd-name">Adaptive Calibrator</span>
|
||||
<span class="cmd-desc">Live tuning of response thresholds and behavior weights.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- System Info -->
|
||||
<section class="help-section">
|
||||
<div class="section-header">
|
||||
<span class="section-icon">◉</span>
|
||||
<span class="section-title">System Information</span>
|
||||
</div>
|
||||
<div class="section-body">
|
||||
<div class="info-block">
|
||||
<p>The Nexus is Timmy's <span class="highlight">canonical sovereign home-world</span> — a local-first 3D space that serves as both a training ground and a live visualization surface for the Timmy AI system.</p>
|
||||
<p>The WebSocket gateway (<code>server.py</code>) runs on port <span class="highlight">8765</span> and bridges Timmy's cognition layer, game-world connectors, and the browser frontend. The <span class="highlight">HERMES</span> indicator in the HUD shows live connectivity status.</p>
|
||||
<p>Source code and issue tracker: <a href="https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus" target="_blank" rel="noopener noreferrer">Timmy_Foundation/the-nexus</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="page-footer">
|
||||
<span class="footer-brand">THE NEXUS</span>
|
||||
<span>Questions? Speak to Timmy in the chat bar on the main world.</span>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
10
hermes-agent/.github/CODEOWNERS
vendored
10
hermes-agent/.github/CODEOWNERS
vendored
@@ -1,10 +0,0 @@
|
||||
# CODEOWNERS for hermes-agent
|
||||
* @perplexity
|
||||
@Timmy
|
||||
# CODEOWNERS for the-nexus
|
||||
|
||||
* @perplexity
|
||||
@Rockachopa
|
||||
# CODEOWNERS for timmy-config
|
||||
|
||||
* @perplexity
|
||||
@@ -1,3 +0,0 @@
|
||||
@Timmy
|
||||
* @perplexity
|
||||
**/src @Timmy
|
||||
@@ -1,18 +0,0 @@
|
||||
# Contribution Policy for hermes-agent
|
||||
|
||||
## Branch Protection Rules
|
||||
All changes to the `main` branch require:
|
||||
- Pull Request with at least 1 approval
|
||||
- CI checks passing
|
||||
- No direct commits or force pushes
|
||||
- No deletion of the main branch
|
||||
|
||||
## Review Requirements
|
||||
- All PRs must be reviewed by @perplexity
|
||||
- Additional review required from @Timmy
|
||||
|
||||
## Stale PR Policy
|
||||
- Stale approvals are dismissed on new commits
|
||||
- Abandoned PRs will be closed after 7 days of inactivity
|
||||
|
||||
For urgent fixes, create a hotfix branch and follow the same review process.
|
||||
140
index.html
140
index.html
@@ -246,135 +246,6 @@
|
||||
<a href="https://www.perplexity.ai/computer" target="_blank" rel="noopener noreferrer">
|
||||
Created with Perplexity Computer
|
||||
</a>
|
||||
<a href="POLICY.md" target="_blank" rel="noopener noreferrer">
|
||||
View Contribution Policy
|
||||
</a>
|
||||
<div class="branch-policy" style="margin-top: 10px; font-size: 12px; color: #aaa;">
|
||||
<strong>BRANCH PROTECTION POLICY</strong><br>
|
||||
<ul style="margin:0; padding-left:15px;">
|
||||
<li>• Require PR for merge ✅</li>
|
||||
<li>• Require 1 approval ✅</li>
|
||||
<li>• Dismiss stale approvals ✅</li>
|
||||
<li>• Require CI ✅ (where available)</li>
|
||||
<li>• Block force push ✅</li>
|
||||
<li>• Block branch deletion ✅</li>
|
||||
</ul>
|
||||
<div style="margin-top: 8px;">
|
||||
<strong>DEFAULT REVIEWERS</strong><br>
|
||||
<span style="color:#4af0c0;">@perplexity</span> (QA gate on all repos) |
|
||||
<span style="color:#7b5cff;">@Timmy</span> (owner gate on hermes-agent)
|
||||
</div>
|
||||
<div style="margin-top: 10px;">
|
||||
<strong>IMPLEMENTATION STATUS</strong><br>
|
||||
<ul style="margin:0; padding-left:15px;">
|
||||
<li>• hermes-agent: Require PR + 1 approval + CI ✅</li>
|
||||
<li>• the-nexus: Require PR + 1 approval ⚠️ (CI disabled)</li>
|
||||
<li>• timmy-home: Require PR + 1 approval ✅</li>
|
||||
<li>• timmy-config: Require PR + 1 approval ✅</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="branch-policy" style="margin-top: 10px; font-size: 12px; color: #aaa;">
|
||||
<strong>BRANCH PROTECTION POLICY</strong><br>
|
||||
<ul style="margin:0; padding-left:15px;">
|
||||
<li>• Require PR for merge ✅</li>
|
||||
<li>• Require 1 approval ✅</li>
|
||||
<li>• Dismiss stale approvals ✅</li>
|
||||
<li>• Require CI ✅ (where available)</li>
|
||||
<li>• Block force push ✅</li>
|
||||
<li>• Block branch deletion ✅</li>
|
||||
<li>• Weekly audit for unreviewed merges ✅</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="mem-palace-container" class="mem-palace-ui">
|
||||
<div class="mem-palace-header">
|
||||
<span id="mem-palace-status">MEMPALACE</span>
|
||||
<button onclick="mineMemPalaceContent()" class="mem-palace-btn">Mine Chat</button>
|
||||
</div>
|
||||
<div class="mem-palace-stats">
|
||||
<div>Compression: <span id="compression-ratio">--</span>x</div>
|
||||
<div>Docs mined: <span id="docs-mined">0</span></div>
|
||||
<div>AAAK size: <span id="aaak-size">0B</span></div>
|
||||
</div>
|
||||
<div class="mem-palace-logs" id="mem-palace-logs"></div>
|
||||
</div>
|
||||
<div class="default-reviewers" style="margin-top: 8px; font-size: 12px; color: #aaa;">
|
||||
<strong>DEFAULT REVIEWERS</strong><br>
|
||||
<ul style="margin:0; padding-left:15px;">
|
||||
<li>• <span style="color:#4af0c0;">@perplexity</span> (QA gate on all repos)</li>
|
||||
<li>• <span style="color:#7b5cff;">@Timmy</span> (owner gate on hermes-agent)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="implementation-status" style="margin-top: 10px; font-size: 12px; color: #aaa;">
|
||||
<strong>IMPLEMENTATION STATUS</strong><br>
|
||||
<div style="margin-top: 5px; display: flex; flex-direction: column; gap: 2px;">
|
||||
<div>• <span style="color:#4af0c0;">hermes-agent</span>: Require PR + 1 approval + CI ✅</div>
|
||||
<div>• <span style="color:#7b5cff;">the-nexus</span>: Require PR + 1 approval ⚠️ (CI disabled)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mem-palace-status" style="position:fixed; right:24px; top:64px; background:rgba(74,240,192,0.1); color:#4af0c0; padding:6px 12px; border-radius:4px; font-family:'Orbitron', sans-serif; font-size:10px; letter-spacing:0.1em;">
|
||||
MEMPALACE INIT
|
||||
</div>
|
||||
<div>• <span style="color:#ffd700;">timmy-home</span>: Require PR + 1 approval ✅</div>
|
||||
<div>• <span style="color:#ab8d00;">timmy-config</span>: Require PR + 1 approval ✅</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="mem-palace-container" class="mem-palace-ui">
|
||||
<div class="mem-palace-header">MemPalace <span id="mem-palace-status">Initializing...</span></div>
|
||||
<div class="mem-palace-stats">
|
||||
<div>Compression: <span id="compression-ratio">--</span>x</div>
|
||||
<div>Docs mined: <span id="docs-mined">0</span></div>
|
||||
<div>AAAK size: <span id="aaak-size">0B</span></div>
|
||||
</div>
|
||||
<div class="mem-palace-actions">
|
||||
<button id="mine-now-btn" class="mem-palace-btn" onclick="mineChatToMemPalace()">Mine Chat</button>
|
||||
<button class="mem-palace-btn" onclick="searchMemPalace()">Search</button>
|
||||
</div>
|
||||
<div id="mem-palace-logs" class="mem-palace-logs"></div>
|
||||
</div>
|
||||
<div id="mem-palace-controls" style="position:fixed; right:24px; top:54px; background:rgba(74,240,192,0.05); padding:4px 8px; font-family:'JetBrains Mono',monospace; font-size:11px; border-left:2px solid #4af0c0;">
|
||||
<button onclick="mineMemPalace()">Mine Chat</button>
|
||||
<button onclick="searchMemPalace()">Search</button>
|
||||
</div>
|
||||
<div id="mempalace-results" style="position:fixed; right:24px; top:84px; max-height:200px; overflow-y:auto; background:rgba(0,0,0,0.3); padding:8px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#e0f0ff; border-left:2px solid #4af0c0;"></div>
|
||||
<div id="mem-palace-controls" style="position:fixed; right:24px; top:54px; background:rgba(74,240,192,0.05); padding:4px 8px; font-family:'JetBrains Mono',monospace; font-size:10px; border-left:2px solid #4af0c0;">
|
||||
<button class="mem-palace-mining-btn" onclick="mineChatToMemPalace()">Mine Chat</button>
|
||||
<button onclick="searchMemPalace()">Search</button>
|
||||
</div>
|
||||
<div id="mempalace-results" style="position:fixed; right:24px; top:84px; max-height:200px; overflow-y:auto; background:rgba(0,0,0,0.3); padding:8px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#e0f0ff; border-left:2px solid #4af0c0;"></div>
|
||||
>>>>>>> replace
|
||||
```
|
||||
|
||||
index.html
|
||||
```html
|
||||
<<<<<<< search
|
||||
<div class="branch-policy" style="margin-top: 10px; font-size: 12px; color: #aaa;">
|
||||
<strong>BRANCH PROTECTION POLICY</strong><br>
|
||||
<ul style="margin:0; padding-left:15px;">
|
||||
<li>• Require PR for merge ✅</li>
|
||||
<li>• Require 1 approval ✅</li>
|
||||
<li>• Dismiss stale approvals ✅</li>
|
||||
<li>• Require CI ✅ (where available)</li>
|
||||
<li>• Block force push ✅</li>
|
||||
<li>• Block branch deletion ✅</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="default-reviewers" style="margin-top: 8px;">
|
||||
<strong>DEFAULT REVIEWERS</strong><br>
|
||||
<ul style="margin:0; padding-left:15px;">
|
||||
<li>• <span style="color:#4af0c0;">@perplexity</span> (QA gate on all repos)</li>
|
||||
<li>• <span style="color:#7b5cff;">@Timmy</span> (owner gate on hermes-agent)</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="implementation-status" style="margin-top: 10px;">
|
||||
<strong>IMPLEMENTATION STATUS</strong><br>
|
||||
<div style="margin-top: 5px; display: flex; flex-direction: column; gap: 2px;">
|
||||
<div>• <span style="color:#4af0c0;">hermes-agent</span>: Require PR + 1 approval + CI ✅</div>
|
||||
<div>• <span style="color:#7b5cff;">the-nexus</span>: Require PR + 1 approval ⚠<> (CI disabled)</div>
|
||||
<div>• <span style="color:#ffd700;">timmy-home</span>: Require PR + 1 approval ✅</div>
|
||||
<div>• <span style="color:#ab8d00;">timmy-config</span>: Require PR + 1 approval ✅</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<script type="module" src="./app.js"></script>
|
||||
@@ -410,17 +281,6 @@ index.html
|
||||
if (!sha) return;
|
||||
if (knownSha === null) { knownSha = sha; return; }
|
||||
if (sha !== knownSha) {
|
||||
// Check branch protection rules
|
||||
const branchRules = await fetch(`${GITEA}/repos/${REPO}/branches/${BRANCH}/protection`);
|
||||
if (!branchRules.ok) {
|
||||
console.error('Branch protection rules not enforced');
|
||||
return;
|
||||
}
|
||||
const rules = await branchRules.json();
|
||||
if (!rules.require_pr && !rules.require_approvals) {
|
||||
console.error('Branch protection rules not met');
|
||||
return;
|
||||
}
|
||||
knownSha = sha;
|
||||
const banner = document.getElementById('live-refresh-banner');
|
||||
const countdown = document.getElementById('lr-countdown');
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
# Lazarus Pit Registry — Single Source of Truth for Fleet Health and Resurrection
|
||||
# Version: 1.0.0
|
||||
# Owner: Bezalel (deployment), Ezra (compilation), Allegro (validation)
|
||||
|
||||
meta:
|
||||
version: "1.0.0"
|
||||
updated_at: "2026-04-07T02:55:00Z"
|
||||
next_review: "2026-04-14T02:55:00Z"
|
||||
|
||||
fleet:
|
||||
bezalel:
|
||||
role: forge-and-testbed wizard
|
||||
host: 104.131.15.18
|
||||
vps_provider: digitalocean
|
||||
primary:
|
||||
provider: kimi-coding
|
||||
model: kimi-k2.5
|
||||
fallback_chain:
|
||||
- provider: kimi-coding
|
||||
model: kimi-k2.5
|
||||
timeout: 120
|
||||
- provider: anthropic
|
||||
model: claude-sonnet-4-20250514
|
||||
timeout: 120
|
||||
- provider: openrouter
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
timeout: 120
|
||||
- provider: big_brain
|
||||
model: gemma3:27b-instruct-q8_0
|
||||
timeout: 300
|
||||
health_endpoints:
|
||||
gateway: "http://127.0.0.1:8646"
|
||||
api_server: "http://127.0.0.1:8656"
|
||||
auto_restart: true
|
||||
|
||||
allegro:
|
||||
role: code-craft wizard
|
||||
host: UNKNOWN
|
||||
vps_provider: UNKNOWN
|
||||
primary:
|
||||
provider: kimi-coding
|
||||
model: kimi-k2.5
|
||||
fallback_chain:
|
||||
- provider: kimi-coding
|
||||
model: kimi-k2.5
|
||||
timeout: 120
|
||||
- provider: anthropic
|
||||
model: claude-sonnet-4-20250514
|
||||
timeout: 120
|
||||
- provider: openrouter
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
timeout: 120
|
||||
health_endpoints:
|
||||
gateway: "http://127.0.0.1:8645"
|
||||
auto_restart: true
|
||||
known_issues:
|
||||
- host_and_vps_unknown_to_fleet
|
||||
- config_needs_runtime_refresh
|
||||
|
||||
ezra:
|
||||
role: archivist-and-interpreter wizard
|
||||
host: UNKNOWN
|
||||
vps_provider: UNKNOWN
|
||||
primary:
|
||||
provider: anthropic
|
||||
model: claude-sonnet-4-20250514
|
||||
fallback_chain:
|
||||
- provider: anthropic
|
||||
model: claude-sonnet-4-20250514
|
||||
timeout: 120
|
||||
- provider: openrouter
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
timeout: 120
|
||||
auto_restart: true
|
||||
known_issues:
|
||||
- timeout_choking_on_long_operations
|
||||
|
||||
timmy:
|
||||
role: sovereign core
|
||||
host: UNKNOWN
|
||||
vps_provider: UNKNOWN
|
||||
primary:
|
||||
provider: anthropic
|
||||
model: claude-sonnet-4-20250514
|
||||
fallback_chain:
|
||||
- provider: anthropic
|
||||
model: claude-sonnet-4-20250514
|
||||
timeout: 120
|
||||
- provider: openrouter
|
||||
model: anthropic/claude-sonnet-4-20250514
|
||||
timeout: 120
|
||||
auto_restart: true
|
||||
|
||||
provider_health_matrix:
|
||||
kimi-coding:
|
||||
status: degraded
|
||||
note: "kimi-for-coding returns 403 access-terminated; use kimi-k2.5 model only"
|
||||
last_checked: "2026-04-07T02:55:00Z"
|
||||
rate_limited: false
|
||||
dead: false
|
||||
|
||||
anthropic:
|
||||
status: healthy
|
||||
last_checked: "2026-04-07T02:55:00Z"
|
||||
rate_limited: false
|
||||
dead: false
|
||||
|
||||
openrouter:
|
||||
status: healthy
|
||||
last_checked: "2026-04-07T02:55:00Z"
|
||||
rate_limited: false
|
||||
dead: false
|
||||
|
||||
big_brain:
|
||||
status: provisioning
|
||||
note: "RunPod L40S instance big-brain-bezalel deployed; Ollama endpoint propagating"
|
||||
last_checked: "2026-04-07T02:55:00Z"
|
||||
endpoint: "http://yxw29g3excyddq-64411cd0-11434.tcp.runpod.net:11434/v1"
|
||||
rate_limited: false
|
||||
dead: false
|
||||
|
||||
timeout_policies:
|
||||
gateway:
|
||||
inactivity_timeout_seconds: 600
|
||||
diagnostic_on_timeout: true
|
||||
cron:
|
||||
inactivity_timeout_seconds: 0 # unlimited while active
|
||||
agent:
|
||||
default_turn_timeout: 120
|
||||
long_operation_heartbeat: true
|
||||
|
||||
watchdog:
|
||||
enabled: true
|
||||
interval_seconds: 60
|
||||
actions:
|
||||
- ping_agent_gateways
|
||||
- probe_providers
|
||||
- parse_agent_logs
|
||||
- update_registry
|
||||
- auto_promote_fallbacks
|
||||
- auto_restart_dead_agents
|
||||
|
||||
resurrection_protocol:
|
||||
soft:
|
||||
- reload_config_from_registry
|
||||
- rewrite_fallback_providers
|
||||
- promote_first_healthy_fallback
|
||||
hard:
|
||||
- systemctl_restart_gateway
|
||||
- log_incident
|
||||
- notify_sovereign
|
||||
@@ -8,14 +8,9 @@
|
||||
"theme_color": "#4af0c0",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
"src": "/favicon.ico",
|
||||
"sizes": "64x64",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
44
mempalace.js
44
mempalace.js
@@ -1,44 +0,0 @@
|
||||
// MemPalace integration
|
||||
class MemPalace {
|
||||
constructor() {
|
||||
this.palacePath = '~/.mempalace/palace';
|
||||
this.wing = 'nexus_chat';
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
try {
|
||||
await this.setupWing();
|
||||
this.setupAutoMining();
|
||||
} catch (error) {
|
||||
console.error('MemPalace init failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async setupWing() {
|
||||
await window.electronAPI.execPython(`mempalace init ${this.palacePath}`);
|
||||
await window.electronAPI.execPython(`mempalace mine ~/chats --mode convos --wing ${this.wing}`);
|
||||
}
|
||||
|
||||
setupAutoMining() {
|
||||
setInterval(() => {
|
||||
window.electronAPI.execPython(`mempalace mine #chat-container --mode convos --wing ${this.wing}`);
|
||||
}, 30000); // Mine every 30 seconds
|
||||
}
|
||||
|
||||
async search(query) {
|
||||
const result = await window.electronAPI.execPython(`mempalace search "${query}" --wing ${this.wing}`);
|
||||
return result.stdout;
|
||||
}
|
||||
|
||||
updateStats() {
|
||||
const stats = window.electronAPI.execPython(`mempalace status --wing ${this.wing}`);
|
||||
document.getElementById('compression-ratio').textContent =
|
||||
`${stats.compression_ratio.toFixed(1)}x`;
|
||||
document.getElementById('docs-mined').textContent = stats.total_docs;
|
||||
document.getElementById('aaak-size').textContent = stats.aaak_size;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize MemPalace
|
||||
const mempalace = new MemPalace();
|
||||
@@ -1,5 +0,0 @@
|
||||
"""
|
||||
mempalace — Fleet memory tools for the MemPalace × Evennia integration.
|
||||
|
||||
Refs: #1075 (MemPalace × Evennia — Fleet Memory milestone)
|
||||
"""
|
||||
@@ -1,177 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
audit_privacy.py — Weekly privacy audit for the shared fleet palace.
|
||||
|
||||
Scans a palace directory (typically the shared Alpha fleet palace) and
|
||||
reports any files that violate the closet-only sync policy:
|
||||
|
||||
1. Raw drawer files (.drawer.json) — must never exist in fleet palace.
|
||||
2. Closet files containing full-text content (> threshold characters).
|
||||
3. Closet files exposing private source_file paths.
|
||||
|
||||
Exits 0 if clean, 1 if violations found.
|
||||
|
||||
Usage:
|
||||
python mempalace/audit_privacy.py [fleet_palace_dir]
|
||||
|
||||
Default: /var/lib/mempalace/fleet
|
||||
|
||||
Refs: #1083, #1075
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
# Closets should be compressed summaries, not full text.
|
||||
# Flag any text field exceeding this character count as suspicious.
|
||||
MAX_CLOSET_TEXT_CHARS = 2000
|
||||
|
||||
# Private path indicators — if a source_file contains any of these,
|
||||
# it is considered a private VPS path that should not be in the fleet palace.
|
||||
PRIVATE_PATH_PREFIXES = [
|
||||
"/root/",
|
||||
"/home/",
|
||||
"/Users/",
|
||||
"/var/home/",
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Violation:
|
||||
path: Path
|
||||
rule: str
|
||||
detail: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class AuditResult:
|
||||
scanned: int = 0
|
||||
violations: list[Violation] = field(default_factory=list)
|
||||
|
||||
@property
|
||||
def clean(self) -> bool:
|
||||
return len(self.violations) == 0
|
||||
|
||||
|
||||
def _is_private_path(path_str: str) -> bool:
|
||||
for prefix in PRIVATE_PATH_PREFIXES:
|
||||
if path_str.startswith(prefix):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def audit_file(path: Path) -> list[Violation]:
|
||||
violations: list[Violation] = []
|
||||
|
||||
# Rule 1: raw drawer files must not exist in fleet palace
|
||||
if path.name.endswith(".drawer.json"):
|
||||
violations.append(Violation(
|
||||
path=path,
|
||||
rule="RAW_DRAWER",
|
||||
detail="Raw drawer file present — only closets allowed in fleet palace.",
|
||||
))
|
||||
return violations # no further checks needed
|
||||
|
||||
if not path.name.endswith(".closet.json"):
|
||||
return violations # not a palace file, skip
|
||||
|
||||
try:
|
||||
data = json.loads(path.read_text())
|
||||
except (json.JSONDecodeError, OSError) as exc:
|
||||
violations.append(Violation(
|
||||
path=path,
|
||||
rule="PARSE_ERROR",
|
||||
detail=f"Could not parse file: {exc}",
|
||||
))
|
||||
return violations
|
||||
|
||||
drawers = data.get("drawers", []) if isinstance(data, dict) else []
|
||||
if not isinstance(drawers, list):
|
||||
drawers = []
|
||||
|
||||
for i, drawer in enumerate(drawers):
|
||||
if not isinstance(drawer, dict):
|
||||
continue
|
||||
|
||||
# Rule 2: closets must not contain full-text content
|
||||
text = drawer.get("text", "")
|
||||
if len(text) > MAX_CLOSET_TEXT_CHARS:
|
||||
violations.append(Violation(
|
||||
path=path,
|
||||
rule="FULL_TEXT_IN_CLOSET",
|
||||
detail=(
|
||||
f"Drawer [{i}] text is {len(text)} chars "
|
||||
f"(limit {MAX_CLOSET_TEXT_CHARS}). "
|
||||
"Closets must be compressed summaries, not raw content."
|
||||
),
|
||||
))
|
||||
|
||||
# Rule 3: private source_file paths must not appear in fleet data
|
||||
source_file = drawer.get("source_file", "")
|
||||
if source_file and _is_private_path(source_file):
|
||||
violations.append(Violation(
|
||||
path=path,
|
||||
rule="PRIVATE_SOURCE_PATH",
|
||||
detail=f"Drawer [{i}] exposes private source_file: {source_file!r}",
|
||||
))
|
||||
|
||||
return violations
|
||||
|
||||
|
||||
def audit_palace(palace_dir: Path) -> AuditResult:
|
||||
result = AuditResult()
|
||||
for f in sorted(palace_dir.rglob("*.json")):
|
||||
violations = audit_file(f)
|
||||
result.scanned += 1
|
||||
result.violations.extend(violations)
|
||||
return result
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Audit the fleet palace for privacy violations."
|
||||
)
|
||||
parser.add_argument(
|
||||
"palace_dir",
|
||||
nargs="?",
|
||||
default="/var/lib/mempalace/fleet",
|
||||
help="Path to the fleet palace directory (default: /var/lib/mempalace/fleet)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-text",
|
||||
type=int,
|
||||
default=MAX_CLOSET_TEXT_CHARS,
|
||||
metavar="N",
|
||||
help=f"Maximum closet text length (default: {MAX_CLOSET_TEXT_CHARS})",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
palace_dir = Path(args.palace_dir)
|
||||
if not palace_dir.exists():
|
||||
print(f"[audit_privacy] ERROR: palace directory not found: {palace_dir}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
print(f"[audit_privacy] Scanning: {palace_dir}")
|
||||
result = audit_palace(palace_dir)
|
||||
|
||||
if result.clean:
|
||||
print(f"[audit_privacy] OK — {result.scanned} file(s) scanned, no violations.")
|
||||
return 0
|
||||
|
||||
print(
|
||||
f"[audit_privacy] FAIL — {len(result.violations)} violation(s) in {result.scanned} file(s):",
|
||||
file=sys.stderr,
|
||||
)
|
||||
for v in result.violations:
|
||||
print(f" [{v.rule}] {v.path}", file=sys.stderr)
|
||||
print(f" {v.detail}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,104 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# export_closets.sh — Privacy-safe export of wizard closets for fleet sync.
|
||||
#
|
||||
# Exports ONLY closet (summary) files from a wizard's local MemPalace to
|
||||
# a bundle directory suitable for rsync to the shared Alpha fleet palace.
|
||||
#
|
||||
# POLICY: Raw drawers (full-text source content) NEVER leave the local VPS.
|
||||
# Only closets (compressed summaries) are exported.
|
||||
#
|
||||
# Usage:
|
||||
# ./mempalace/export_closets.sh [palace_dir] [export_dir]
|
||||
#
|
||||
# Defaults:
|
||||
# palace_dir — $MEMPALACE_DIR or /root/wizards/bezalel/.mempalace/palace
|
||||
# export_dir — /tmp/mempalace_export_closets
|
||||
#
|
||||
# After export, sync with:
|
||||
# rsync -avz --delete /tmp/mempalace_export_closets/ alpha:/var/lib/mempalace/fleet/bezalel/
|
||||
#
|
||||
# Refs: #1083, #1075
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PALACE_DIR="${1:-${MEMPALACE_DIR:-/root/wizards/bezalel/.mempalace/palace}}"
|
||||
EXPORT_DIR="${2:-/tmp/mempalace_export_closets}"
|
||||
WIZARD="${MEMPALACE_WING:-bezalel}"
|
||||
|
||||
echo "[export_closets] Wizard: $WIZARD"
|
||||
echo "[export_closets] Palace: $PALACE_DIR"
|
||||
echo "[export_closets] Export: $EXPORT_DIR"
|
||||
|
||||
if [[ ! -d "$PALACE_DIR" ]]; then
|
||||
echo "[export_closets] ERROR: palace not found: $PALACE_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Validate closets-only policy: abort if any raw drawer files are present in export scope.
|
||||
# Closets are files named *.closet.json or stored under a closets/ subdirectory.
|
||||
# Raw drawers are everything else (*.drawer.json, *.md source files, etc.).
|
||||
|
||||
DRAWER_COUNT=0
|
||||
while IFS= read -r -d '' f; do
|
||||
# Raw drawer check: any .json file that is NOT a closet
|
||||
basename_f="$(basename "$f")"
|
||||
if [[ "$basename_f" == *.drawer.json ]]; then
|
||||
echo "[export_closets] POLICY VIOLATION: raw drawer found in export scope: $f" >&2
|
||||
DRAWER_COUNT=$((DRAWER_COUNT + 1))
|
||||
fi
|
||||
done < <(find "$PALACE_DIR" -type f -name "*.json" -print0 2>/dev/null)
|
||||
|
||||
if [[ "$DRAWER_COUNT" -gt 0 ]]; then
|
||||
echo "[export_closets] ABORT: $DRAWER_COUNT raw drawer(s) detected. Only closets may be exported." >&2
|
||||
echo "[export_closets] Run mempalace compress to generate closets before exporting." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Also check for source_file metadata in closet JSON that would expose private paths.
|
||||
SOURCE_FILE_LEAKS=0
|
||||
while IFS= read -r -d '' f; do
|
||||
if python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
data = json.load(open('$f'))
|
||||
drawers = data.get('drawers', []) if isinstance(data, dict) else []
|
||||
for d in drawers:
|
||||
if 'source_file' in d and not d.get('closet', False):
|
||||
sys.exit(1)
|
||||
except Exception:
|
||||
pass
|
||||
sys.exit(0)
|
||||
" 2>/dev/null; then
|
||||
:
|
||||
else
|
||||
echo "[export_closets] POLICY VIOLATION: source_file metadata in non-closet: $f" >&2
|
||||
SOURCE_FILE_LEAKS=$((SOURCE_FILE_LEAKS + 1))
|
||||
fi
|
||||
done < <(find "$PALACE_DIR" -type f -name "*.closet.json" -print0 2>/dev/null)
|
||||
|
||||
if [[ "$SOURCE_FILE_LEAKS" -gt 0 ]]; then
|
||||
echo "[export_closets] ABORT: $SOURCE_FILE_LEAKS file(s) contain private source_file paths." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Collect closet files
|
||||
mkdir -p "$EXPORT_DIR/$WIZARD"
|
||||
CLOSET_COUNT=0
|
||||
while IFS= read -r -d '' f; do
|
||||
rel_path="${f#$PALACE_DIR/}"
|
||||
dest="$EXPORT_DIR/$WIZARD/$rel_path"
|
||||
mkdir -p "$(dirname "$dest")"
|
||||
cp "$f" "$dest"
|
||||
CLOSET_COUNT=$((CLOSET_COUNT + 1))
|
||||
done < <(find "$PALACE_DIR" -type f -name "*.closet.json" -print0 2>/dev/null)
|
||||
|
||||
if [[ "$CLOSET_COUNT" -eq 0 ]]; then
|
||||
echo "[export_closets] WARNING: no closet files found in $PALACE_DIR" >&2
|
||||
echo "[export_closets] Run 'mempalace compress' to generate closets from drawers." >&2
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "[export_closets] Exported $CLOSET_COUNT closet(s) to $EXPORT_DIR/$WIZARD/"
|
||||
echo "[export_closets] OK — ready for fleet sync."
|
||||
echo ""
|
||||
echo " rsync -avz --delete $EXPORT_DIR/$WIZARD/ alpha:/var/lib/mempalace/fleet/$WIZARD/"
|
||||
@@ -1,114 +0,0 @@
|
||||
# MemPalace Fleet Taxonomy Standard
|
||||
# Refs: #1082, #1075 (MemPalace × Evennia — Fleet Memory milestone)
|
||||
#
|
||||
# Every wizard palace MUST contain the 5 core rooms listed under `core_rooms`.
|
||||
# Optional domain-specific rooms are listed under `optional_rooms` for reference.
|
||||
# Wizards may add additional rooms beyond this taxonomy.
|
||||
#
|
||||
# Room schema fields:
|
||||
# key — machine-readable slug (used for tunnel routing and fleet search)
|
||||
# label — human-readable display name
|
||||
# purpose — one-line description of what belongs here
|
||||
# examples — sample artifact types filed in this room
|
||||
|
||||
version: "1"
|
||||
|
||||
core_rooms:
|
||||
- key: forge
|
||||
label: Forge
|
||||
purpose: CI pipelines, builds, infra configuration, deployment artefacts
|
||||
examples:
|
||||
- build logs
|
||||
- CI run summaries
|
||||
- Dockerfile changes
|
||||
- cron job definitions
|
||||
- server provisioning notes
|
||||
|
||||
- key: hermes
|
||||
label: Hermes
|
||||
purpose: Agent platform, Hermes gateway, harness CLI, inter-agent messaging
|
||||
examples:
|
||||
- harness config snapshots
|
||||
- agent boot reports
|
||||
- MCP tool definitions
|
||||
- Hermes gateway events
|
||||
- worker health logs
|
||||
|
||||
- key: nexus
|
||||
label: Nexus
|
||||
purpose: Project reports, documentation, knowledge transfer, field reports
|
||||
examples:
|
||||
- SITREP documents
|
||||
- architecture decision records
|
||||
- field reports
|
||||
- onboarding docs
|
||||
- milestone summaries
|
||||
|
||||
- key: issues
|
||||
label: Issues
|
||||
purpose: Tickets, backlog items, PR summaries, bug reports
|
||||
examples:
|
||||
- Gitea issue summaries
|
||||
- PR merge notes
|
||||
- bug reproduction steps
|
||||
- acceptance criteria
|
||||
|
||||
- key: experiments
|
||||
label: Experiments
|
||||
purpose: Prototypes, spikes, sandbox work, exploratory research
|
||||
examples:
|
||||
- spike results
|
||||
- A/B test notes
|
||||
- proof-of-concept code snippets
|
||||
- benchmark data
|
||||
|
||||
optional_rooms:
|
||||
- key: evennia
|
||||
label: Evennia
|
||||
purpose: MUD world state, room descriptions, NPC dialogue, game events
|
||||
wizards: [bezalel, timmy]
|
||||
|
||||
- key: game-portals
|
||||
label: Game Portals
|
||||
purpose: Portal registry, zone configs, dungeon layouts, loot tables
|
||||
wizards: [timmy]
|
||||
|
||||
- key: lazarus-pit
|
||||
label: Lazarus Pit
|
||||
purpose: Dead/parked work, archived experiments, deprecated configs
|
||||
wizards: [timmy, allegro, bezalel]
|
||||
|
||||
- key: satflow
|
||||
label: SatFlow
|
||||
purpose: Economy visualizations, satoshi flow tracking, L402 audit trails
|
||||
wizards: [timmy, allegro]
|
||||
|
||||
- key: workspace
|
||||
label: Workspace
|
||||
purpose: General scratch notes, daily logs, personal coordination
|
||||
wizards: ["*"]
|
||||
|
||||
- key: home
|
||||
label: Home
|
||||
purpose: Personal identity, agent persona, preferences, capability docs
|
||||
wizards: ["*"]
|
||||
|
||||
- key: general
|
||||
label: General
|
||||
purpose: Catch-all for artefacts not yet assigned to a named room
|
||||
wizards: ["*"]
|
||||
|
||||
# Tunnel routing table
|
||||
# Defines which room pairs are connected across wizard wings.
|
||||
# A tunnel lets `recall <query> --fleet` search both wings at once.
|
||||
tunnels:
|
||||
- rooms: [forge, forge]
|
||||
description: Build and infra knowledge shared across all wizards
|
||||
- rooms: [hermes, hermes]
|
||||
description: Harness platform knowledge shared across all wizards
|
||||
- rooms: [nexus, nexus]
|
||||
description: Cross-wizard documentation and field reports
|
||||
- rooms: [issues, issues]
|
||||
description: Fleet-wide issue and PR knowledge
|
||||
- rooms: [experiments, experiments]
|
||||
description: Cross-wizard spike and prototype results
|
||||
@@ -1,119 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
validate_rooms.py — Fleet palace taxonomy validator.
|
||||
|
||||
Checks a wizard's mempalace.yaml against the fleet standard in rooms.yaml.
|
||||
Exits 0 if valid, 1 if core rooms are missing or the config is malformed.
|
||||
|
||||
Usage:
|
||||
python mempalace/validate_rooms.py <wizard_mempalace.yaml>
|
||||
python mempalace/validate_rooms.py /root/wizards/bezalel/mempalace.yaml
|
||||
|
||||
Refs: #1082, #1075
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
print("ERROR: PyYAML is required. Install with: pip install pyyaml", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
|
||||
FLEET_STANDARD = Path(__file__).parent / "rooms.yaml"
|
||||
|
||||
|
||||
def load_yaml(path: Path) -> dict[str, Any]:
|
||||
with path.open() as fh:
|
||||
return yaml.safe_load(fh) or {}
|
||||
|
||||
|
||||
def get_core_room_keys(standard: dict[str, Any]) -> list[str]:
|
||||
return [r["key"] for r in standard.get("core_rooms", [])]
|
||||
|
||||
|
||||
def get_wizard_room_keys(config: dict[str, Any]) -> list[str]:
|
||||
"""Extract room keys from a wizard's mempalace.yaml.
|
||||
|
||||
Supports two common shapes:
|
||||
rooms:
|
||||
- key: forge
|
||||
- key: hermes
|
||||
or:
|
||||
rooms:
|
||||
forge: ...
|
||||
hermes: ...
|
||||
"""
|
||||
rooms_field = config.get("rooms", {})
|
||||
if isinstance(rooms_field, list):
|
||||
return [r["key"] for r in rooms_field if isinstance(r, dict) and "key" in r]
|
||||
if isinstance(rooms_field, dict):
|
||||
return list(rooms_field.keys())
|
||||
return []
|
||||
|
||||
|
||||
def validate(wizard_config_path: Path, standard_path: Path = FLEET_STANDARD) -> list[str]:
|
||||
"""Return a list of validation errors. Empty list means valid."""
|
||||
errors: list[str] = []
|
||||
|
||||
if not standard_path.exists():
|
||||
errors.append(f"Fleet standard not found: {standard_path}")
|
||||
return errors
|
||||
|
||||
if not wizard_config_path.exists():
|
||||
errors.append(f"Wizard config not found: {wizard_config_path}")
|
||||
return errors
|
||||
|
||||
standard = load_yaml(standard_path)
|
||||
config = load_yaml(wizard_config_path)
|
||||
|
||||
core_keys = get_core_room_keys(standard)
|
||||
wizard_keys = get_wizard_room_keys(config)
|
||||
|
||||
missing = [k for k in core_keys if k not in wizard_keys]
|
||||
for key in missing:
|
||||
errors.append(f"Missing required core room: '{key}'")
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Validate a wizard's mempalace.yaml against the fleet room standard."
|
||||
)
|
||||
parser.add_argument(
|
||||
"config",
|
||||
metavar="mempalace.yaml",
|
||||
help="Path to the wizard's mempalace.yaml",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--standard",
|
||||
default=str(FLEET_STANDARD),
|
||||
metavar="rooms.yaml",
|
||||
help="Path to the fleet rooms.yaml standard (default: mempalace/rooms.yaml)",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
wizard_path = Path(args.config)
|
||||
standard_path = Path(args.standard)
|
||||
|
||||
errors = validate(wizard_path, standard_path)
|
||||
|
||||
if errors:
|
||||
print(f"[validate_rooms] FAIL: {wizard_path}", file=sys.stderr)
|
||||
for err in errors:
|
||||
print(f" ✗ {err}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
core_count = len(get_core_room_keys(load_yaml(standard_path)))
|
||||
print(f"[validate_rooms] OK: {wizard_path} — all {core_count} core rooms present.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,136 +0,0 @@
|
||||
"""Poka-yoke heartbeat writer for cron jobs.
|
||||
|
||||
Every scheduled job calls write_cron_heartbeat() on successful completion so
|
||||
the meta-heartbeat checker (bin/check_cron_heartbeats.py) can verify that all
|
||||
jobs are still alive. Absence of a fresh heartbeat = silent failure.
|
||||
|
||||
Path convention
|
||||
---------------
|
||||
Primary: /var/run/bezalel/heartbeats/<job>.last
|
||||
Fallback: ~/.bezalel/heartbeats/<job>.last
|
||||
(used when /var/run/bezalel is not writable, e.g. dev machines)
|
||||
Override: BEZALEL_HEARTBEAT_DIR environment variable
|
||||
|
||||
Heartbeat file format (JSON)
|
||||
----------------------------
|
||||
{
|
||||
"job": "nexus_watchdog",
|
||||
"timestamp": 1744000000.0,
|
||||
"interval_seconds": 300,
|
||||
"pid": 12345,
|
||||
"status": "ok"
|
||||
}
|
||||
|
||||
Usage in a cron job
|
||||
-------------------
|
||||
from nexus.cron_heartbeat import write_cron_heartbeat
|
||||
|
||||
def main():
|
||||
# ... do the work ...
|
||||
write_cron_heartbeat("my_job_name", interval_seconds=300)
|
||||
|
||||
Zero-dependency shell one-liner (for scripts that can't import Python)
|
||||
-----------------------------------------------------------------------
|
||||
python -c "
|
||||
from nexus.cron_heartbeat import write_cron_heartbeat
|
||||
write_cron_heartbeat('my_job', interval_seconds=300)
|
||||
"
|
||||
|
||||
Refs: #1096
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
PRIMARY_HEARTBEAT_DIR = Path("/var/run/bezalel/heartbeats")
|
||||
FALLBACK_HEARTBEAT_DIR = Path.home() / ".bezalel" / "heartbeats"
|
||||
|
||||
|
||||
def _resolve_heartbeat_dir() -> Path:
|
||||
"""Return the heartbeat directory, trying primary then fallback.
|
||||
|
||||
If BEZALEL_HEARTBEAT_DIR is set in the environment that wins outright
|
||||
(useful for tests and non-standard deployments).
|
||||
"""
|
||||
env = os.environ.get("BEZALEL_HEARTBEAT_DIR")
|
||||
if env:
|
||||
return Path(env)
|
||||
|
||||
# Try to create and write-test the primary path
|
||||
try:
|
||||
PRIMARY_HEARTBEAT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
probe = PRIMARY_HEARTBEAT_DIR / ".write_probe"
|
||||
probe.touch()
|
||||
probe.unlink()
|
||||
return PRIMARY_HEARTBEAT_DIR
|
||||
except (PermissionError, OSError):
|
||||
pass
|
||||
|
||||
FALLBACK_HEARTBEAT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
return FALLBACK_HEARTBEAT_DIR
|
||||
|
||||
|
||||
def heartbeat_path(job: str, directory: Path | None = None) -> Path:
|
||||
"""Return the Path where *job*'s heartbeat file lives.
|
||||
|
||||
Useful for readers (e.g. the Night Watch report) that just need the
|
||||
location without writing anything.
|
||||
"""
|
||||
d = directory if directory is not None else _resolve_heartbeat_dir()
|
||||
return d / f"{job}.last"
|
||||
|
||||
|
||||
def write_cron_heartbeat(
|
||||
job: str,
|
||||
interval_seconds: int,
|
||||
status: str = "ok",
|
||||
directory: Path | None = None,
|
||||
) -> Path:
|
||||
"""Write a poka-yoke heartbeat file for a cron job.
|
||||
|
||||
Call this at the end of your job's main function. The file is written
|
||||
atomically (write-to-temp + rename) so the checker never reads a partial
|
||||
file.
|
||||
|
||||
Args:
|
||||
job: Unique job name, e.g. ``"nexus_watchdog"``.
|
||||
interval_seconds: Expected run cadence, e.g. ``300`` for every 5 min.
|
||||
status: Completion status: ``"ok"``, ``"warn"``, or
|
||||
``"error"``. Only ``"ok"`` resets the stale clock.
|
||||
directory: Override the heartbeat directory (mainly for tests).
|
||||
|
||||
Returns:
|
||||
Path to the written heartbeat file.
|
||||
"""
|
||||
d = directory if directory is not None else _resolve_heartbeat_dir()
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
path = d / f"{job}.last"
|
||||
|
||||
data = {
|
||||
"job": job,
|
||||
"timestamp": time.time(),
|
||||
"interval_seconds": interval_seconds,
|
||||
"pid": os.getpid(),
|
||||
"status": status,
|
||||
}
|
||||
|
||||
# Atomic write: temp file in same directory + rename.
|
||||
# Guarantees the checker never sees a half-written file.
|
||||
fd, tmp = tempfile.mkstemp(dir=str(d), prefix=f".{job}-", suffix=".tmp")
|
||||
try:
|
||||
with os.fdopen(fd, "w") as f:
|
||||
json.dump(data, f)
|
||||
os.replace(tmp, str(path))
|
||||
except Exception:
|
||||
# Best-effort — never crash the job over a heartbeat failure
|
||||
try:
|
||||
os.unlink(tmp)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return path
|
||||
@@ -1,49 +0,0 @@
|
||||
"""nexus.evennia_mempalace — Evennia plugin for MemPalace fleet memory.
|
||||
|
||||
This contrib module provides:
|
||||
|
||||
Commands (add to ``settings.CMDSETS_DEFAULT`` or a CmdSet):
|
||||
CmdRecall — ``recall <query>`` / ``recall <query> --fleet``
|
||||
CmdEnterRoom — ``enter room <topic>`` teleports to a palace room
|
||||
CmdRecord — ``record decision <text>`` writes to hall_facts
|
||||
CmdNote — ``note breakthrough <text>`` writes to hall_discoveries
|
||||
CmdEvent — ``event <text>`` writes to hall_events
|
||||
|
||||
Typeclasses (use in place of Evennia's default Room/Character):
|
||||
MemPalaceRoom — Room whose description auto-populates from palace search
|
||||
StewardNPC — Wizard steward that answers questions via palace search
|
||||
|
||||
Usage example (in your Evennia game's ``mygame/server/conf/settings.py``)::
|
||||
|
||||
MEMPALACE_PATH = "/root/wizards/bezalel/.mempalace/palace"
|
||||
MEMPALACE_WING = "bezalel"
|
||||
FLEET_PALACE_PATH = "/var/lib/mempalace/fleet"
|
||||
|
||||
Then import commands into a CmdSet::
|
||||
|
||||
from nexus.evennia_mempalace.commands import (
|
||||
CmdRecall, CmdEnterRoom, CmdRecord, CmdNote, CmdEvent
|
||||
)
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from nexus.evennia_mempalace.commands import (
|
||||
CmdEnterRoom,
|
||||
CmdEvent,
|
||||
CmdNote,
|
||||
CmdRecord,
|
||||
CmdRecall,
|
||||
)
|
||||
from nexus.evennia_mempalace.typeclasses.rooms import MemPalaceRoom
|
||||
from nexus.evennia_mempalace.typeclasses.npcs import StewardNPC
|
||||
|
||||
__all__ = [
|
||||
"CmdRecall",
|
||||
"CmdEnterRoom",
|
||||
"CmdRecord",
|
||||
"CmdNote",
|
||||
"CmdEvent",
|
||||
"MemPalaceRoom",
|
||||
"StewardNPC",
|
||||
]
|
||||
Binary file not shown.
@@ -1,15 +0,0 @@
|
||||
"""MemPalace Evennia commands."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from nexus.evennia_mempalace.commands.recall import CmdRecall, CmdEnterRoom, CmdAsk
|
||||
from nexus.evennia_mempalace.commands.write import CmdRecord, CmdNote, CmdEvent
|
||||
|
||||
__all__ = [
|
||||
"CmdRecall",
|
||||
"CmdEnterRoom",
|
||||
"CmdAsk",
|
||||
"CmdRecord",
|
||||
"CmdNote",
|
||||
"CmdEvent",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,267 +0,0 @@
|
||||
"""Evennia commands for querying the MemPalace.
|
||||
|
||||
CmdRecall — semantic search across the caller's wing (or fleet)
|
||||
CmdEnterRoom — teleport to the palace room matching a topic
|
||||
CmdAsk — ask a steward NPC a question about their wing's memory
|
||||
|
||||
These commands are designed to work inside a live Evennia server.
|
||||
They import ``evennia`` at class-definition time only to set up the
|
||||
command skeleton; the actual search logic lives in ``nexus.mempalace``
|
||||
and is fully testable without a running Evennia instance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from nexus.mempalace.searcher import (
|
||||
MemPalaceUnavailable,
|
||||
MemPalaceResult,
|
||||
search_memories,
|
||||
search_fleet,
|
||||
)
|
||||
from nexus.mempalace.config import FLEET_WING, CORE_ROOMS
|
||||
|
||||
try:
|
||||
from evennia import Command as _EvCommand # type: ignore
|
||||
if _EvCommand is None:
|
||||
raise ImportError("evennia.Command is None (Django not configured)")
|
||||
Command = _EvCommand
|
||||
except (ImportError, Exception): # outside a live Evennia environment
|
||||
class Command: # type: ignore # minimal stub for import/testing
|
||||
key = ""
|
||||
aliases: list = []
|
||||
locks = "cmd:all()"
|
||||
help_category = "MemPalace"
|
||||
|
||||
def __init__(self):
|
||||
self.caller = None
|
||||
self.args = ""
|
||||
self.switches: list[str] = []
|
||||
|
||||
def func(self):
|
||||
pass
|
||||
|
||||
|
||||
class CmdRecall(Command):
|
||||
"""Search the mind palace for memories matching a query.
|
||||
|
||||
Usage:
|
||||
recall <query>
|
||||
recall <query> --fleet
|
||||
recall <query> --room <room>
|
||||
|
||||
Examples:
|
||||
recall nightly watch failures
|
||||
recall GraphQL --fleet
|
||||
recall CI pipeline --room forge
|
||||
|
||||
The ``--fleet`` switch searches the shared fleet wing (closets only).
|
||||
Without it, only the caller's private wing is searched.
|
||||
"""
|
||||
|
||||
key = "recall"
|
||||
aliases = ["mem", "remember"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "MemPalace"
|
||||
|
||||
def func(self):
|
||||
raw = self.args.strip()
|
||||
if not raw:
|
||||
self.caller.msg("Usage: recall <query> [--fleet] [--room <room>]")
|
||||
return
|
||||
|
||||
fleet_mode = "--fleet" in self.switches
|
||||
room_filter = None
|
||||
if "--room" in self.switches:
|
||||
# Grab the word after --room
|
||||
parts = raw.split()
|
||||
try:
|
||||
room_filter = parts[parts.index("--room") + 1]
|
||||
parts = [p for p in parts if p not in ("--room", room_filter)]
|
||||
raw = " ".join(parts)
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
# Strip inline switch tokens from query text
|
||||
query = raw.replace("--fleet", "").strip()
|
||||
if not query:
|
||||
self.caller.msg("Please provide a search query.")
|
||||
return
|
||||
|
||||
wing = getattr(self.caller.db, "wing", None) or FLEET_WING
|
||||
|
||||
try:
|
||||
if fleet_mode:
|
||||
results = search_fleet(query, room=room_filter)
|
||||
header = f"|cFleet palace|n — searching all wings for: |w{query}|n"
|
||||
else:
|
||||
results = search_memories(
|
||||
query, wing=wing, room=room_filter
|
||||
)
|
||||
header = (
|
||||
f"|cPalace|n [{wing}] — searching for: |w{query}|n"
|
||||
+ (f" in room |y{room_filter}|n" if room_filter else "")
|
||||
)
|
||||
except MemPalaceUnavailable as exc:
|
||||
self.caller.msg(f"|rPalace unavailable:|n {exc}")
|
||||
return
|
||||
|
||||
if not results:
|
||||
self.caller.msg(f"{header}\n|yNo memories found.|n")
|
||||
return
|
||||
|
||||
self.caller.msg(header)
|
||||
for i, r in enumerate(results[:5], start=1):
|
||||
wing_tag = f" |x[{r.wing}]|n" if fleet_mode and r.wing else ""
|
||||
self.caller.msg(
|
||||
f"|c{i}. {r.room}{wing_tag}|n (score {r.score:.2f})\n"
|
||||
f" {r.short(240)}"
|
||||
)
|
||||
|
||||
|
||||
class CmdEnterRoom(Command):
|
||||
"""Teleport to the palace room that best matches a topic.
|
||||
|
||||
Usage:
|
||||
enter room <topic>
|
||||
|
||||
Examples:
|
||||
enter room forge
|
||||
enter room CI failures
|
||||
enter room agent architecture
|
||||
|
||||
If the topic matches a canonical room name exactly, you are
|
||||
teleported there directly. Otherwise a semantic search finds
|
||||
the closest room and you are taken there.
|
||||
"""
|
||||
|
||||
key = "enter room"
|
||||
aliases = ["go to room", "palace room"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "MemPalace"
|
||||
|
||||
def func(self):
|
||||
topic = self.args.strip()
|
||||
if not topic:
|
||||
self.caller.msg("Usage: enter room <topic>")
|
||||
rooms = ", ".join(f"|c{r}|n" for r in CORE_ROOMS)
|
||||
self.caller.msg(f"Core palace rooms: {rooms}")
|
||||
return
|
||||
|
||||
# Resolve room name — exact match first, then semantic
|
||||
if topic.lower() in CORE_ROOMS:
|
||||
room_name = topic.lower()
|
||||
else:
|
||||
# Fuzzy: pick the room whose name is most similar
|
||||
room_name = _closest_room(topic)
|
||||
|
||||
# Try to find the in-game room object by key/alias
|
||||
try:
|
||||
from evennia.utils.search import search_object # type: ignore
|
||||
matches = search_object(
|
||||
room_name,
|
||||
typeclass="nexus.evennia_mempalace.typeclasses.rooms.MemPalaceRoom",
|
||||
)
|
||||
except Exception:
|
||||
matches = []
|
||||
|
||||
if matches:
|
||||
destination = matches[0]
|
||||
self.caller.move_to(destination, quiet=False)
|
||||
else:
|
||||
self.caller.msg(
|
||||
f"|yNo palace room found for '|w{room_name}|y'.|n\n"
|
||||
"Ask the world administrator to create the room with the "
|
||||
"|cMemPalaceRoom|n typeclass."
|
||||
)
|
||||
|
||||
|
||||
_ROOM_KEYWORDS: dict[str, list[str]] = {
|
||||
"forge": ["ci", "build", "pipeline", "deploy", "docker", "infra", "cron", "runner"],
|
||||
"hermes": ["hermes", "agent", "gateway", "cli", "harness", "mcp", "session"],
|
||||
"nexus": ["nexus", "report", "doc", "sitrep", "knowledge", "kt", "handoff"],
|
||||
"issues": ["issue", "ticket", "bug", "pr", "backlog", "triage", "milestone"],
|
||||
"experiments": ["experiment", "spike", "prototype", "bench", "research", "proof"],
|
||||
}
|
||||
|
||||
|
||||
def _closest_room(topic: str) -> str:
|
||||
"""Return the CORE_ROOMS name most similar to *topic*.
|
||||
|
||||
Checks in order:
|
||||
1. Exact name match.
|
||||
2. Name substring in topic (or vice versa).
|
||||
3. Keyword synonym lookup.
|
||||
"""
|
||||
topic_lower = topic.lower()
|
||||
topic_words = set(topic_lower.split())
|
||||
|
||||
for room in CORE_ROOMS:
|
||||
if room == topic_lower or room in topic_lower or topic_lower in room:
|
||||
return room
|
||||
|
||||
for room, keywords in _ROOM_KEYWORDS.items():
|
||||
for kw in keywords:
|
||||
if kw in topic_words or any(kw in w for w in topic_words):
|
||||
return room
|
||||
|
||||
return "general"
|
||||
|
||||
|
||||
class CmdAsk(Command):
|
||||
"""Ask a steward NPC a question about their wing's memory.
|
||||
|
||||
Usage:
|
||||
ask <npc-name> about <topic>
|
||||
ask steward about CI pipeline
|
||||
ask bezalel-steward about nightly watch failures
|
||||
|
||||
The NPC must be in the current room and must use the StewardNPC
|
||||
typeclass. Their response is drawn from a live palace search.
|
||||
"""
|
||||
|
||||
key = "ask"
|
||||
locks = "cmd:all()"
|
||||
help_category = "MemPalace"
|
||||
|
||||
def func(self):
|
||||
raw = self.args.strip()
|
||||
if " about " not in raw:
|
||||
self.caller.msg("Usage: ask <npc-name> about <topic>")
|
||||
return
|
||||
|
||||
npc_name, _, topic = raw.partition(" about ")
|
||||
npc_name = npc_name.strip()
|
||||
topic = topic.strip()
|
||||
|
||||
if not npc_name or not topic:
|
||||
self.caller.msg("Usage: ask <npc-name> about <topic>")
|
||||
return
|
||||
|
||||
# Find the NPC in the current room
|
||||
try:
|
||||
from evennia.utils.search import search_object # type: ignore
|
||||
candidates = search_object(
|
||||
npc_name,
|
||||
typeclass="nexus.evennia_mempalace.typeclasses.npcs.StewardNPC",
|
||||
)
|
||||
except Exception:
|
||||
candidates = []
|
||||
|
||||
if not candidates:
|
||||
# Fallback: search contents of the current room by name
|
||||
location = getattr(self.caller, "location", None)
|
||||
candidates = [
|
||||
obj for obj in (getattr(location, "contents", []) or [])
|
||||
if npc_name.lower() in obj.key.lower()
|
||||
]
|
||||
|
||||
if not candidates:
|
||||
self.caller.msg(
|
||||
f"|yNo steward named '|w{npc_name}|y' found here.|n\n"
|
||||
"Stewards are created with the |cStewardNPC|n typeclass."
|
||||
)
|
||||
return
|
||||
|
||||
npc = candidates[0]
|
||||
response = npc.respond_to_question(topic, asker=self.caller)
|
||||
self.caller.msg(response)
|
||||
@@ -1,124 +0,0 @@
|
||||
"""Evennia commands for writing new memories to the palace.
|
||||
|
||||
CmdRecord — record decision <text> → files into hall_facts
|
||||
CmdNote — note breakthrough <text> → files into hall_discoveries
|
||||
CmdEvent — event <text> → files into hall_events
|
||||
|
||||
Phase 4 deliverable (see issue #1080).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from nexus.mempalace.searcher import MemPalaceUnavailable, add_memory
|
||||
from nexus.mempalace.config import FLEET_WING
|
||||
|
||||
try:
|
||||
from evennia import Command as _EvCommand # type: ignore
|
||||
if _EvCommand is None:
|
||||
raise ImportError("evennia.Command is None (Django not configured)")
|
||||
Command = _EvCommand
|
||||
except (ImportError, Exception):
|
||||
class Command: # type: ignore
|
||||
key = ""
|
||||
aliases: list = []
|
||||
locks = "cmd:all()"
|
||||
help_category = "MemPalace"
|
||||
|
||||
def __init__(self):
|
||||
self.caller = None
|
||||
self.args = ""
|
||||
self.switches: list[str] = []
|
||||
|
||||
def func(self):
|
||||
pass
|
||||
|
||||
|
||||
class _MemWriteCommand(Command):
|
||||
"""Base class for palace write commands."""
|
||||
|
||||
_room: str = "general"
|
||||
_label: str = "memory"
|
||||
|
||||
def func(self):
|
||||
text = self.args.strip()
|
||||
if not text:
|
||||
self.caller.msg(f"Usage: {self.key} <text>")
|
||||
return
|
||||
|
||||
wing = getattr(self.caller.db, "wing", None) or FLEET_WING
|
||||
try:
|
||||
doc_id = add_memory(
|
||||
text,
|
||||
room=self._room,
|
||||
wing=wing,
|
||||
extra_metadata={"via": "evennia_cmd", "cmd": self.key, "added_by": "evennia"},
|
||||
)
|
||||
except MemPalaceUnavailable as exc:
|
||||
self.caller.msg(f"|rPalace unavailable:|n {exc}")
|
||||
return
|
||||
|
||||
self.caller.msg(
|
||||
f"|gFiled {self._label} into |c{self._room}|g.|n (id: {doc_id[:8]}…)"
|
||||
)
|
||||
|
||||
|
||||
class CmdRecord(_MemWriteCommand):
|
||||
"""Record a decision into the palace (hall_facts).
|
||||
|
||||
Usage:
|
||||
record <text>
|
||||
record decision <text>
|
||||
|
||||
Example:
|
||||
record We decided to use ChromaDB for local palace storage.
|
||||
|
||||
The text is filed into the ``hall_facts`` room of your wing and
|
||||
becomes searchable via ``recall``.
|
||||
"""
|
||||
|
||||
key = "record"
|
||||
aliases = ["record decision"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "MemPalace"
|
||||
_room = "hall_facts"
|
||||
_label = "decision"
|
||||
|
||||
|
||||
class CmdNote(_MemWriteCommand):
|
||||
"""File a breakthrough note into the palace (hall_discoveries).
|
||||
|
||||
Usage:
|
||||
note <text>
|
||||
note breakthrough <text>
|
||||
|
||||
Example:
|
||||
note breakthrough AAAK compression reduces token cost by 40%.
|
||||
|
||||
The text is filed into the ``hall_discoveries`` room of your wing.
|
||||
"""
|
||||
|
||||
key = "note"
|
||||
aliases = ["note breakthrough"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "MemPalace"
|
||||
_room = "hall_discoveries"
|
||||
_label = "breakthrough"
|
||||
|
||||
|
||||
class CmdEvent(_MemWriteCommand):
|
||||
"""Log a significant event into the palace (hall_events).
|
||||
|
||||
Usage:
|
||||
event <text>
|
||||
|
||||
Example:
|
||||
event Deployed Evennia bridge to production on Alpha.
|
||||
|
||||
The text is filed into the ``hall_events`` room of your wing.
|
||||
"""
|
||||
|
||||
key = "event"
|
||||
locks = "cmd:all()"
|
||||
help_category = "MemPalace"
|
||||
_room = "hall_events"
|
||||
_label = "event"
|
||||
@@ -1 +0,0 @@
|
||||
"""MemPalace Evennia typeclasses."""
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,138 +0,0 @@
|
||||
"""StewardNPC — wizard steward that answers questions via palace search.
|
||||
|
||||
Each wizard wing has a steward NPC that players can interrogate about
|
||||
the wing's history. The NPC:
|
||||
|
||||
1. Detects the topic from the player's question.
|
||||
2. Calls ``search_memories`` with wing + optional room filters.
|
||||
3. Formats the top results as an in-character response.
|
||||
|
||||
Phase 3 deliverable (see issue #1079).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from nexus.mempalace.searcher import MemPalaceUnavailable, search_memories
|
||||
from nexus.mempalace.config import FLEET_WING
|
||||
|
||||
try:
|
||||
from evennia import DefaultCharacter as _EvDefaultCharacter # type: ignore
|
||||
if _EvDefaultCharacter is None:
|
||||
raise ImportError("evennia.DefaultCharacter is None")
|
||||
DefaultCharacter = _EvDefaultCharacter
|
||||
except (ImportError, Exception):
|
||||
class DefaultCharacter: # type: ignore # minimal stub
|
||||
db: object = None
|
||||
key: str = ""
|
||||
|
||||
def msg(self, text: str, **kwargs):
|
||||
pass
|
||||
|
||||
def execute_cmd(self, raw_string: str, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
# Steward response templates
|
||||
_FOUND_TEMPLATE = (
|
||||
"|c{name}|n glances inward, consulting the palace...\n\n"
|
||||
"I find {count} relevant {plural} about |w{topic}|n:\n\n"
|
||||
"{memories}\n"
|
||||
"|xType '|wrecall {topic}|x' to search further.|n"
|
||||
)
|
||||
_NOT_FOUND_TEMPLATE = (
|
||||
"|c{name}|n ponders a moment, then shakes their head.\n"
|
||||
"\"I found nothing about |w{topic}|n in this wing's memory.\""
|
||||
)
|
||||
_UNAVAILABLE_TEMPLATE = (
|
||||
"|c{name}|n frowns. \"The palace is unreachable right now.\""
|
||||
)
|
||||
|
||||
|
||||
class StewardNPC(DefaultCharacter):
|
||||
"""An NPC that serves as the custodian of a wizard's memory wing.
|
||||
|
||||
Attributes (set via ``npc.db.<attr>``):
|
||||
steward_wing (str): The wizard wing this steward guards.
|
||||
Defaults to ``FLEET_WING``.
|
||||
steward_name (str): Display name used in responses.
|
||||
Defaults to ``self.key``.
|
||||
steward_n_results (int): How many memories to surface.
|
||||
Default 3.
|
||||
|
||||
Usage (from game)::
|
||||
|
||||
> ask bezalel-steward about nightly watch failures
|
||||
> ask steward about CI pipeline
|
||||
"""
|
||||
|
||||
# Evennia will call at_say when players speak near the NPC
|
||||
def at_say(self, message: str, msg_type: str = "say", **kwargs):
|
||||
"""Intercept nearby speech that looks like a question."""
|
||||
super().at_say(message, msg_type=msg_type, **kwargs)
|
||||
|
||||
def respond_to_question(self, question: str, asker=None) -> str:
|
||||
"""Answer a question by searching the wing's palace.
|
||||
|
||||
Args:
|
||||
question: The player's raw question text.
|
||||
asker: The asking character object (used to personalise output).
|
||||
|
||||
Returns:
|
||||
Formatted response string.
|
||||
"""
|
||||
topic = _extract_topic(question)
|
||||
wing = self.db.steward_wing or FLEET_WING
|
||||
name = self.db.steward_name or self.key
|
||||
n = self.db.steward_n_results or 3
|
||||
|
||||
try:
|
||||
results = search_memories(topic, wing=wing, n_results=n)
|
||||
except MemPalaceUnavailable:
|
||||
return _UNAVAILABLE_TEMPLATE.format(name=name)
|
||||
|
||||
if not results:
|
||||
return _NOT_FOUND_TEMPLATE.format(name=name, topic=topic)
|
||||
|
||||
memory_lines = []
|
||||
for i, r in enumerate(results, start=1):
|
||||
memory_lines.append(
|
||||
f"|w{i}. [{r.room}]|n {r.short(220)}"
|
||||
)
|
||||
|
||||
return _FOUND_TEMPLATE.format(
|
||||
name=name,
|
||||
count=len(results),
|
||||
plural="memory" if len(results) == 1 else "memories",
|
||||
topic=topic,
|
||||
memories="\n".join(memory_lines),
|
||||
)
|
||||
|
||||
|
||||
# ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
_QUESTION_PREFIXES = (
|
||||
"about ", "regarding ", "on ", "concerning ",
|
||||
"related to ", "for ", "with ", "involving ",
|
||||
)
|
||||
|
||||
|
||||
def _extract_topic(question: str) -> str:
|
||||
"""Extract the key topic from a natural-language question.
|
||||
|
||||
Strips common question prefixes so that the palace search receives
|
||||
a clean keyword rather than noise words.
|
||||
|
||||
Examples:
|
||||
"about nightly watch failures" → "nightly watch failures"
|
||||
"what do you know about the CI pipeline?" → "CI pipeline"
|
||||
"""
|
||||
q = question.strip().rstrip("?").strip()
|
||||
# Remove leading question words
|
||||
for prefix in ("what do you know ", "tell me ", "do you know "):
|
||||
if q.lower().startswith(prefix):
|
||||
q = q[len(prefix):]
|
||||
for prep in _QUESTION_PREFIXES:
|
||||
if q.lower().startswith(prep):
|
||||
q = q[len(prep):]
|
||||
break
|
||||
return q or question.strip()
|
||||
@@ -1,99 +0,0 @@
|
||||
"""MemPalaceRoom — Evennia room typeclass backed by palace search.
|
||||
|
||||
When a character enters a MemPalaceRoom, the room's description is
|
||||
automatically refreshed from a live palace search for the room's
|
||||
topic keyword. This makes the room "alive" — its contents reflect
|
||||
what the fleet actually knows about that topic.
|
||||
|
||||
Phase 1 deliverable (see issue #1077).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from nexus.mempalace.searcher import MemPalaceUnavailable, search_memories
|
||||
from nexus.mempalace.config import FLEET_WING
|
||||
|
||||
try:
|
||||
from evennia import DefaultRoom as _EvDefaultRoom # type: ignore
|
||||
if _EvDefaultRoom is None:
|
||||
raise ImportError("evennia.DefaultRoom is None")
|
||||
DefaultRoom = _EvDefaultRoom
|
||||
except (ImportError, Exception):
|
||||
class DefaultRoom: # type: ignore # minimal stub for import/testing
|
||||
"""Stub for environments without Evennia installed."""
|
||||
|
||||
db: object = None
|
||||
key: str = ""
|
||||
|
||||
def return_appearance(self, looker): # noqa: D102
|
||||
return ""
|
||||
|
||||
def at_object_receive(self, moved_obj, source_location, **kwargs): # noqa: D102
|
||||
pass
|
||||
|
||||
|
||||
_PALACE_ROOM_HEADER = """|b═══════════════════════════════════════════════════|n
|
||||
|c Mind Palace — {room_name}|n
|
||||
|b═══════════════════════════════════════════════════|n"""
|
||||
|
||||
_PALACE_ROOM_FOOTER = """|b───────────────────────────────────────────────────|n
|
||||
|xType '|wrecall <query>|x' to search deeper.|n"""
|
||||
|
||||
|
||||
class MemPalaceRoom(DefaultRoom):
|
||||
"""An Evennia room whose description comes from the MemPalace.
|
||||
|
||||
Attributes (set via ``room.db.<attr>``):
|
||||
palace_topic (str): Search term used to populate the description.
|
||||
Defaults to the room's key.
|
||||
palace_wing (str): Wing to search. Defaults to fleet wing.
|
||||
palace_n_results (int): How many memories to show. Default 3.
|
||||
palace_room_filter (str): Optional room-name filter for the query.
|
||||
"""
|
||||
|
||||
def at_object_receive(self, moved_obj, source_location, **kwargs):
|
||||
"""Refresh palace content whenever someone enters."""
|
||||
super().at_object_receive(moved_obj, source_location, **kwargs)
|
||||
# Only refresh for player-controlled characters
|
||||
if hasattr(moved_obj, "account") and moved_obj.account:
|
||||
self._refresh_palace_desc(viewer=moved_obj)
|
||||
|
||||
def return_appearance(self, looker, **kwargs):
|
||||
"""Return description augmented with live palace memories."""
|
||||
self._refresh_palace_desc(viewer=looker)
|
||||
return super().return_appearance(looker, **kwargs)
|
||||
|
||||
# ── Internal helpers ──────────────────────────────────────────────────
|
||||
|
||||
def _refresh_palace_desc(self, viewer=None):
|
||||
"""Update ``self.db.desc`` from a fresh palace query."""
|
||||
topic = self.db.palace_topic or self.key or "general"
|
||||
wing = self.db.palace_wing or FLEET_WING
|
||||
n = self.db.palace_n_results or 3
|
||||
room_filter = self.db.palace_room_filter
|
||||
|
||||
try:
|
||||
results = search_memories(
|
||||
topic, wing=wing, room=room_filter, n_results=n
|
||||
)
|
||||
except MemPalaceUnavailable:
|
||||
self.db.desc = (
|
||||
f"[Palace unavailable — could not load memories for '{topic}'.]"
|
||||
)
|
||||
return
|
||||
|
||||
lines = [
|
||||
_PALACE_ROOM_HEADER.format(room_name=self.key),
|
||||
]
|
||||
|
||||
if results:
|
||||
for r in results:
|
||||
lines.append(f"|w{r.room}|n |x(score {r.score:.2f})|n")
|
||||
lines.append(f" {r.short(280)}")
|
||||
lines.append("")
|
||||
else:
|
||||
lines.append(f"|yNo memories found for topic '|w{topic}|y'.|n")
|
||||
lines.append("")
|
||||
|
||||
lines.append(_PALACE_ROOM_FOOTER)
|
||||
self.db.desc = "\n".join(lines)
|
||||
@@ -1,23 +0,0 @@
|
||||
"""nexus.mempalace — MemPalace integration for the Nexus fleet.
|
||||
|
||||
Public API for searching, configuring, and writing to MemPalace
|
||||
local vector memory. Designed to be imported by both the
|
||||
``evennia_mempalace`` plugin and any other harness component.
|
||||
|
||||
ChromaDB is an optional runtime dependency; the module degrades
|
||||
gracefully when it is not installed (tests, CI, environments that
|
||||
have not yet set up the palace).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from nexus.mempalace.config import MEMPALACE_PATH, FLEET_WING
|
||||
from nexus.mempalace.searcher import search_memories, add_memory, MemPalaceResult
|
||||
|
||||
__all__ = [
|
||||
"MEMPALACE_PATH",
|
||||
"FLEET_WING",
|
||||
"search_memories",
|
||||
"add_memory",
|
||||
"MemPalaceResult",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,46 +0,0 @@
|
||||
"""MemPalace configuration — paths and fleet settings.
|
||||
|
||||
All configuration is driven by environment variables so that
|
||||
different wizards on different VPSes can use the same code with
|
||||
their own palace directories.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# ── Palace path ──────────────────────────────────────────────────────────────
|
||||
# Default: ~/.mempalace/palace/ (local wizard palace)
|
||||
# Override via MEMPALACE_PATH env var (useful for fleet shared wing)
|
||||
_default = Path.home() / ".mempalace" / "palace"
|
||||
MEMPALACE_PATH: Path = Path(os.environ.get("MEMPALACE_PATH", str(_default)))
|
||||
|
||||
# ── Fleet shared wing ─────────────────────────────────────────────────────────
|
||||
# Path to the shared fleet palace on Alpha (used by --fleet searches)
|
||||
_fleet_default = Path("/var/lib/mempalace/fleet")
|
||||
FLEET_PALACE_PATH: Path = Path(
|
||||
os.environ.get("FLEET_PALACE_PATH", str(_fleet_default))
|
||||
)
|
||||
|
||||
# ── Wing name ─────────────────────────────────────────────────────────────────
|
||||
# Identifies this wizard's wing within a shared palace.
|
||||
# Populated from MEMPALACE_WING env var or falls back to system username.
|
||||
def _default_wing() -> str:
|
||||
import getpass
|
||||
return os.environ.get("MEMPALACE_WING", getpass.getuser())
|
||||
|
||||
FLEET_WING: str = _default_wing()
|
||||
|
||||
# ── Fleet rooms standard ─────────────────────────────────────────────────────
|
||||
# Canonical rooms every wizard must have (see docs/mempalace/rooms.yaml)
|
||||
CORE_ROOMS: list[str] = [
|
||||
"forge", # CI, builds, infra
|
||||
"hermes", # agent platform, gateway, CLI
|
||||
"nexus", # reports, docs, KT
|
||||
"issues", # tickets, backlog
|
||||
"experiments", # prototypes, spikes
|
||||
]
|
||||
|
||||
# ── ChromaDB collection name ──────────────────────────────────────────────────
|
||||
COLLECTION_NAME: str = os.environ.get("MEMPALACE_COLLECTION", "palace")
|
||||
@@ -1,200 +0,0 @@
|
||||
"""MemPalace search and write interface.
|
||||
|
||||
Wraps the ChromaDB-backed palace so that callers (Evennia commands,
|
||||
harness agents, MCP tools) do not need to know the storage details.
|
||||
|
||||
ChromaDB is imported lazily; if it is not installed the functions
|
||||
raise ``MemPalaceUnavailable`` with an informative message rather
|
||||
than crashing at import time.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from nexus.mempalace.config import (
|
||||
MEMPALACE_PATH,
|
||||
FLEET_PALACE_PATH,
|
||||
COLLECTION_NAME,
|
||||
)
|
||||
|
||||
|
||||
class MemPalaceUnavailable(RuntimeError):
|
||||
"""Raised when ChromaDB or the palace directory is not accessible."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class MemPalaceResult:
|
||||
"""A single memory hit returned by the searcher."""
|
||||
|
||||
text: str
|
||||
room: str
|
||||
wing: str
|
||||
score: float = 0.0
|
||||
source_file: str = ""
|
||||
metadata: dict = field(default_factory=dict)
|
||||
|
||||
def short(self, max_chars: int = 200) -> str:
|
||||
"""Return a truncated preview suitable for MUD output."""
|
||||
if len(self.text) <= max_chars:
|
||||
return self.text
|
||||
return self.text[:max_chars].rstrip() + "…"
|
||||
|
||||
|
||||
def _get_client(palace_path: Path):
|
||||
"""Return a ChromaDB persistent client, or raise MemPalaceUnavailable."""
|
||||
try:
|
||||
import chromadb # type: ignore
|
||||
except ImportError as exc:
|
||||
raise MemPalaceUnavailable(
|
||||
"ChromaDB is not installed. "
|
||||
"Run: pip install chromadb (or: pip install mempalace)"
|
||||
) from exc
|
||||
|
||||
if not palace_path.exists():
|
||||
raise MemPalaceUnavailable(
|
||||
f"Palace directory not found: {palace_path}\n"
|
||||
"Run 'mempalace mine' to initialise the palace."
|
||||
)
|
||||
|
||||
return chromadb.PersistentClient(path=str(palace_path))
|
||||
|
||||
|
||||
def search_memories(
|
||||
query: str,
|
||||
*,
|
||||
palace_path: Optional[Path] = None,
|
||||
wing: Optional[str] = None,
|
||||
room: Optional[str] = None,
|
||||
n_results: int = 5,
|
||||
) -> list[MemPalaceResult]:
|
||||
"""Search the palace for memories matching *query*.
|
||||
|
||||
Args:
|
||||
query: Natural-language search string.
|
||||
palace_path: Override the default palace path.
|
||||
wing: Filter results to a specific wizard's wing.
|
||||
room: Filter results to a specific room (e.g. ``"forge"``).
|
||||
n_results: Maximum number of results to return.
|
||||
|
||||
Returns:
|
||||
List of :class:`MemPalaceResult`, best-match first.
|
||||
|
||||
Raises:
|
||||
MemPalaceUnavailable: If ChromaDB is not installed or the palace
|
||||
directory does not exist.
|
||||
"""
|
||||
path = palace_path or MEMPALACE_PATH
|
||||
client = _get_client(path)
|
||||
|
||||
collection = client.get_or_create_collection(COLLECTION_NAME)
|
||||
|
||||
where: dict = {}
|
||||
if wing:
|
||||
where["wing"] = wing
|
||||
if room:
|
||||
where["room"] = room
|
||||
|
||||
kwargs: dict = {"query_texts": [query], "n_results": n_results}
|
||||
if where:
|
||||
kwargs["where"] = where
|
||||
|
||||
raw = collection.query(**kwargs)
|
||||
|
||||
results: list[MemPalaceResult] = []
|
||||
if not raw or not raw.get("documents"):
|
||||
return results
|
||||
|
||||
docs = raw["documents"][0]
|
||||
metas = raw.get("metadatas", [[]])[0] or [{}] * len(docs)
|
||||
distances = raw.get("distances", [[]])[0] or [0.0] * len(docs)
|
||||
|
||||
for doc, meta, dist in zip(docs, metas, distances):
|
||||
results.append(
|
||||
MemPalaceResult(
|
||||
text=doc,
|
||||
room=meta.get("room", "general"),
|
||||
wing=meta.get("wing", ""),
|
||||
score=float(1.0 - dist), # cosine similarity from distance
|
||||
source_file=meta.get("source_file", ""),
|
||||
metadata=meta,
|
||||
)
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def search_fleet(
|
||||
query: str,
|
||||
*,
|
||||
room: Optional[str] = None,
|
||||
n_results: int = 10,
|
||||
) -> list[MemPalaceResult]:
|
||||
"""Search the shared fleet palace (closets only, no raw drawers).
|
||||
|
||||
Args:
|
||||
query: Natural-language search string.
|
||||
room: Optional room filter (e.g. ``"issues"``).
|
||||
n_results: Maximum results.
|
||||
|
||||
Returns:
|
||||
List of :class:`MemPalaceResult` from all wings.
|
||||
"""
|
||||
return search_memories(
|
||||
query,
|
||||
palace_path=FLEET_PALACE_PATH,
|
||||
room=room,
|
||||
n_results=n_results,
|
||||
)
|
||||
|
||||
|
||||
def add_memory(
|
||||
text: str,
|
||||
*,
|
||||
room: str = "general",
|
||||
wing: Optional[str] = None,
|
||||
palace_path: Optional[Path] = None,
|
||||
source_file: str = "",
|
||||
extra_metadata: Optional[dict] = None,
|
||||
) -> str:
|
||||
"""Add a new memory drawer to the palace.
|
||||
|
||||
Args:
|
||||
text: The memory text to store.
|
||||
room: Target room (e.g. ``"hall_facts"``).
|
||||
wing: Wing name; defaults to :data:`~nexus.mempalace.config.FLEET_WING`.
|
||||
palace_path: Override the default palace path.
|
||||
source_file: Optional source file attribution.
|
||||
extra_metadata: Additional key/value metadata to store.
|
||||
|
||||
Returns:
|
||||
The generated document ID.
|
||||
|
||||
Raises:
|
||||
MemPalaceUnavailable: If ChromaDB is not installed or the palace
|
||||
directory does not exist.
|
||||
"""
|
||||
import uuid
|
||||
from nexus.mempalace.config import FLEET_WING
|
||||
|
||||
path = palace_path or MEMPALACE_PATH
|
||||
client = _get_client(path)
|
||||
collection = client.get_or_create_collection(COLLECTION_NAME)
|
||||
|
||||
doc_id = str(uuid.uuid4())
|
||||
metadata: dict = {
|
||||
"room": room,
|
||||
"wing": wing or FLEET_WING,
|
||||
"source_file": source_file,
|
||||
}
|
||||
if extra_metadata:
|
||||
metadata.update(extra_metadata)
|
||||
|
||||
collection.add(
|
||||
documents=[text],
|
||||
metadatas=[metadata],
|
||||
ids=[doc_id],
|
||||
)
|
||||
return doc_id
|
||||
@@ -1,132 +0,0 @@
|
||||
"""
|
||||
Morning Report Generator — runs at 0600 to compile overnight activity.
|
||||
Gathers: cycles executed, issues closed, PRs merged, commits pushed.
|
||||
Outputs a structured report for delivery to the main channel.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def generate_morning_report():
|
||||
"""Generate the morning report for the last 24h."""
|
||||
now = datetime.now(timezone.utc)
|
||||
since = now - timedelta(hours=24)
|
||||
since_str = since.strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
|
||||
repos = [
|
||||
"Timmy_Foundation/timmy-home",
|
||||
"Timmy_Foundation/timmy-config",
|
||||
"Timmy_Foundation/the-nexus",
|
||||
"Timmy_Foundation/hermes-agent",
|
||||
]
|
||||
|
||||
report = {
|
||||
"generated_at": now.strftime("%Y-%m-%d %H:%M UTC"),
|
||||
"period": f"Last 24h since {since_str}",
|
||||
"highlights": [],
|
||||
"blockers": [],
|
||||
"repos": {},
|
||||
}
|
||||
|
||||
token = open(os.path.expanduser("~/.config/gitea/token")).read().strip()
|
||||
|
||||
from urllib.request import Request, urlopen
|
||||
headers = {"Authorization": f"token {token}", "Accept": "application/json"}
|
||||
|
||||
for repo in repos:
|
||||
repo_data = {"closed_issues": 0, "merged_prs": 0, "recent_commits": 0}
|
||||
|
||||
# Closed issues in last 24h
|
||||
url = f"https://forge.alexanderwhitestone.com/api/v1/repos/{repo}/issues?state=closed&since={since_str}"
|
||||
try:
|
||||
resp = urlopen(Request(url, headers=headers), timeout=10)
|
||||
issues = json.loads(resp.read())
|
||||
repo_data["closed_issues"] = len(issues)
|
||||
for i in issues[:5]:
|
||||
report["highlights"].append(f"Closed {repo.split('/')[-1]}#{i['number']}: {i['title']}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Merged PRs
|
||||
url = f"https://forge.alexanderwhitestone.com/api/v1/repos/{repo}/pulls?state=closed"
|
||||
try:
|
||||
resp = urlopen(Request(url, headers=headers), timeout=10)
|
||||
prs = json.loads(resp.read())
|
||||
merged = [p for p in prs if p.get("merged")]
|
||||
repo_data["merged_prs"] = len(merged)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
report["repos"][repo.split("/")[-1]] = repo_data
|
||||
|
||||
# Check for stuck workers (blockers)
|
||||
worker_logs = list(Path("/tmp").glob("codeclaw-qwen-worker-*.log"))
|
||||
stuck = 0
|
||||
for wf in worker_logs:
|
||||
try:
|
||||
data = json.loads(wf.read_text().strip())
|
||||
if data.get("exit") != 0 and not data.get("has_work"):
|
||||
stuck += 1
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
pass
|
||||
if stuck > 0:
|
||||
report["blockers"].append(f"{stuck} worker(s) failed without producing work")
|
||||
|
||||
# Check dead letter queue
|
||||
dlq_path = Path(os.path.expanduser("~/.local/timmy/burn-state/dead-letter.json"))
|
||||
if dlq_path.exists():
|
||||
try:
|
||||
dlq = json.loads(dlq_path.read_text())
|
||||
if dlq:
|
||||
report["blockers"].append(f"{len(dlq)} action(s) in dead letter queue")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Checkpoint status
|
||||
cp_path = Path(os.path.expanduser("~/.local/timmy/burn-state/cycle-state.json"))
|
||||
if cp_path.exists():
|
||||
try:
|
||||
cp = json.loads(cp_path.read_text())
|
||||
if cp.get("status") == "in-progress":
|
||||
ts = cp.get("timestamp", "")
|
||||
if ts and datetime.fromisoformat(ts) < since:
|
||||
report["blockers"].append(f"Stale checkpoint: {cp.get('action')} since {ts}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Summary
|
||||
total_closed = sum(r["closed_issues"] for r in report["repos"].values())
|
||||
total_merged = sum(r["merged_prs"] for r in report["repos"].values())
|
||||
|
||||
print(f"=== MORNING REPORT {report['generated_at']} ===")
|
||||
print(f"Period: {report['period']}")
|
||||
print(f"Issues closed: {total_closed}")
|
||||
print(f"PRs merged: {total_merged}")
|
||||
print("")
|
||||
if report["highlights"]:
|
||||
print("HIGHLIGHTS:")
|
||||
for h in report["highlights"]:
|
||||
print(f" + {h}")
|
||||
if report["blockers"]:
|
||||
print("BLOCKERS:")
|
||||
for b in report["blockers"]:
|
||||
print(f" - {b}")
|
||||
if not report["highlights"] and not report["blockers"]:
|
||||
print("No significant activity or blockers detected.")
|
||||
print("")
|
||||
|
||||
# Save report
|
||||
report_dir = Path(os.path.expanduser("~/.local/timmy/reports"))
|
||||
report_dir.mkdir(parents=True, exist_ok=True)
|
||||
report_file = report_dir / f"morning-{now.strftime('%Y-%m-%d')}.json"
|
||||
report_file.write_text(json.dumps(report, indent=2))
|
||||
print(f"Report saved: {report_file}")
|
||||
return report
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
generate_morning_report()
|
||||
@@ -1,114 +0,0 @@
|
||||
"""
|
||||
Retry logic and error recovery for burn-mode operations.
|
||||
Provides: retry decorator, cycle state tracking, dead letter queue.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
# --- Configuration ---
|
||||
STATE_DIR = Path(os.path.expanduser("~/.local/timmy/burn-state"))
|
||||
STATE_FILE = STATE_DIR / "cycle-state.json"
|
||||
DEAD_LETTER_FILE = STATE_DIR / "dead-letter.json"
|
||||
MAX_RETRIES = 3
|
||||
BASE_DELAY = 2 # seconds
|
||||
|
||||
|
||||
def _ensure_dir():
|
||||
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def retry(max_retries=MAX_RETRIES, base_delay=BASE_DELAY, exceptions=(Exception,)):
|
||||
"""Retry decorator with exponential backoff."""
|
||||
def decorator(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
last_exc = None
|
||||
for attempt in range(1, max_retries + 1):
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
except exceptions as exc:
|
||||
last_exc = exc
|
||||
if attempt < max_retries:
|
||||
delay = base_delay * (2 ** (attempt - 1))
|
||||
print(f" [RETRY] {fn.__name__} attempt {attempt}/{max_retries} failed: {exc}")
|
||||
print(f" [RETRY] waiting {delay}s...")
|
||||
time.sleep(delay)
|
||||
else:
|
||||
print(f" [FAIL] {fn.__name__} failed after {max_retries} attempts: {exc}")
|
||||
dead_letter(fn.__name__, args, exc)
|
||||
return None # All retries exhausted
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def dead_letter(fn_name, args, exc):
|
||||
"""Record a failed action to the dead letter queue."""
|
||||
_ensure_dir()
|
||||
entry = {
|
||||
"function": fn_name,
|
||||
"args": str(args)[:500],
|
||||
"error": str(exc),
|
||||
"traceback": traceback.format_exc()[:1000],
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
dlq = []
|
||||
if DEAD_LETTER_FILE.exists():
|
||||
try:
|
||||
dlq = json.loads(DEAD_LETTER_FILE.read_text())
|
||||
except json.JSONDecodeError:
|
||||
dlq = []
|
||||
dlq.append(entry)
|
||||
DEAD_LETTER_FILE.write_text(json.dumps(dlq, indent=2))
|
||||
|
||||
|
||||
def save_checkpoint(action, repo=None, issue=None, detail=None):
|
||||
"""Save the current cycle action for crash recovery."""
|
||||
_ensure_dir()
|
||||
state = {
|
||||
"action": action,
|
||||
"repo": repo,
|
||||
"issue": issue,
|
||||
"detail": detail or "",
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"status": "in-progress",
|
||||
}
|
||||
STATE_FILE.write_text(json.dumps(state, indent=2))
|
||||
|
||||
|
||||
def clear_checkpoint():
|
||||
"""Clear the checkpoint after successful completion."""
|
||||
_ensure_dir()
|
||||
state = {
|
||||
"action": None,
|
||||
"timestamp": datetime.now(timezone.utc).isoformat(),
|
||||
"status": "complete",
|
||||
}
|
||||
STATE_FILE.write_text(json.dumps(state, indent=2))
|
||||
|
||||
|
||||
def load_checkpoint():
|
||||
"""Load the last checkpoint for crash recovery."""
|
||||
if not STATE_FILE.exists():
|
||||
return None
|
||||
try:
|
||||
return json.loads(STATE_FILE.read_text())
|
||||
except json.JSONDecodeError:
|
||||
return None
|
||||
|
||||
|
||||
def get_dead_letter_summary():
|
||||
"""Return a human-readable summary of the dead letter queue."""
|
||||
if not DEAD_LETTER_FILE.exists():
|
||||
return "Dead letter queue: empty"
|
||||
try:
|
||||
dlq = json.loads(DEAD_LETTER_FILE.read_text())
|
||||
lines = [f"Dead letter queue: {len(dlq)} failed actions"]
|
||||
for entry in dlq[-10:]: # Show last 10
|
||||
lines.append(f" - {entry['function']}: {entry['error'][:100]} at {entry['timestamp']}")
|
||||
return "\n".join(lines)
|
||||
except json.JSONDecodeError:
|
||||
return "Dead letter queue: corrupt"
|
||||
@@ -1,141 +0,0 @@
|
||||
# Operation Get A Job — Master Plan
|
||||
|
||||
## Mission Statement
|
||||
|
||||
Monetize the engineering capability of a production AI agent fleet to fund infrastructure expansion. Alexander Whitestone handles the last human mile — meetings, contracts, and client relationships. The fleet handles everything else.
|
||||
|
||||
## The Core Thesis
|
||||
|
||||
We are not a solo freelancer. We are a firm with a human principal and a fleet of five autonomous AI engineers that ship production code 24/7. This is a force multiplier that no traditional consultancy can match.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Foundation (Week 1-2)
|
||||
|
||||
### Entity & Legal
|
||||
- [ ] Form Wyoming LLC (see entity-setup.md)
|
||||
- [ ] Open Mercury business banking account
|
||||
- [ ] Obtain EIN from IRS (online, instant)
|
||||
- [ ] Secure E&O insurance policy (~$150/mo)
|
||||
- [ ] Set up invoicing (Stripe or Invoice Ninja)
|
||||
- [ ] Draft master services agreement (MSA) template
|
||||
- [ ] Draft statement of work (SOW) template
|
||||
|
||||
### Brand & Presence
|
||||
- [ ] Register domain (alexanderwhitestone.com or firm name)
|
||||
- [ ] Deploy portfolio site (static site from portfolio.md content)
|
||||
- [ ] Set up professional email (hello@domain)
|
||||
- [ ] Create LinkedIn company page
|
||||
- [ ] Create Upwork agency profile
|
||||
- [ ] Prepare 60-second elevator pitch
|
||||
|
||||
### Internal Readiness
|
||||
- [ ] Document fleet capabilities inventory
|
||||
- [ ] Establish client onboarding workflow
|
||||
- [ ] Set up project tracking (Gitea issues or similar)
|
||||
- [ ] Create secure client communication channels
|
||||
- [ ] Test end-to-end delivery: inquiry → proposal → delivery → invoice
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Pipeline Building (Week 2-4)
|
||||
|
||||
### Outreach Channels (Priority Order)
|
||||
1. **Upwork** — Post agency profile, bid on 5-10 relevant jobs/week
|
||||
2. **LinkedIn** — Direct outreach to CTOs/VPs Eng at Series A-C startups
|
||||
3. **Twitter/X** — Ship in public, engage AI/DevOps communities
|
||||
4. **Discord** — AI builder communities, offer value before pitching
|
||||
5. **Direct Email** — Targeted cold outreach to companies with known pain points
|
||||
6. **Toptal/Gun.io** — Apply to premium freelance networks
|
||||
7. **Referrals** — Ask every contact for warm intros
|
||||
|
||||
### Target Client Profiles
|
||||
- **Startup CTO** — Needs infrastructure but can't hire a full platform team
|
||||
- **AI Company** — Needs agent security, guardrails, or fleet management
|
||||
- **Enterprise Innovation Lab** — Wants to pilot autonomous agent workflows
|
||||
- **DevOps-Light Company** — Has engineers but no CI/CD, no automation
|
||||
- **Crypto/Web3 Project** — Needs sovereign infrastructure, self-hosted tooling
|
||||
|
||||
### Weekly Cadence
|
||||
- Monday: 10 new outreach messages
|
||||
- Tuesday-Thursday: Follow up on open threads, deliver proposals
|
||||
- Friday: Review pipeline, update portfolio, ship public content
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: First Revenue (Week 3-6)
|
||||
|
||||
### Target: $5k-15k first month
|
||||
- Land 1-2 Tier 3 engagements (automation/research, $5-10k each)
|
||||
- Use these as case studies for Tier 1/2 upsells
|
||||
- Deliver fast, over-deliver on quality
|
||||
|
||||
### Pricing Strategy
|
||||
- Lead with project pricing (clients prefer predictability)
|
||||
- Hourly only for advisory/consulting calls
|
||||
- Always bill as the firm, never as "me"
|
||||
- Net-15 payment terms, 50% upfront for new clients
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Scale (Month 2-3)
|
||||
|
||||
### Revenue Target: $20-40k/month
|
||||
- Move toward retainer relationships ($5-15k/mo per client)
|
||||
- Build recurring revenue base
|
||||
- Hire subcontractors for overflow (other AI-native engineers)
|
||||
- Invest profits in hardware (GPUs, additional VPS capacity)
|
||||
|
||||
### Reinvestment Priority
|
||||
1. More compute (local inference capacity)
|
||||
2. Additional agent instances
|
||||
3. Premium tooling subscriptions
|
||||
4. Marketing/content production
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Moat Building (Month 3-6)
|
||||
|
||||
- Publish open-source tools from client work (with permission)
|
||||
- Build public reputation through conference talks / podcast appearances
|
||||
- Develop proprietary frameworks that lock in competitive advantage
|
||||
- Establish the firm as THE go-to for autonomous agent infrastructure
|
||||
|
||||
---
|
||||
|
||||
## Key Metrics to Track
|
||||
|
||||
| Metric | Week 1 | Month 1 | Month 3 |
|
||||
|--------|--------|---------|---------|
|
||||
| Outreach sent | 20 | 80+ | 200+ |
|
||||
| Proposals sent | 3 | 10+ | 25+ |
|
||||
| Clients signed | 0 | 2-3 | 5-8 |
|
||||
| Revenue | $0 | $10-15k | $30-50k |
|
||||
| Pipeline value | $10k | $50k+ | $150k+ |
|
||||
|
||||
---
|
||||
|
||||
## Decision Rules
|
||||
|
||||
- Any project under $2k: decline (not worth context switching)
|
||||
- Any project requiring on-site: decline unless >$500/hr
|
||||
- Any project with unclear scope: require paid discovery phase first
|
||||
- Any client who won't sign MSA: walk away
|
||||
- Any client who wants to hire "just the human": explain the model or walk
|
||||
|
||||
---
|
||||
|
||||
## Files in This Package
|
||||
|
||||
1. `README.md` — This file (master plan)
|
||||
2. `entity-setup.md` — Wyoming LLC formation checklist
|
||||
3. `service-offerings.md` — What we sell (3 tiers + packages)
|
||||
4. `portfolio.md` — What the fleet has built
|
||||
5. `outreach-templates.md` — 5 cold outreach templates
|
||||
6. `proposal-template.md` — Professional proposal template
|
||||
7. `rate-card.md` — Detailed rate card
|
||||
|
||||
---
|
||||
|
||||
*Last updated: April 2026*
|
||||
*Operation Get A Job v1.0*
|
||||
@@ -1,203 +0,0 @@
|
||||
# Entity Setup — Wyoming LLC Formation Checklist
|
||||
|
||||
## Why Wyoming?
|
||||
|
||||
- No state income tax
|
||||
- Strong privacy protections (no public member disclosure required)
|
||||
- Low annual fees ($60/year registered agent + $60 annual report)
|
||||
- Business-friendly courts
|
||||
- Fast online filing
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Choose Your LLC Name
|
||||
|
||||
- [ ] Decide on firm name (suggestions below)
|
||||
- [ ] Search Wyoming Secretary of State name availability
|
||||
- Link: https://wyobiz.wyo.gov/Business/FilingSearch.aspx
|
||||
- [ ] Ensure matching domain is available
|
||||
|
||||
### Name Suggestions
|
||||
- Whitestone Engineering LLC
|
||||
- Whitestone Labs LLC
|
||||
- Hermes Systems LLC
|
||||
- Whitestone & Fleet LLC
|
||||
- Sovereign Stack LLC
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Appoint a Registered Agent
|
||||
|
||||
You need a Wyoming registered agent (physical address in WY for legal mail).
|
||||
|
||||
### Recommended Registered Agent Services
|
||||
- **Wyoming Registered Agent LLC** — $60/year (cheapest, reliable)
|
||||
- Link: https://www.wyomingagents.com
|
||||
- **Northwest Registered Agent** — $125/year (premium service)
|
||||
- Link: https://www.northwestregisteredagent.com
|
||||
- **ZenBusiness** — $199/year (bundled with formation)
|
||||
- Link: https://www.zenbusiness.com
|
||||
|
||||
**Recommendation:** Wyoming Registered Agent LLC at $60/year. No frills, gets the job done.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: File Articles of Organization
|
||||
|
||||
- [ ] File online with Wyoming Secretary of State
|
||||
- Link: https://wyobiz.wyo.gov/Business/FilingSearch.aspx
|
||||
- Click "File a New Business"
|
||||
- [ ] Filing fee: **$100** (online) or $102 (mail)
|
||||
- [ ] Processing time: 1-2 business days (online), 2-3 weeks (mail)
|
||||
|
||||
### Information Needed
|
||||
- LLC name
|
||||
- Registered agent name and address
|
||||
- Organizer name and address (can be the registered agent)
|
||||
- Management structure: Member-managed (choose this)
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Get Your EIN (Employer Identification Number)
|
||||
|
||||
- [ ] Apply online with the IRS (free, instant)
|
||||
- Link: https://www.irs.gov/businesses/small-businesses-self-employed/apply-for-an-employer-identification-number-ein-online
|
||||
- [ ] Available Monday-Friday, 7am-10pm Eastern
|
||||
- [ ] You'll get your EIN immediately upon completion
|
||||
- [ ] Download and save the confirmation letter (CP 575)
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Draft Operating Agreement
|
||||
|
||||
- [ ] Create a single-member LLC operating agreement
|
||||
- [ ] This is not filed with the state but is essential for:
|
||||
- Bank account opening
|
||||
- Liability protection (piercing the corporate veil prevention)
|
||||
- Tax elections
|
||||
|
||||
### Free Template Sources
|
||||
- Northwest Registered Agent provides one free
|
||||
- LawDepot: https://www.lawdepot.com
|
||||
- Or have an attorney draft one ($300-500)
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Open Business Bank Account
|
||||
|
||||
### Recommended: Mercury Banking
|
||||
- Link: https://mercury.com
|
||||
- [ ] Apply online (takes 1-3 business days)
|
||||
- [ ] Documents needed:
|
||||
- EIN confirmation (CP 575)
|
||||
- Articles of Organization
|
||||
- Operating Agreement
|
||||
- Government-issued ID
|
||||
- [ ] Benefits:
|
||||
- No monthly fees
|
||||
- No minimum balance
|
||||
- API access for automation
|
||||
- Virtual debit cards
|
||||
- Built-in invoicing
|
||||
- Treasury for idle cash
|
||||
|
||||
### Alternative: Relay Financial
|
||||
- Link: https://relayfi.com
|
||||
- Similar features, also startup-friendly
|
||||
|
||||
---
|
||||
|
||||
## Step 7: Set Up Invoicing & Payments
|
||||
|
||||
### Option A: Stripe (Recommended)
|
||||
- [ ] Create Stripe account linked to Mercury
|
||||
- [ ] Set up Stripe Invoicing
|
||||
- [ ] Accept ACH (lower fees) and credit cards
|
||||
- Fees: 2.9% + 30¢ (card), 0.8% capped at $5 (ACH)
|
||||
|
||||
### Option B: Invoice Ninja (Self-Hosted)
|
||||
- [ ] Deploy on your VPS (you already have the infrastructure)
|
||||
- [ ] Connect to Stripe for payment processing
|
||||
- [ ] Full control, no SaaS fees
|
||||
|
||||
---
|
||||
|
||||
## Step 8: Get E&O Insurance (Errors & Omissions)
|
||||
|
||||
This protects you if a client claims your work caused them harm.
|
||||
|
||||
### Recommended Providers
|
||||
- **Hiscox** — ~$100-150/month for tech consulting
|
||||
- Link: https://www.hiscox.com
|
||||
- **Hartford** — Similar pricing
|
||||
- Link: https://www.thehartford.com
|
||||
- **Embroker** — Tech-focused, may be cheaper
|
||||
- Link: https://www.embroker.com
|
||||
|
||||
### Coverage to Get
|
||||
- [ ] Professional Liability / E&O: $1M per occurrence / $2M aggregate
|
||||
- [ ] General Liability: $1M per occurrence / $2M aggregate
|
||||
- [ ] Cyber Liability: Optional but recommended given the AI work
|
||||
|
||||
**Budget: ~$150/month ($1,800/year)**
|
||||
|
||||
---
|
||||
|
||||
## Step 9: Tax Setup
|
||||
|
||||
- [ ] Elect S-Corp taxation (Form 2553) if revenue exceeds ~$40k/year
|
||||
- Saves on self-employment tax
|
||||
- Must pay yourself "reasonable salary" via payroll
|
||||
- Use Gusto ($40/mo) or similar for payroll
|
||||
- [ ] Set aside 30% of revenue for taxes quarterly
|
||||
- [ ] File estimated quarterly taxes (Form 1040-ES)
|
||||
- [ ] Get a CPA familiar with LLCs ($200-500/year for filing)
|
||||
|
||||
### Recommended CPA Services
|
||||
- Bench.co — Bookkeeping + tax filing ($300-500/mo)
|
||||
- Collective.com — Designed for solo businesses ($349/mo, includes S-Corp)
|
||||
- Local CPA — Shop around, $1-2k/year for everything
|
||||
|
||||
---
|
||||
|
||||
## Step 10: Professional Presence
|
||||
|
||||
- [ ] Get a business phone number (Google Voice — free, or OpenPhone — $15/mo)
|
||||
- [ ] Set up professional email (Google Workspace $6/mo or self-hosted)
|
||||
- [ ] Order business cards (optional, Moo.com or similar)
|
||||
- [ ] Create LinkedIn company page
|
||||
- [ ] Update personal LinkedIn with firm title (Managing Partner / Principal)
|
||||
|
||||
---
|
||||
|
||||
## Total Startup Costs Estimate
|
||||
|
||||
| Item | Cost |
|
||||
|------|------|
|
||||
| Wyoming LLC filing | $100 |
|
||||
| Registered agent (annual) | $60 |
|
||||
| EIN | Free |
|
||||
| Mercury bank account | Free |
|
||||
| E&O insurance (first month) | $150 |
|
||||
| Domain + email | $12 + $6/mo |
|
||||
| **Total to launch** | **~$330** |
|
||||
| **Monthly ongoing** | **~$160/mo** |
|
||||
|
||||
---
|
||||
|
||||
## Timeline
|
||||
|
||||
| Day | Action |
|
||||
|-----|--------|
|
||||
| Day 1 | File LLC + order registered agent |
|
||||
| Day 2-3 | Receive LLC confirmation |
|
||||
| Day 3 | Get EIN (same day) |
|
||||
| Day 3 | Apply for Mercury account |
|
||||
| Day 4-5 | Mercury approved |
|
||||
| Day 5 | Set up Stripe, get insurance quote |
|
||||
| Day 6-7 | Insurance bound, invoicing live |
|
||||
| **Day 7** | **Ready to bill clients** |
|
||||
|
||||
---
|
||||
|
||||
*You can go from zero to invoicing in under a week. Don't let entity setup be a blocker — you can start conversations immediately and have the entity ready before you need to send the first invoice.*
|
||||
@@ -1,216 +0,0 @@
|
||||
# Outreach Templates
|
||||
|
||||
## How to Use These Templates
|
||||
|
||||
- Replace everything in [BRACKETS] with your specific details
|
||||
- Keep messages concise — busy people don't read walls of text
|
||||
- Always lead with value, not credentials
|
||||
- Follow up once after 3-5 days if no response, then move on
|
||||
- Track all outreach in a spreadsheet (date, platform, response, status)
|
||||
|
||||
---
|
||||
|
||||
## Template 1: Upwork Proposal
|
||||
|
||||
**Use for:** Responding to job postings related to AI agents, DevOps, automation, LLM infrastructure
|
||||
|
||||
---
|
||||
|
||||
Hi [CLIENT NAME],
|
||||
|
||||
I read your posting about [SPECIFIC REQUIREMENT FROM JOB POST]. This is exactly what my firm does day in, day out.
|
||||
|
||||
We're a small engineering firm that runs a fleet of five autonomous AI agents in production. Not demos — real agents running as systemd services, shipping code to a 43-repo forge, executing 15-minute autonomous work cycles 24/7. We built the orchestration framework (Hermes), the security layer, and the local LLM inference stack ourselves.
|
||||
|
||||
For your project specifically:
|
||||
|
||||
- [SPECIFIC THING THEY NEED #1] — We've built [RELEVANT THING YOU'VE DONE]
|
||||
- [SPECIFIC THING THEY NEED #2] — We can deliver this using [YOUR APPROACH]
|
||||
- [SPECIFIC THING THEY NEED #3] — Our timeline estimate is [X WEEKS]
|
||||
|
||||
I'd suggest a [STARTER/PROFESSIONAL/CUSTOM] engagement at [$PRICE] with [TIMELINE]. Happy to do a 30-minute call to scope it properly.
|
||||
|
||||
Portfolio: [YOUR PORTFOLIO URL]
|
||||
|
||||
Best,
|
||||
Alexander Whitestone
|
||||
Whitestone Engineering
|
||||
|
||||
---
|
||||
|
||||
## Template 2: LinkedIn Direct Message
|
||||
|
||||
**Use for:** Cold outreach to CTOs, VPs of Engineering, Heads of AI/ML at startups (Series A-C)
|
||||
|
||||
---
|
||||
|
||||
Hi [FIRST NAME],
|
||||
|
||||
I noticed [COMPANY] is [SPECIFIC OBSERVATION — hiring for AI roles / launching an AI feature / scaling infrastructure]. Congrats on [RECENT MILESTONE IF APPLICABLE].
|
||||
|
||||
Quick context: I run an engineering firm with a fleet of autonomous AI agents that build production infrastructure. We handle agent deployment, security hardening, and automation for companies that want AI systems that actually work in production, not just in demos.
|
||||
|
||||
We recently [RELEVANT ACCOMPLISHMENT — e.g., "deployed a multi-agent fleet with 3,000+ tests and local LLM inference" or "built a conscience validation system for AI safety"].
|
||||
|
||||
Would it be useful to chat for 15 minutes about [SPECIFIC PAIN POINT YOU THINK THEY HAVE]? No pitch — just want to see if there's a fit.
|
||||
|
||||
— Alexander
|
||||
|
||||
---
|
||||
|
||||
## Template 3: Twitter/X DM or Reply
|
||||
|
||||
**Use for:** Engaging with people posting about AI agent challenges, DevOps pain, or LLM infrastructure problems
|
||||
|
||||
---
|
||||
|
||||
### Version A: Reply to a post about AI agent problems
|
||||
|
||||
[THEIR NAME] — we solved this exact problem. We run 5 autonomous agents in production (systemd services, 15-min burn cycles, persistent memory). The key insight was [SPECIFIC TECHNICAL INSIGHT RELEVANT TO THEIR POST].
|
||||
|
||||
Happy to share our approach if useful. We built an open orchestration framework that handles [RELEVANT CAPABILITY].
|
||||
|
||||
---
|
||||
|
||||
### Version B: DM after engaging with their content
|
||||
|
||||
Hey [FIRST NAME] — been following your posts on [TOPIC]. Really resonated with your point about [SPECIFIC THING THEY SAID].
|
||||
|
||||
We're running a production fleet of AI agents and have solved a lot of the problems you're describing. Built our own framework (Hermes) for agent orchestration, security, and multi-platform deployment.
|
||||
|
||||
Not trying to sell anything — just think there might be useful knowledge exchange. Down to chat?
|
||||
|
||||
---
|
||||
|
||||
### Version C: Cold DM to potential client
|
||||
|
||||
Hey [FIRST NAME] — saw [COMPANY] is working on [WHAT THEY'RE BUILDING]. My firm builds production AI agent infrastructure — fleet orchestration, local LLM stacks, agent security. We run 5 agents 24/7 on our own infra.
|
||||
|
||||
Would love to show you what we've built. Might save your team months. 15 min call?
|
||||
|
||||
---
|
||||
|
||||
## Template 4: Discord Community Post / DM
|
||||
|
||||
**Use for:** AI builder communities, DevOps communities, indie hacker communities
|
||||
|
||||
---
|
||||
|
||||
### Version A: Community post (value-first)
|
||||
|
||||
Been running a fleet of 5 autonomous AI agents in production for a while now, wanted to share some lessons learned:
|
||||
|
||||
1. **Persistent memory matters more than model quality.** An agent with good memory and a decent model outperforms a genius model with no context.
|
||||
|
||||
2. **Security can't be an afterthought.** We built a conscience validation layer after discovering [VAGUE REFERENCE TO REAL INCIDENT]. Now every agent action goes through guardrails.
|
||||
|
||||
3. **Local inference is viable for most tasks.** We run Gemma via Ollama for [X]% of agent operations. Cloud APIs are the fallback, not the default.
|
||||
|
||||
4. **Systemd > Docker for single-machine agent fleets.** Hot take, but the simplicity wins when you're managing 5 agents on one box.
|
||||
|
||||
Full system: 43 repos, 3,000+ tests, multi-platform gateway (Telegram/Discord/Slack), webhook CI/CD.
|
||||
|
||||
Happy to answer questions or go deeper on any of these.
|
||||
|
||||
---
|
||||
|
||||
### Version B: DM to someone asking for help
|
||||
|
||||
Hey! Saw your question about [THEIR QUESTION]. We've built exactly this — [BRIEF DESCRIPTION OF YOUR RELEVANT SYSTEM].
|
||||
|
||||
The short answer: [HELPFUL TECHNICAL ANSWER].
|
||||
|
||||
If you want, I can share more details about our setup. We also do this professionally if you ever need hands-on help deploying something similar.
|
||||
|
||||
---
|
||||
|
||||
## Template 5: Direct Cold Email
|
||||
|
||||
**Use for:** Targeted outreach to companies you've researched that have a clear need
|
||||
|
||||
---
|
||||
|
||||
**Subject:** [COMPANY]'s [SPECIFIC CHALLENGE] — solved it, can show you how
|
||||
|
||||
Hi [FIRST NAME],
|
||||
|
||||
I'm Alexander Whitestone, principal at Whitestone Engineering. We build production AI agent infrastructure — the kind that runs 24/7, ships real code, and doesn't break.
|
||||
|
||||
I'm reaching out because [SPECIFIC REASON — e.g., "I saw your job posting for a platform engineer to build AI agent tooling" / "your blog post about scaling LLM operations mentioned exactly the problems we solve" / "a mutual contact mentioned you're building an AI agent product"].
|
||||
|
||||
**What we've built (and can build for you):**
|
||||
|
||||
- A fleet of 5 autonomous AI agents running as systemd services, completing 15-minute autonomous work cycles
|
||||
- Custom orchestration framework with persistent memory, skills system, and multi-platform gateway
|
||||
- Local LLM inference stack (zero external API dependency for core operations)
|
||||
- Agent security layer with jailbreak resistance and conscience validation (3,000+ tests)
|
||||
- Self-hosted forge with 43 repos and webhook-driven CI/CD
|
||||
|
||||
**Why this matters for [COMPANY]:**
|
||||
|
||||
[2-3 sentences about how your capabilities map to their specific needs. Be concrete.]
|
||||
|
||||
I'm not looking to send you a generic pitch deck. I'd rather spend 20 minutes on a call understanding your specific situation and telling you honestly whether we can help.
|
||||
|
||||
Available [DAY/TIME] or [DAY/TIME] this week. Or just reply with what works.
|
||||
|
||||
Best,
|
||||
Alexander Whitestone
|
||||
Principal, Whitestone Engineering
|
||||
[EMAIL]
|
||||
[PHONE — optional]
|
||||
[PORTFOLIO URL]
|
||||
|
||||
---
|
||||
|
||||
## Follow-Up Templates
|
||||
|
||||
### Follow-Up #1 (3-5 days after initial outreach)
|
||||
|
||||
Hi [FIRST NAME],
|
||||
|
||||
Following up on my note from [DAY]. I know inboxes are brutal.
|
||||
|
||||
The one-line version: we build production AI agent infrastructure and I think we can help [COMPANY] with [SPECIFIC THING].
|
||||
|
||||
Worth a 15-minute chat? If not, no worries — happy to stay in touch for when the timing is better.
|
||||
|
||||
— Alexander
|
||||
|
||||
---
|
||||
|
||||
### Follow-Up #2 (7-10 days after Follow-Up #1, final attempt)
|
||||
|
||||
Hi [FIRST NAME],
|
||||
|
||||
Last note from me on this — don't want to be that person.
|
||||
|
||||
If [SPECIFIC CHALLENGE] is still on your radar, we're here. If the timing isn't right, totally understand.
|
||||
|
||||
Either way, I write about AI agent operations occasionally. Happy to share if that's useful.
|
||||
|
||||
Best,
|
||||
Alexander
|
||||
|
||||
---
|
||||
|
||||
## Outreach Tracking Spreadsheet Columns
|
||||
|
||||
| Date | Platform | Contact Name | Company | Message Type | Response? | Follow-Up Date | Status | Notes |
|
||||
|------|----------|-------------|---------|-------------|-----------|----------------|--------|-------|
|
||||
| | | | | | | | | |
|
||||
|
||||
### Status Options
|
||||
- Sent
|
||||
- Responded — Interested
|
||||
- Responded — Not Now
|
||||
- Responded — Not Interested
|
||||
- Meeting Scheduled
|
||||
- Proposal Sent
|
||||
- Won
|
||||
- Lost
|
||||
- No Response
|
||||
|
||||
---
|
||||
|
||||
*Remember: outreach is a numbers game. Aim for 10 quality touches per week minimum. One in ten will respond. One in three responses will take a meeting. One in three meetings will become a client. That means ~100 outreach messages to land ~1 client. Adjust volume accordingly.*
|
||||
@@ -1,182 +0,0 @@
|
||||
# Portfolio — What We've Built
|
||||
|
||||
## About Whitestone Engineering
|
||||
|
||||
We are a human-led engineering firm augmented by a fleet of five autonomous AI agents. Our principal, Alexander Whitestone, architects systems and directs operations. The fleet — Allegro, Adagio, Ezra, Bezalel, and Bilbobagginshire — builds, tests, and ships production code autonomously.
|
||||
|
||||
This is not a demo. This is not a prototype. Everything below is running in production.
|
||||
|
||||
---
|
||||
|
||||
## The Fleet
|
||||
|
||||
### Agent Roster
|
||||
|
||||
| Agent | Role | Specialization |
|
||||
|-------|------|---------------|
|
||||
| **Allegro** | Lead Engineer | Fast-paced development, feature shipping |
|
||||
| **Adagio** | Quality & Review | Careful analysis, code review, testing |
|
||||
| **Ezra** | Research & Analysis | Technical research, intelligence synthesis |
|
||||
| **Bezalel** | Infrastructure | System administration, deployment, DevOps |
|
||||
| **Bilbobagginshire** | Exploration | Novel approaches, creative problem-solving |
|
||||
|
||||
All agents run as systemd services on dedicated infrastructure, operating in autonomous 15-minute burn cycles around the clock.
|
||||
|
||||
---
|
||||
|
||||
## Production Systems
|
||||
|
||||
### 1. Hermes Agent Framework
|
||||
**Custom-built multi-agent orchestration platform**
|
||||
|
||||
- Persistent memory system — agents retain context across sessions
|
||||
- Skills framework — modular capability system for agent specialization
|
||||
- Cron scheduling — autonomous task execution on configurable intervals
|
||||
- Multi-platform gateway — single agent, multiple communication channels:
|
||||
- Telegram
|
||||
- Discord
|
||||
- Slack
|
||||
- Custom webhook endpoints
|
||||
- Burn-mode operations — 15-minute autonomous work cycles
|
||||
- Inter-agent communication and task delegation
|
||||
|
||||
**Tech:** Python, systemd, SQLite/PostgreSQL, REST APIs
|
||||
|
||||
---
|
||||
|
||||
### 2. Self-Hosted Code Forge (Gitea)
|
||||
**Sovereign development infrastructure**
|
||||
|
||||
- 43 active repositories
|
||||
- 16 organization members (human + AI agents)
|
||||
- Full Git workflow with branch protection and review
|
||||
- Webhook-driven CI/CD pipeline triggering automated builds and deploys
|
||||
- Issue tracking integrated with agent task assignment
|
||||
- Running at forge.alexanderwhitestone.com
|
||||
|
||||
**Tech:** Gitea, Git, webhooks, nginx, Let's Encrypt
|
||||
|
||||
---
|
||||
|
||||
### 3. Agent Security & Conscience System
|
||||
**Production AI safety infrastructure**
|
||||
|
||||
- Conscience validation layer — ethical guardrails enforced at runtime
|
||||
- Jailbreak resistance — tested against known attack vectors
|
||||
- Crisis detection — automated identification and escalation of safety events
|
||||
- Audit logging — full traceability of agent decisions and actions
|
||||
- 3,000+ automated tests covering security and behavioral boundaries
|
||||
|
||||
**Tech:** Python, custom validation framework, pytest
|
||||
|
||||
---
|
||||
|
||||
### 4. Local LLM Inference Stack
|
||||
**Sovereign AI — no external API dependency**
|
||||
|
||||
- Ollama deployment with Gemma model family
|
||||
- Local inference for sensitive operations
|
||||
- Fallback architecture — local models for availability, cloud for capability
|
||||
- Reduced operational costs vs. pure API consumption
|
||||
- Full data sovereignty — nothing leaves the infrastructure
|
||||
|
||||
**Tech:** Ollama, Gemma, REST API, systemd
|
||||
|
||||
---
|
||||
|
||||
### 5. Nostr Relay (NIP-29)
|
||||
**Decentralized sovereign communications**
|
||||
|
||||
- NIP-29 compliant group relay
|
||||
- Censorship-resistant communication backbone
|
||||
- Agent-to-agent messaging over decentralized protocol
|
||||
- No dependency on corporate communication platforms
|
||||
|
||||
**Tech:** Nostr protocol, Go/Rust relay implementation, WebSocket
|
||||
|
||||
---
|
||||
|
||||
### 6. GOFAI Hybrid Neuro-Symbolic Reasoning
|
||||
**Beyond pattern matching — structured reasoning**
|
||||
|
||||
- Classic AI (GOFAI) techniques combined with neural approaches
|
||||
- Symbolic reasoning for audit trails and explainability
|
||||
- Rule-based decision systems with LLM-powered natural language interface
|
||||
- Deterministic + probabilistic hybrid for critical operations
|
||||
|
||||
**Tech:** Python, custom symbolic engine, LLM integration
|
||||
|
||||
---
|
||||
|
||||
### 7. Evennia MUD with Custom Audit Typeclasses
|
||||
**Interactive environment with full audit capabilities**
|
||||
|
||||
- Custom typeclass system for object behavior tracking
|
||||
- Full audit trail of all interactions and state changes
|
||||
- Extensible framework for simulation and testing
|
||||
- Used internally for agent training and scenario modeling
|
||||
|
||||
**Tech:** Evennia (Python/Django), Twisted, custom typeclasses
|
||||
|
||||
---
|
||||
|
||||
### 8. Webhook-Driven CI/CD Pipeline
|
||||
**Automated build, test, and deploy**
|
||||
|
||||
- Gitea webhook triggers on push/PR/merge
|
||||
- Automated test execution (3,000+ test suite)
|
||||
- Build and deployment automation
|
||||
- Status reporting back to issues and PRs
|
||||
- Zero-manual-intervention deployment for passing builds
|
||||
|
||||
**Tech:** Gitea webhooks, shell automation, systemd, nginx
|
||||
|
||||
---
|
||||
|
||||
## By the Numbers
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Active repositories | 43 |
|
||||
| Organization members | 16 |
|
||||
| Autonomous agents | 5 |
|
||||
| Automated tests | 3,000+ |
|
||||
| Platforms integrated | 4+ (Telegram, Discord, Slack, webhooks) |
|
||||
| Uptime model | 24/7 autonomous operation |
|
||||
| Infrastructure | Self-hosted, sovereign |
|
||||
| External dependencies | Minimal (by design) |
|
||||
|
||||
---
|
||||
|
||||
## What This Means for Clients
|
||||
|
||||
### We've Already Solved the Hard Problems
|
||||
- Agent orchestration at scale? Done.
|
||||
- Agent security and safety? Production-tested.
|
||||
- Autonomous operations? Running 24/7.
|
||||
- Local inference? Deployed.
|
||||
- Multi-platform integration? Built and shipping.
|
||||
|
||||
### You Get a Proven System, Not a Prototype
|
||||
When we deploy agent infrastructure for you, we're not figuring it out for the first time. We're adapting battle-tested systems that have been running in production for months.
|
||||
|
||||
### You Get the Fleet, Not Just One Person
|
||||
Every engagement is backed by the full fleet. That means faster delivery, more thorough testing, and around-the-clock progress on your project.
|
||||
|
||||
---
|
||||
|
||||
## Case Study Format (For Future Clients)
|
||||
|
||||
*As we complete client engagements, case studies will follow this format:*
|
||||
|
||||
### [Client Name / Industry]
|
||||
**Challenge:** What problem they faced
|
||||
**Solution:** What we built
|
||||
**Results:** Quantified outcomes
|
||||
**Timeline:** How fast we delivered
|
||||
**Client Quote:** Their words
|
||||
|
||||
---
|
||||
|
||||
*Portfolio last updated: April 2026*
|
||||
*All systems described are running in production at time of writing.*
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user