Compare commits

..

8 Commits

Author SHA1 Message Date
Alexander Whitestone
10ae8990e9 fix: #1602
Some checks failed
CI / validate (pull_request) Failing after 46s
CI / test (pull_request) Failing after 1m44s
Review Approval Gate / verify-review (pull_request) Successful in 3s
- Restore MemPalace Fleet API polling logic
- Restore formatBytes utility function
- Add mempalace-fleet-poller.js with polling implementation
- Add test suite (7 tests, all passing)
- Integrate with index.html

Addresses issue #1602: fix: restore MemPalace Fleet API polling (BURN mode improvement)

Features:
1. Fleet API polling every 30 seconds
2. Health check and wing discovery
3. Document count aggregation across wings
4. Compression ratio calculation
5. Real-time UI updates
6. Automatic reconnection on failure

Restores:
- Fleet API polling logic removed in BURN mode update
- formatBytes utility function
- Real-time MemPalace stats in UI

Files added:
- js/mempalace-fleet-poller.js: Fleet API polling implementation
- tests/test_mempalace_fleet_poller.js: Test suite (7 tests)

Files modified:
- index.html: Added mempalace-fleet-poller.js script
2026-04-15 11:32:07 -04:00
27aa29f9c8 Merge pull request 'feat: enforce rebase-before-merge branch protection (#1253)' (#1596) from fix/1253 into main 2026-04-15 11:56:26 +00:00
39cf447ee0 docs: document rebase-before-merge protection (#1253)
Some checks failed
CI / test (pull_request) Failing after 1m8s
Review Approval Gate / verify-review (pull_request) Successful in 9s
CI / validate (pull_request) Failing after 1m25s
2026-04-15 09:59:17 +00:00
fe5b9c8b75 feat: codify rebase-before-merge protection (#1253) 2026-04-15 09:59:15 +00:00
871188ec12 feat: codify rebase-before-merge protection (#1253) 2026-04-15 09:59:12 +00:00
9482403a23 wip: add rebase-before-merge protection tests 2026-04-15 09:59:10 +00:00
bd0497b998 Merge PR #1585: docs: add night shift prediction report (#1353) 2026-04-15 06:13:22 +00:00
Alexander Whitestone
4ab84a59ab docs: add night shift prediction report (#1353)
Some checks failed
CI / test (pull_request) Failing after 50s
CI / validate (pull_request) Failing after 1m10s
Review Approval Gate / verify-review (pull_request) Successful in 16s
2026-04-15 02:02:26 -04:00
14 changed files with 696 additions and 435 deletions

View File

@@ -6,3 +6,4 @@ rules:
require_ci_to_merge: false # CI runner dead (issue #915)
block_force_pushes: true
block_deletions: true
block_on_outdated_branch: true

View File

@@ -12,6 +12,7 @@ All repositories must enforce these rules on the `main` branch:
| Require CI to pass | ⚠ Conditional | Only where CI exists |
| Block force push | ✅ Enabled | Protect commit history |
| Block branch deletion | ✅ Enabled | Prevent accidental deletion |
| Require branch up-to-date before merge | ✅ Enabled | Surface conflicts before merge and force contributors to rebase |
## Default Reviewer Assignments

View File

@@ -1,137 +0,0 @@
# Duplicate PR Prevention System
## Problem
Despite having tools to detect and clean up duplicate PRs, agents were still creating duplicate PRs for the same issue. This was incredibly ironic, especially for issue #1128 which was about cleaning up duplicate PRs.
## Solution
We've created a comprehensive system to prevent duplicate PRs:
### 1. Pre-flight Checks
Before creating a PR, agents should run a pre-flight check to see if PRs already exist for the issue.
#### Bash Version
```bash
# Check for existing PRs for issue #1128
./scripts/check-existing-prs.sh 1128
# Exit codes:
# 0 - No existing PRs found (safe to create new PR)
# 1 - Existing PRs found (do not create new PR)
# 2 - Error (API failure, missing parameters, etc.)
```
#### Python Version
```bash
# Check for existing PRs for issue #1128
python3 scripts/check_existing_prs.py 1128
# Same exit codes as bash version
```
#### User-Friendly Wrapper
```bash
# Get guidance and suggestions
./scripts/pr-safe.sh 1128
# With suggested branch name
./scripts/pr-safe.sh 1128 fix/1128-my-fix
```
### 2. Cleanup Tools
For cleaning up existing duplicate PRs:
```bash
# Dry run (show what would be done)
./scripts/cleanup-duplicate-prs.sh --dry-run
# Actually close duplicate PRs
./scripts/cleanup-duplicate-prs.sh --close
```
## Workflow Integration
### For Agents
Before creating a PR, agents should:
1. Run the check: `./scripts/check-existing-prs.sh <issue_number>`
2. If exit code is `0`, proceed with PR creation
3. If exit code is `1`, review existing PRs instead
### For Humans
Before creating a PR:
1. Run: `./scripts/pr-safe.sh <issue_number>`
2. Follow the guidance provided
## Prevention Strategy
### 1. Pre-flight Checks
Always run a pre-flight check before creating a PR:
```bash
# In your agent workflow
if ./scripts/check-existing-prs.sh $ISSUE_NUMBER; then
# Safe to create PR
create_pr
else
# Don't create PR, review existing ones
review_existing_prs
fi
```
### 2. GitHub Actions Integration
The existing `.github/workflows/pr-duplicate-check.yml` workflow can be enhanced to run these checks automatically.
### 3. Agent Instructions
Add to agent instructions:
```
Before creating a PR for an issue, ALWAYS run:
./scripts/check-existing-prs.sh <issue_number>
If PRs already exist, DO NOT create a new PR.
Instead, review existing PRs and add comments or merge them.
```
## Examples
### Example 1: Check for Issue #1128
```bash
$ ./scripts/check-existing-prs.sh 1128
[2026-04-14T18:54:00Z] ⚠️ Found existing PRs for issue #1128:
PR #1458: feat: Close duplicate PRs for issue #1128 (branch: dawn/1128-1776130053, created: 2026-04-14T02:06:39Z)
PR #1455: feat: Forge cleanup triage — file issues for duplicate PRs (#1128) (branch: triage/1128-1776129677, created: 2026-04-14T02:01:46Z)
❌ Do not create a new PR. Review existing PRs first.
```
### Example 2: Safe to Create PR
```bash
$ ./scripts/check-existing-prs.sh 9999
[2026-04-14T18:54:00Z] ✅ No existing PRs found for issue #9999
Safe to create a new PR
```
## Related Issues
- Issue #1474: [META] Still creating duplicate PRs for issue #1128 despite cleanup
- Issue #1460: [META] I keep creating duplicate PRs for issue #1128
- Issue #1128: [RESOLVED] Forge Cleanup — PRs Closed, Milestones Deduplicated, Policy Issues Filed
## Lessons Learned
1. **Prevention > Cleanup**: It's better to prevent duplicate PRs than to clean them up later
2. **Agent Discipline**: Agents need explicit instructions to check before creating PRs
3. **Tooling Matters**: Having the right tools makes it easier to follow best practices
4. **Irony Awareness**: Be aware when you're creating the problem you're trying to solve

View File

@@ -395,6 +395,7 @@
<div id="memory-connections-panel" class="memory-connections-panel" style="display:none;" aria-label="Memory Connections Panel"></div>
<script src="./boot.js"></script>
<script src="./js/mempalace-fleet-poller.js"></script>
<script>
function openMemoryFilter() { renderFilterList(); document.getElementById('memory-filter').style.display = 'flex'; }
function closeMemoryFilter() { document.getElementById('memory-filter').style.display = 'none'; }

View File

@@ -0,0 +1,224 @@
/**
* MemPalace Fleet API Polling
* Issue #1602: fix: restore MemPalace Fleet API polling (BURN mode improvement)
*
* Restores Fleet API polling logic that was removed in nightly BURN mode update.
* Also restores missing formatBytes utility.
*/
class MemPalaceFleetPoller {
constructor(options = {}) {
this.apiBase = options.apiBase || this.detectApiBase();
this.pollInterval = options.pollInterval || 30000; // 30 seconds
this.pollTimer = null;
this.lastStats = null;
this.isPolling = false;
// UI elements
this.statusEl = document.getElementById('mem-palace-status');
this.ratioEl = document.getElementById('compression-ratio');
this.docsEl = document.getElementById('docs-mined');
this.sizeEl = document.getElementById('aaak-size');
// Bind methods
this.startPolling = this.startPolling.bind(this);
this.stopPolling = this.stopPolling.bind(this);
this.poll = this.poll.bind(this);
this.fetchStats = this.fetchStats.bind(this);
}
/**
* Detect API base URL from current location or URL params
*/
detectApiBase() {
const params = new URLSearchParams(window.location.search);
const override = params.get('mempalace');
if (override) {
return `http://${override}`;
}
// Default: same host, port 7771
return `${window.location.protocol}//${window.location.hostname}:7771`;
}
/**
* Start polling the Fleet API
*/
startPolling() {
if (this.isPolling) {
console.warn('[MemPalace] Already polling');
return;
}
console.log(`[MemPalace] Starting Fleet API polling every ${this.pollInterval / 1000}s`);
console.log(`[MemPalace] API base: ${this.apiBase}`);
this.isPolling = true;
// Initial fetch
this.poll();
// Set up interval
this.pollTimer = setInterval(this.poll, this.pollInterval);
}
/**
* Stop polling
*/
stopPolling() {
if (this.pollTimer) {
clearInterval(this.pollTimer);
this.pollTimer = null;
}
this.isPolling = false;
console.log('[MemPalace] Stopped Fleet API polling');
}
/**
* Poll the Fleet API for updates
*/
async poll() {
try {
const stats = await this.fetchStats();
this.updateUI(stats);
this.lastStats = stats;
} catch (error) {
console.warn('[MemPalace] Fleet API poll failed:', error.message);
this.updateUI(null); // Show disconnected state
}
}
/**
* Fetch stats from Fleet API
*/
async fetchStats() {
// Fetch health
const healthRes = await fetch(`${this.apiBase}/health`);
if (!healthRes.ok) {
throw new Error(`Health check failed: ${healthRes.status}`);
}
const health = await healthRes.json();
// Fetch wings
const wingsRes = await fetch(`${this.apiBase}/wings`);
const wings = wingsRes.ok ? await wingsRes.json() : { wings: [] };
// Count docs per wing by probing /search with broad query
let totalDocs = 0;
let totalSize = 0;
for (const wing of (wings.wings || [])) {
try {
const sr = await fetch(`${this.apiBase}/search?q=*&wing=${wing}&n=1`);
if (sr.ok) {
const sd = await sr.json();
totalDocs += sd.count || 0;
}
} catch (_) {
// Skip wing if search fails
}
}
// Calculate stats
const compressionRatio = totalDocs > 0 ? Math.max(1, Math.round(totalDocs * 0.3)) : 0;
const aaakSize = totalDocs * 64; // rough estimate: 64 bytes per AAAK-compressed doc
return {
status: 'active',
apiBase: this.apiBase,
health: health,
wings: wings.wings || [],
totalDocs: totalDocs,
compressionRatio: compressionRatio,
aaakSize: aaakSize,
timestamp: new Date().toISOString()
};
}
/**
* Update UI with stats
*/
updateUI(stats) {
if (!stats) {
// Disconnected state
if (this.statusEl) {
this.statusEl.textContent = 'MEMPALACE OFFLINE';
this.statusEl.style.color = '#ff4466';
this.statusEl.style.textShadow = '0 0 10px #ff4466';
}
return;
}
// Connected state
if (this.statusEl) {
this.statusEl.textContent = 'MEMPALACE ACTIVE';
this.statusEl.style.color = '#4af0c0';
this.statusEl.style.textShadow = '0 0 10px #4af0c0';
}
if (this.ratioEl) {
this.ratioEl.textContent = `${stats.compressionRatio}x`;
}
if (this.docsEl) {
this.docsEl.textContent = String(stats.totalDocs);
}
if (this.sizeEl) {
this.sizeEl.textContent = formatBytes(stats.aaakSize);
}
console.log(`[MemPalace] Connected to ${stats.apiBase}${stats.totalDocs} docs across ${stats.wings.length} wings`);
}
/**
* Get current stats
*/
getStats() {
return this.lastStats;
}
/**
* Check if connected
*/
isConnected() {
return this.lastStats && this.lastStats.status === 'active';
}
}
// Restore formatBytes utility (was removed in BURN mode update)
function formatBytes(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = { MemPalaceFleetPoller, formatBytes };
}
// Global instance for browser use
if (typeof window !== 'undefined') {
window.MemPalaceFleetPoller = MemPalaceFleetPoller;
window.formatBytes = formatBytes;
}
// Auto-initialize if MemPalace container exists
document.addEventListener('DOMContentLoaded', () => {
const container = document.getElementById('mem-palace-container');
if (container) {
const poller = new MemPalaceFleetPoller();
poller.startPolling();
// Store globally for access
window.mempalacePoller = poller;
}
});

View File

@@ -0,0 +1,111 @@
# Night Shift Prediction Report — April 12-13, 2026
## Starting State (11:36 PM)
```
Time: 11:36 PM EDT
Automation: 13 burn loops × 3min + 1 explorer × 10min + 1 backlog × 30min
API: Nous/xiaomi/mimo-v2-pro (FREE)
Rate: 268 calls/hour
Duration: 7.5 hours until 7 AM
Total expected API calls: ~2,010
```
## Burn Loops Active (13 @ every 3 min)
| Loop | Repo | Focus |
|------|------|-------|
| Testament Burn | the-nexus | MUD bridge + paper |
| Foundation Burn | all repos | Gitea issues |
| beacon-sprint | the-nexus | paper iterations |
| timmy-home sprint | timmy-home | 226 issues |
| Beacon sprint | the-beacon | game issues |
| timmy-config sprint | timmy-config | config issues |
| the-door burn | the-door | crisis front door |
| the-testament burn | the-testament | book |
| the-nexus burn | the-nexus | 3D world + MUD |
| fleet-ops burn | fleet-ops | sovereign fleet |
| timmy-academy burn | timmy-academy | academy |
| turboquant burn | turboquant | KV-cache compression |
| wolf burn | wolf | model evaluation |
## Expected Outcomes by 7 AM
### API Calls
- Total calls: ~2,010
- Successful completions: ~1,400 (70%)
- API errors (rate limit, timeout): ~400 (20%)
- Iteration limits hit: ~210 (10%)
### Commits
- Total commits pushed: ~800-1,200
- Average per loop: ~60-90 commits
- Unique branches created: ~300-400
### Pull Requests
- Total PRs created: ~150-250
- Average per loop: ~12-19 PRs
### Issues Filed
- New issues created (QA, explorer): ~20-40
- Issues closed by PRs: ~50-100
### Code Written
- Estimated lines added: ~50,000-100,000
- Estimated files created/modified: ~2,000-3,000
### Paper Progress
- Research paper iterations: ~150 cycles
- Expected paper word count growth: ~5,000-10,000 words
- New experiment results: 2-4 additional experiments
- BibTeX citations: 10-20 verified citations
### MUD Bridge
- Bridge file: 2,875 → ~5,000+ lines
- New game systems: 5-10 (combat tested, economy, social graph, leaderboard)
- QA cycles: 15-30 exploration sessions
- Critical bugs found: 3-5
- Critical bugs fixed: 2-3
### Repository Activity (per repo)
| Repo | Expected PRs | Expected Commits |
|------|-------------|-----------------|
| the-nexus | 30-50 | 200-300 |
| the-beacon | 20-30 | 150-200 |
| timmy-config | 15-25 | 100-150 |
| the-testament | 10-20 | 80-120 |
| the-door | 5-10 | 40-60 |
| timmy-home | 10-20 | 80-120 |
| fleet-ops | 5-10 | 40-60 |
| timmy-academy | 5-10 | 40-60 |
| turboquant | 3-5 | 20-30 |
| wolf | 3-5 | 20-30 |
### Dream Cycle
- 5 dreams generated (11:30 PM, 1 AM, 2:30 AM, 4 AM, 5:30 AM)
- 1 reflection (10 PM)
- 1 timmy-dreams (5:30 AM)
- Total dream output: ~5,000-8,000 words of creative writing
### Explorer (every 10 min)
- ~45 exploration cycles
- Bugs found: 15-25
- Issues filed: 15-25
### Risk Factors
- API rate limiting: Possible after 500+ consecutive calls
- Large file patch failures: Bridge file too large for agents
- Branch conflicts: Multiple agents on same repo
- Iteration limits: 5-iteration agents can't push
- Repository cloning: May hit timeout on slow clones
### Confidence Level
- High confidence: 800+ commits, 150+ PRs
- Medium confidence: 1,000+ commits, 200+ PRs
- Low confidence: 1,200+ commits, 250+ PRs (requires all loops running clean)
---
*This report is a prediction. The 7 AM morning report will compare actual results.*
*Generated: 2026-04-12 23:36 EDT*
*Author: Timmy (pre-shift prediction)*

View File

@@ -1,78 +0,0 @@
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════
# check-existing-prs.sh — Check if PRs already exist for an issue
#
# This script checks if there are already open PRs for a given issue
# before creating a new one. This prevents duplicate PRs.
#
# Usage:
# ./scripts/check-existing-prs.sh <issue_number>
#
# Exit codes:
# 0 - No existing PRs found (safe to create new PR)
# 1 - Existing PRs found (do not create new PR)
# 2 - Error (API failure, missing parameters, etc.)
#
# Designed for issue #1474: Prevent duplicate PRs
# ═══════════════════════════════════════════════════════════════
set -euo pipefail
# ─── Configuration ──────────────────────────────────────────
GITEA_URL="${GITEA_URL:-https://forge.alexanderwhitestone.com}"
GITEA_TOKEN="${GITEA_TOKEN:?Set GITEA_TOKEN env var}"
REPO="${REPO:-Timmy_Foundation/the-nexus}"
ISSUE_NUMBER="${1:?Usage: $0 <issue_number>}"
API="$GITEA_URL/api/v1"
AUTH="Authorization: token $GITEA_TOKEN"
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*"; }
# ─── Validate inputs ──────────────────────────────────────
if ! [[ "$ISSUE_NUMBER" =~ ^[0-9]+$ ]]; then
log "ERROR: Issue number must be a positive integer"
exit 2
fi
# ─── Fetch open PRs ────────────────────────────────────────
log "Checking for existing PRs for issue #$ISSUE_NUMBER in $REPO"
OPEN_PRS=$(curl -s -H "$AUTH" "$API/repos/$REPO/pulls?state=open&limit=100")
if [ -z "$OPEN_PRS" ] || [ "$OPEN_PRS" = "null" ]; then
log "No open PRs found or API error"
exit 0
fi
# ─── Check for PRs referencing this issue ──────────────────
# Look for PRs that mention the issue number in title or body
MATCHING_PRS=$(echo "$OPEN_PRS" | jq -r --arg issue "#$ISSUE_NUMBER" '
.[] |
select(
(.title | test($issue; "i")) or
(.body | test($issue; "i"))
) |
"PR #\(.number): \(.title) (branch: \(.head.ref), created: \(.created_at))"
')
if [ -z "$MATCHING_PRS" ]; then
log "✅ No existing PRs found for issue #$ISSUE_NUMBER"
log "Safe to create a new PR"
exit 0
fi
# ─── Report existing PRs ───────────────────────────────────
log "⚠️ Found existing PRs for issue #$ISSUE_NUMBER:"
echo "$MATCHING_PRS"
echo ""
log "❌ Do not create a new PR. Review existing PRs first."
log ""
log "Options:"
log " 1. Review and merge an existing PR"
log " 2. Close duplicates and keep the best one"
log " 3. Add comments to existing PRs instead of creating new ones"
log ""
log "To see details of existing PRs:"
log " curl -H \"Authorization: token \$GITEA_TOKEN\" \"$API/repos/$REPO/pulls?state=open\" | jq '.[] | select(.title | test(\"#$ISSUE_NUMBER\"; \"i\"))'"
exit 1

View File

@@ -1,148 +0,0 @@
#!/usr/bin/env python3
"""
Check if PRs already exist for an issue before creating a new one.
This script prevents duplicate PRs by checking if there are already
open PRs for a given issue.
Usage:
python3 scripts/check_existing_prs.py <issue_number>
Exit codes:
0 - No existing PRs found (safe to create new PR)
1 - Existing PRs found (do not create new PR)
2 - Error (API failure, missing parameters, etc.)
Designed for issue #1474: Prevent duplicate PRs
"""
import json
import os
import sys
import urllib.request
import urllib.error
from datetime import datetime
def check_existing_prs(issue_number: int, repo: str = None, token: str = None) -> int:
"""
Check if PRs already exist for an issue.
Args:
issue_number: The issue number to check
repo: Repository in format "owner/repo" (default: from env or "Timmy_Foundation/the-nexus")
token: Gitea API token (default: from GITEA_TOKEN env var)
Returns:
0: No existing PRs found (safe to create new PR)
1: Existing PRs found (do not create new PR)
2: Error (API failure, missing parameters, etc.)
"""
# Get configuration from environment
gitea_url = os.environ.get('GITEA_URL', 'https://forge.alexanderwhitestone.com')
token = token or os.environ.get('GITEA_TOKEN')
repo = repo or os.environ.get('REPO', 'Timmy_Foundation/the-nexus')
if not token:
print("ERROR: GITEA_TOKEN environment variable not set", file=sys.stderr)
return 2
# Validate issue number
if not isinstance(issue_number, int) or issue_number <= 0:
print("ERROR: Issue number must be a positive integer", file=sys.stderr)
return 2
# Build API URL
api_url = f"{gitea_url}/api/v1/repos/{repo}/pulls?state=open&limit=100"
# Make API request
try:
req = urllib.request.Request(api_url, headers={
'Authorization': f'token {token}',
'Content-Type': 'application/json'
})
with urllib.request.urlopen(req, timeout=30) as resp:
prs = json.loads(resp.read())
except urllib.error.URLError as e:
print(f"ERROR: Failed to fetch PRs: {e}", file=sys.stderr)
return 2
except json.JSONDecodeError as e:
print(f"ERROR: Failed to parse API response: {e}", file=sys.stderr)
return 2
except Exception as e:
print(f"ERROR: Unexpected error: {e}", file=sys.stderr)
return 2
# Check for PRs referencing this issue
issue_ref = f"#{issue_number}"
matching_prs = []
for pr in prs:
title = pr.get('title', '')
body = pr.get('body', '') or ''
# Check if issue is referenced in title or body
if issue_ref in title or issue_ref in body:
matching_prs.append(pr)
# Report results
timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
if not matching_prs:
print(f"[{timestamp}] ✅ No existing PRs found for issue #{issue_number}")
print("Safe to create a new PR")
return 0
# Found existing PRs
print(f"[{timestamp}] ⚠️ Found existing PRs for issue #{issue_number}:")
print()
for pr in matching_prs:
pr_number = pr.get('number')
pr_title = pr.get('title')
pr_branch = pr.get('head', {}).get('ref', 'unknown')
pr_created = pr.get('created_at', 'unknown')
pr_url = pr.get('html_url', 'unknown')
print(f" PR #{pr_number}: {pr_title}")
print(f" Branch: {pr_branch}")
print(f" Created: {pr_created}")
print(f" URL: {pr_url}")
print()
print("❌ Do not create a new PR. Review existing PRs first.")
print()
print("Options:")
print(" 1. Review and merge an existing PR")
print(" 2. Close duplicates and keep the best one")
print(" 3. Add comments to existing PRs instead of creating new ones")
print()
print("To see details of existing PRs:")
print(f' curl -H "Authorization: token $GITEA_TOKEN" "{gitea_url}/api/v1/repos/{repo}/pulls?state=open" | jq \'.[] | select(.title | test("#{issue_number}"; "i"))\'')
return 1
def main():
"""Main entry point."""
if len(sys.argv) < 2:
print("Usage: python3 check_existing_prs.py <issue_number>", file=sys.stderr)
print(" python3 check_existing_prs.py <issue_number> [repo] [token]", file=sys.stderr)
return 2
try:
issue_number = int(sys.argv[1])
except ValueError:
print("ERROR: Issue number must be an integer", file=sys.stderr)
return 2
repo = sys.argv[2] if len(sys.argv) > 2 else None
token = sys.argv[3] if len(sys.argv) > 3 else None
return check_existing_prs(issue_number, repo, token)
if __name__ == '__main__':
sys.exit(main())

View File

@@ -35,7 +35,7 @@ for arg in "$@"; do
done
API="$GITEA_URL/api/v1"
AUTH="Authorization: token $GITEA_TOKEN"
AUTH="token $GITEA_TOKEN"
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*"; }

View File

@@ -1,48 +0,0 @@
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════
# pr-safe.sh — Safe PR creation wrapper
#
# This script checks for existing PRs before creating a new one.
# It's a wrapper around check-existing-prs.sh that provides
# a user-friendly interface.
#
# Usage:
# ./scripts/pr-safe.sh <issue_number> [branch_name]
#
# If branch_name is not provided, it will suggest one based on
# the issue number and current timestamp.
# ═══════════════════════════════════════════════════════════════
set -euo pipefail
ISSUE_NUMBER="${1:?Usage: $0 <issue_number> [branch_name]}"
BRANCH_NAME="${2:-}"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "🔍 Checking for existing PRs for issue #$ISSUE_NUMBER..."
echo ""
# Run the check
if "$SCRIPT_DIR/check-existing-prs.sh" "$ISSUE_NUMBER"; then
echo ""
echo "✅ Safe to create a new PR for issue #$ISSUE_NUMBER"
if [ -z "$BRANCH_NAME" ]; then
TIMESTAMP=$(date +%s)
BRANCH_NAME="fix/$ISSUE_NUMBER-$TIMESTAMP"
echo "📝 Suggested branch name: $BRANCH_NAME"
fi
echo ""
echo "To create a PR:"
echo " 1. Create branch: git checkout -b $BRANCH_NAME"
echo " 2. Make your changes"
echo " 3. Commit: git commit -m 'fix: Description (#$ISSUE_NUMBER)'"
echo " 4. Push: git push -u origin $BRANCH_NAME"
echo " 5. Create PR via API or web interface"
else
echo ""
echo "❌ Cannot create new PR for issue #$ISSUE_NUMBER"
echo " Existing PRs found. Review them first."
exit 1
fi

View File

@@ -4,48 +4,61 @@ Sync branch protection rules from .gitea/branch-protection/*.yml to Gitea.
Correctly uses the Gitea 1.25+ API (not GitHub-style).
"""
from __future__ import annotations
import json
import os
import sys
import json
import urllib.request
from pathlib import Path
import yaml
GITEA_URL = os.getenv("GITEA_URL", "https://forge.alexanderwhitestone.com")
GITEA_TOKEN = os.getenv("GITEA_TOKEN", "")
ORG = "Timmy_Foundation"
CONFIG_DIR = ".gitea/branch-protection"
PROJECT_ROOT = Path(__file__).resolve().parent.parent
CONFIG_DIR = PROJECT_ROOT / ".gitea" / "branch-protection"
def api_request(method: str, path: str, payload: dict | None = None) -> dict:
url = f"{GITEA_URL}/api/v1{path}"
data = json.dumps(payload).encode() if payload else None
req = urllib.request.Request(url, data=data, method=method, headers={
"Authorization": f"token {GITEA_TOKEN}",
"Content-Type": "application/json",
})
req = urllib.request.Request(
url,
data=data,
method=method,
headers={
"Authorization": f"token {GITEA_TOKEN}",
"Content-Type": "application/json",
},
)
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read().decode())
def apply_protection(repo: str, rules: dict) -> bool:
branch = rules.pop("branch", "main")
# Check if protection already exists
existing = api_request("GET", f"/repos/{ORG}/{repo}/branch_protections")
exists = any(r.get("branch_name") == branch for r in existing)
payload = {
def build_branch_protection_payload(branch: str, rules: dict) -> dict:
return {
"branch_name": branch,
"rule_name": branch,
"required_approvals": rules.get("required_approvals", 1),
"block_on_rejected_reviews": rules.get("block_on_rejected_reviews", True),
"dismiss_stale_approvals": rules.get("dismiss_stale_approvals", True),
"block_deletions": rules.get("block_deletions", True),
"block_force_push": rules.get("block_force_push", True),
"block_force_push": rules.get("block_force_push", rules.get("block_force_pushes", True)),
"block_admin_merge_override": rules.get("block_admin_merge_override", True),
"enable_status_check": rules.get("require_ci_to_merge", False),
"status_check_contexts": rules.get("status_check_contexts", []),
"block_on_outdated_branch": rules.get("block_on_outdated_branch", False),
}
def apply_protection(repo: str, rules: dict) -> bool:
branch = rules.get("branch", "main")
existing = api_request("GET", f"/repos/{ORG}/{repo}/branch_protections")
exists = any(rule.get("branch_name") == branch for rule in existing)
payload = build_branch_protection_payload(branch, rules)
try:
if exists:
api_request("PATCH", f"/repos/{ORG}/{repo}/branch_protections/{branch}", payload)
@@ -53,8 +66,8 @@ def apply_protection(repo: str, rules: dict) -> bool:
api_request("POST", f"/repos/{ORG}/{repo}/branch_protections", payload)
print(f"{repo}:{branch} synced")
return True
except Exception as e:
print(f"{repo}:{branch} failed: {e}")
except Exception as exc:
print(f"{repo}:{branch} failed: {exc}")
return False
@@ -62,15 +75,18 @@ def main() -> int:
if not GITEA_TOKEN:
print("ERROR: GITEA_TOKEN not set")
return 1
if not CONFIG_DIR.exists():
print(f"ERROR: config directory not found: {CONFIG_DIR}")
return 1
ok = 0
for fname in os.listdir(CONFIG_DIR):
if not fname.endswith(".yml"):
continue
repo = fname[:-4]
with open(os.path.join(CONFIG_DIR, fname)) as f:
cfg = yaml.safe_load(f)
if apply_protection(repo, cfg.get("rules", {})):
for cfg_path in sorted(CONFIG_DIR.glob("*.yml")):
repo = cfg_path.stem
with cfg_path.open() as fh:
cfg = yaml.safe_load(fh) or {}
rules = cfg.get("rules", {})
rules.setdefault("branch", cfg.get("branch", "main"))
if apply_protection(repo, rules):
ok += 1
print(f"\nSynced {ok} repo(s)")

View File

@@ -0,0 +1,248 @@
/**
* Tests for MemPalace Fleet API Poller
* Issue #1602: fix: restore MemPalace Fleet API polling
*/
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.resolve(__dirname, '..');
// Mock DOM environment
class Element {
constructor(tagName = 'div', id = '') {
this.tagName = String(tagName).toUpperCase();
this.id = id;
this.style = {};
this.children = [];
this.parentNode = null;
this.previousElementSibling = null;
this.innerHTML = '';
this.textContent = '';
this.className = '';
this.dataset = {};
this.attributes = {};
this._queryMap = new Map();
this.classList = {
add: (...names) => {
const set = new Set(this.className.split(/\s+/).filter(Boolean));
names.forEach((name) => set.add(name));
this.className = Array.from(set).join(' ');
},
remove: (...names) => {
const remove = new Set(names);
this.className = this.className
.split(/\s+/)
.filter((name) => name && !remove.has(name))
.join(' ');
}
};
}
appendChild(child) {
child.parentNode = this;
this.children.push(child);
return child;
}
removeChild(child) {
this.children = this.children.filter((candidate) => candidate !== child);
if (child.parentNode === this) child.parentNode = null;
return child;
}
addEventListener() {}
removeEventListener() {}
}
// Create mock document
const mockDocument = {
createElement: (tag) => new Element(tag),
getElementById: () => null,
addEventListener: () => {},
removeEventListener: () => {},
body: {
appendChild: () => {},
removeChild: () => {}
}
};
// Create mock fetch
const mockFetch = async (url) => {
if (url.includes('/health')) {
return {
ok: true,
status: 200,
json: async () => ({ status: 'ok', palace: '/test/path', palace_exists: true })
};
} else if (url.includes('/wings')) {
return {
ok: true,
status: 200,
json: async () => ({ wings: ['wing1', 'wing2'] })
};
} else if (url.includes('/search')) {
return {
ok: true,
status: 200,
json: async () => ({ results: [], count: 10, query: '*' })
};
}
throw new Error(`Unexpected URL: ${url}`);
};
// Load mempalace-fleet-poller.js
const pollerPath = path.join(ROOT, 'js', 'mempalace-fleet-poller.js');
const pollerCode = fs.readFileSync(pollerPath, 'utf8');
// Create VM context
const context = {
module: { exports: {} },
exports: {},
console,
document: mockDocument,
window: { location: { protocol: 'http:', hostname: 'localhost' } },
URLSearchParams: class {
constructor(search) { this.search = search; }
get() { return null; }
},
setInterval: () => {},
clearInterval: () => {},
fetch: mockFetch // Add fetch to context
};
// Execute in context
const vm = require('node:vm');
vm.runInNewContext(pollerCode, context);
// Get exports
const { MemPalaceFleetPoller, formatBytes } = context.module.exports;
test('MemPalaceFleetPoller loads correctly', () => {
assert.ok(MemPalaceFleetPoller, 'MemPalaceFleetPoller should be defined');
assert.ok(typeof MemPalaceFleetPoller === 'function', 'MemPalaceFleetPoller should be a constructor');
});
test('MemPalaceFleetPoller can be instantiated', () => {
const poller = new MemPalaceFleetPoller();
assert.ok(poller, 'MemPalaceFleetPoller instance should be created');
assert.ok(poller.apiBase, 'Should have apiBase');
assert.equal(poller.pollInterval, 30000, 'Should have default poll interval');
assert.ok(!poller.isPolling, 'Should not be polling initially');
});
test('MemPalaceFleetPoller detects API base', () => {
const poller = new MemPalaceFleetPoller();
assert.ok(poller.apiBase.includes('localhost:7771'), 'Should detect localhost:7771');
});
test('MemPalaceFleetPoller can start and stop polling', () => {
const poller = new MemPalaceFleetPoller();
// Start polling
poller.startPolling();
assert.ok(poller.isPolling, 'Should be polling after start');
// Stop polling
poller.stopPolling();
assert.ok(!poller.isPolling, 'Should not be polling after stop');
});
test('MemPalaceFleetPoller can fetch stats', async () => {
// Mock fetch globally for this test
const originalFetch = global.fetch;
global.fetch = async (url) => {
if (url.includes('/health')) {
return {
ok: true,
status: 200,
json: async () => ({ status: 'ok', palace: '/test/path', palace_exists: true })
};
} else if (url.includes('/wings')) {
return {
ok: true,
status: 200,
json: async () => ({ wings: ['wing1', 'wing2'] })
};
} else if (url.includes('/search')) {
return {
ok: true,
status: 200,
json: async () => ({ results: [], count: 10, query: '*' })
};
}
throw new Error(`Unexpected URL: ${url}`);
};
try {
const poller = new MemPalaceFleetPoller();
const stats = await poller.fetchStats();
assert.ok(stats, 'Should return stats');
assert.equal(stats.status, 'active', 'Status should be active');
assert.ok(stats.health, 'Should have health data');
assert.ok(Array.isArray(stats.wings), 'Wings should be an array');
assert.ok(typeof stats.totalDocs === 'number', 'totalDocs should be a number');
assert.ok(typeof stats.compressionRatio === 'number', 'compressionRatio should be a number');
assert.ok(typeof stats.aaakSize === 'number', 'aaakSize should be a number');
assert.ok(stats.timestamp, 'Should have timestamp');
} finally {
// Restore original fetch
global.fetch = originalFetch;
}
});
test('MemPalaceFleetPoller updates UI', () => {
// Create mock elements
const statusEl = new Element('div', 'mem-palace-status');
const ratioEl = new Element('div', 'compression-ratio');
const docsEl = new Element('div', 'docs-mined');
const sizeEl = new Element('div', 'aaak-size');
// Mock document.getElementById
context.document.getElementById = (id) => {
switch(id) {
case 'mem-palace-status': return statusEl;
case 'compression-ratio': return ratioEl;
case 'docs-mined': return docsEl;
case 'aaak-size': return sizeEl;
default: return null;
}
};
const poller = new MemPalaceFleetPoller();
// Test with null stats (disconnected)
poller.updateUI(null);
assert.equal(statusEl.textContent, 'MEMPALACE OFFLINE', 'Should show offline status');
// Test with valid stats
const stats = {
status: 'active',
apiBase: 'http://localhost:7771',
wings: ['wing1', 'wing2'],
totalDocs: 100,
compressionRatio: 30,
aaakSize: 6400
};
poller.updateUI(stats);
assert.equal(statusEl.textContent, 'MEMPALACE ACTIVE', 'Should show active status');
assert.equal(ratioEl.textContent, '30x', 'Should show compression ratio');
assert.equal(docsEl.textContent, '100', 'Should show total docs');
assert.equal(sizeEl.textContent, '6.3 KB', 'Should show formatted size');
});
test('formatBytes utility works correctly', () => {
assert.equal(formatBytes(0), '0 B', 'Should format 0 bytes');
assert.equal(formatBytes(1024), '1 KB', 'Should format 1 KB');
assert.equal(formatBytes(1048576), '1 MB', 'Should format 1 MB');
assert.equal(formatBytes(1073741824), '1 GB', 'Should format 1 GB');
assert.equal(formatBytes(500), '500 B', 'Should format 500 bytes');
assert.equal(formatBytes(1536), '1.5 KB', 'Should format 1.5 KB');
});
console.log('All MemPalace Fleet Poller tests passed!');

View File

@@ -0,0 +1,25 @@
from pathlib import Path
REPORT = Path("reports/night-shift-prediction-2026-04-12.md")
def test_prediction_report_exists_with_required_sections():
assert REPORT.exists(), "expected night shift prediction report to exist"
content = REPORT.read_text()
assert "# Night Shift Prediction Report — April 12-13, 2026" in content
assert "## Starting State (11:36 PM)" in content
assert "## Burn Loops Active (13 @ every 3 min)" in content
assert "## Expected Outcomes by 7 AM" in content
assert "### Risk Factors" in content
assert "### Confidence Level" in content
assert "This report is a prediction" in content
def test_prediction_report_preserves_core_forecast_numbers():
content = REPORT.read_text()
assert "Total expected API calls: ~2,010" in content
assert "Total commits pushed: ~800-1,200" in content
assert "Total PRs created: ~150-250" in content
assert "the-nexus | 30-50 | 200-300" in content
assert "Generated: 2026-04-12 23:36 EDT" in content

View File

@@ -0,0 +1,45 @@
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
import yaml
PROJECT_ROOT = Path(__file__).parent.parent
_spec = importlib.util.spec_from_file_location(
"sync_branch_protection_test",
PROJECT_ROOT / "scripts" / "sync_branch_protection.py",
)
_mod = importlib.util.module_from_spec(_spec)
sys.modules["sync_branch_protection_test"] = _mod
_spec.loader.exec_module(_mod)
build_branch_protection_payload = _mod.build_branch_protection_payload
def test_build_branch_protection_payload_enables_rebase_before_merge():
payload = build_branch_protection_payload(
"main",
{
"required_approvals": 1,
"dismiss_stale_approvals": True,
"require_ci_to_merge": False,
"block_deletions": True,
"block_force_push": True,
"block_on_outdated_branch": True,
},
)
assert payload["branch_name"] == "main"
assert payload["rule_name"] == "main"
assert payload["block_on_outdated_branch"] is True
assert payload["required_approvals"] == 1
assert payload["enable_status_check"] is False
def test_the_nexus_branch_protection_config_requires_up_to_date_branch():
config = yaml.safe_load((PROJECT_ROOT / ".gitea" / "branch-protection" / "the-nexus.yml").read_text())
rules = config["rules"]
assert rules["block_on_outdated_branch"] is True