Compare commits
1 Commits
fix/1540-s
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c66292727e |
@@ -6,4 +6,3 @@ rules:
|
||||
require_ci_to_merge: false # CI runner dead (issue #915)
|
||||
block_force_pushes: true
|
||||
block_deletions: true
|
||||
block_on_outdated_branch: true
|
||||
|
||||
1
.github/BRANCH_PROTECTION.md
vendored
1
.github/BRANCH_PROTECTION.md
vendored
@@ -12,7 +12,6 @@ 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
|
||||
|
||||
|
||||
18
app.js
18
app.js
@@ -714,11 +714,6 @@ async function init() {
|
||||
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 1000);
|
||||
camera.position.copy(playerPos);
|
||||
|
||||
// Initialize avatar and LOD systems
|
||||
if (window.AvatarCustomization) window.AvatarCustomization.init(scene, camera);
|
||||
if (window.LODSystem) window.LODSystem.init(scene, camera);
|
||||
if (window.SpatialSearch) window.SpatialSearch.init(scene, camera, playerPos);
|
||||
|
||||
updateLoad(20);
|
||||
|
||||
createSkybox();
|
||||
@@ -735,15 +730,6 @@ async function init() {
|
||||
const response = await fetch('./portals.json');
|
||||
const portalData = await response.json();
|
||||
createPortals(portalData);
|
||||
|
||||
// Register portals with spatial search
|
||||
if (window.SpatialSearch) {
|
||||
portals.forEach(p => {
|
||||
if (p.config && p.config.name && p.group) {
|
||||
SpatialSearch.register('portal', p, p.config.name);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to load portals.json:', e);
|
||||
addChatMessage('error', 'Portal registry offline. Check logs.');
|
||||
@@ -3571,10 +3557,6 @@ function gameLoop() {
|
||||
|
||||
if (composer) { composer.render(); } else { renderer.render(scene, camera); }
|
||||
|
||||
// Update avatar and LOD systems
|
||||
if (window.AvatarCustomization && playerPos) window.AvatarCustomization.update(playerPos);
|
||||
if (window.LODSystem && playerPos) window.LODSystem.update(playerPos);
|
||||
|
||||
updateAshStorm(delta, elapsed);
|
||||
|
||||
// Project Mnemosyne - Memory Orb Animation
|
||||
|
||||
@@ -22,9 +22,8 @@
|
||||
<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;700&family=Orbitron:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
<link rel="stylesheet" href="./avatar-customization.css">
|
||||
<link rel="stylesheet" href="./spatial-search.css">
|
||||
<link rel="stylesheet" href="./style.css">
|
||||
<link rel="manifest" href="./manifest.json">
|
||||
<script type="importmap">
|
||||
{
|
||||
"imports": {
|
||||
@@ -396,9 +395,6 @@
|
||||
<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="./avatar-customization.js"></script>
|
||||
<script src="./lod-system.js"></script>
|
||||
<script src="./spatial-search.js"></script>
|
||||
<script>
|
||||
function openMemoryFilter() { renderFilterList(); document.getElementById('memory-filter').style.display = 'flex'; }
|
||||
function closeMemoryFilter() { document.getElementById('memory-filter').style.display = 'none'; }
|
||||
|
||||
186
lod-system.js
186
lod-system.js
@@ -1,186 +0,0 @@
|
||||
/**
|
||||
* LOD (Level of Detail) System for The Nexus
|
||||
*
|
||||
* Optimizes rendering when many avatars/users are visible:
|
||||
* - Distance-based LOD: far users become billboard sprites
|
||||
* - Occlusion: skip rendering users behind walls
|
||||
* - Budget: maintain 60 FPS target with 50+ avatars
|
||||
*
|
||||
* Usage:
|
||||
* LODSystem.init(scene, camera);
|
||||
* LODSystem.registerAvatar(avatarMesh, userId);
|
||||
* LODSystem.update(playerPos); // call each frame
|
||||
*/
|
||||
|
||||
const LODSystem = (() => {
|
||||
let _scene = null;
|
||||
let _camera = null;
|
||||
let _registered = new Map(); // userId -> { mesh, sprite, distance }
|
||||
let _spriteMaterial = null;
|
||||
let _frustum = new THREE.Frustum();
|
||||
let _projScreenMatrix = new THREE.Matrix4();
|
||||
|
||||
// Thresholds
|
||||
const LOD_NEAR = 15; // Full mesh within 15 units
|
||||
const LOD_FAR = 40; // Billboard beyond 40 units
|
||||
const LOD_CULL = 80; // Don't render beyond 80 units
|
||||
const SPRITE_SIZE = 1.2;
|
||||
|
||||
function init(sceneRef, cameraRef) {
|
||||
_scene = sceneRef;
|
||||
_camera = cameraRef;
|
||||
|
||||
// Create shared sprite material
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
// Simple avatar indicator: colored circle
|
||||
ctx.fillStyle = '#00ffcc';
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 32, 20, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#0a0f1a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 28, 8, 0, Math.PI * 2); // head
|
||||
ctx.fill();
|
||||
|
||||
const texture = new THREE.CanvasTexture(canvas);
|
||||
_spriteMaterial = new THREE.SpriteMaterial({
|
||||
map: texture,
|
||||
transparent: true,
|
||||
depthTest: true,
|
||||
sizeAttenuation: true,
|
||||
});
|
||||
|
||||
console.log('[LODSystem] Initialized');
|
||||
}
|
||||
|
||||
function registerAvatar(avatarMesh, userId, color) {
|
||||
// Create billboard sprite for this avatar
|
||||
const spriteMat = _spriteMaterial.clone();
|
||||
if (color) {
|
||||
// Tint sprite to match avatar color
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 32, 20, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#0a0f1a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 28, 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
spriteMat.map = new THREE.CanvasTexture(canvas);
|
||||
spriteMat.map.needsUpdate = true;
|
||||
}
|
||||
|
||||
const sprite = new THREE.Sprite(spriteMat);
|
||||
sprite.scale.set(SPRITE_SIZE, SPRITE_SIZE, 1);
|
||||
sprite.visible = false;
|
||||
_scene.add(sprite);
|
||||
|
||||
_registered.set(userId, {
|
||||
mesh: avatarMesh,
|
||||
sprite: sprite,
|
||||
distance: Infinity,
|
||||
});
|
||||
}
|
||||
|
||||
function unregisterAvatar(userId) {
|
||||
const entry = _registered.get(userId);
|
||||
if (entry) {
|
||||
_scene.remove(entry.sprite);
|
||||
entry.sprite.material.dispose();
|
||||
_registered.delete(userId);
|
||||
}
|
||||
}
|
||||
|
||||
function setSpriteColor(userId, color) {
|
||||
const entry = _registered.get(userId);
|
||||
if (!entry) return;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 64;
|
||||
canvas.height = 64;
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 32, 20, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.fillStyle = '#0a0f1a';
|
||||
ctx.beginPath();
|
||||
ctx.arc(32, 28, 8, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
entry.sprite.material.map = new THREE.CanvasTexture(canvas);
|
||||
entry.sprite.material.map.needsUpdate = true;
|
||||
}
|
||||
|
||||
function update(playerPos) {
|
||||
if (!_camera) return;
|
||||
|
||||
// Update frustum for culling
|
||||
_projScreenMatrix.multiplyMatrices(
|
||||
_camera.projectionMatrix,
|
||||
_camera.matrixWorldInverse
|
||||
);
|
||||
_frustum.setFromProjectionMatrix(_projScreenMatrix);
|
||||
|
||||
_registered.forEach((entry, userId) => {
|
||||
if (!entry.mesh) return;
|
||||
|
||||
const meshPos = entry.mesh.position;
|
||||
const distance = playerPos.distanceTo(meshPos);
|
||||
entry.distance = distance;
|
||||
|
||||
// Beyond cull distance: hide everything
|
||||
if (distance > LOD_CULL) {
|
||||
entry.mesh.visible = false;
|
||||
entry.sprite.visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if in camera frustum
|
||||
const inFrustum = _frustum.containsPoint(meshPos);
|
||||
if (!inFrustum) {
|
||||
entry.mesh.visible = false;
|
||||
entry.sprite.visible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// LOD switching
|
||||
if (distance <= LOD_NEAR) {
|
||||
// Near: full mesh
|
||||
entry.mesh.visible = true;
|
||||
entry.sprite.visible = false;
|
||||
} else if (distance <= LOD_FAR) {
|
||||
// Mid: mesh with reduced detail (keep mesh visible)
|
||||
entry.mesh.visible = true;
|
||||
entry.sprite.visible = false;
|
||||
} else {
|
||||
// Far: billboard sprite
|
||||
entry.mesh.visible = false;
|
||||
entry.sprite.visible = true;
|
||||
entry.sprite.position.copy(meshPos);
|
||||
entry.sprite.position.y += 1.2; // above avatar center
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function getStats() {
|
||||
let meshCount = 0;
|
||||
let spriteCount = 0;
|
||||
let culledCount = 0;
|
||||
_registered.forEach(entry => {
|
||||
if (entry.mesh.visible) meshCount++;
|
||||
else if (entry.sprite.visible) spriteCount++;
|
||||
else culledCount++;
|
||||
});
|
||||
return { total: _registered.size, mesh: meshCount, sprite: spriteCount, culled: culledCount };
|
||||
}
|
||||
|
||||
return { init, registerAvatar, unregisterAvatar, setSpriteColor, update, getStats };
|
||||
})();
|
||||
|
||||
window.LODSystem = LODSystem;
|
||||
@@ -1,111 +0,0 @@
|
||||
# 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)*
|
||||
135
scripts/claim-issue.sh
Executable file
135
scripts/claim-issue.sh
Executable file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env bash
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# claim-issue.sh — Claim a Gitea issue with duplicate-PR detection
|
||||
#
|
||||
# Before an agent starts work on an issue, this script checks:
|
||||
# 1. Is the issue already assigned?
|
||||
# 2. Do open PRs already reference this issue?
|
||||
# 3. Is the issue closed?
|
||||
#
|
||||
# Only proceeds to assign if all checks pass.
|
||||
#
|
||||
# Usage:
|
||||
# ./scripts/claim-issue.sh <issue_number> [repo] [assignee]
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 — Claimed successfully
|
||||
# 1 — BLOCKED (duplicate PR exists, already assigned, or issue closed)
|
||||
# 2 — Error (missing args, API failure)
|
||||
#
|
||||
# Issue #1492: Duplicate-PR detection in agent claim workflow.
|
||||
# Issue #1480: The meta-problem this prevents.
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
set -euo pipefail
|
||||
|
||||
ISSUE_NUM="${1:-}"
|
||||
REPO="${2:-Timmy_Foundation/the-nexus}"
|
||||
ASSIGNEE="${3:-timmy}"
|
||||
|
||||
if [ -z "$ISSUE_NUM" ]; then
|
||||
echo "Usage: $0 <issue_number> [repo] [assignee]"
|
||||
echo "Example: $0 1128"
|
||||
echo " $0 1339 Timmy_Foundation/the-nexus allegro"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
GITEA_URL="${GITEA_URL:-https://forge.alexanderwhitestone.com}"
|
||||
GITEA_TOKEN="${GITEA_TOKEN:-}"
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
TOKEN_FILE="${HOME}/.config/gitea/token"
|
||||
if [ -f "$TOKEN_FILE" ]; then
|
||||
GITEA_TOKEN=$(cat "$TOKEN_FILE" | tr -d '[:space:]')
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -z "$GITEA_TOKEN" ]; then
|
||||
echo "Error: No GITEA_TOKEN. Set env var or create ~/.config/gitea/token"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
API="$GITEA_URL/api/v1"
|
||||
AUTH="Authorization: token $GITEA_TOKEN"
|
||||
|
||||
log() { echo "[$(date -u +%H:%M:%S)] $*"; }
|
||||
|
||||
echo "═══ Claim Issue #$ISSUE_NUM ═══"
|
||||
echo ""
|
||||
|
||||
# ── Step 1: Fetch the issue ──────────────────────────────────
|
||||
ISSUE=$(curl -s -H "$AUTH" "$API/repos/$REPO/issues/$ISSUE_NUM")
|
||||
|
||||
if echo "$ISSUE" | jq -e '.message' > /dev/null 2>&1; then
|
||||
ERROR=$(echo "$ISSUE" | jq -r '.message')
|
||||
echo "✗ Error fetching issue: $ERROR"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
ISSUE_STATE=$(echo "$ISSUE" | jq -r '.state')
|
||||
ISSUE_TITLE=$(echo "$ISSUE" | jq -r '.title')
|
||||
ISSUE_ASSIGNEES=$(echo "$ISSUE" | jq -r '.assignees // [] | map(.login) | join(", ")')
|
||||
|
||||
echo "Issue: #$ISSUE_NUM — $ISSUE_TITLE"
|
||||
echo "State: $ISSUE_STATE"
|
||||
echo "Assignees: ${ISSUE_ASSIGNEES:-none}"
|
||||
echo ""
|
||||
|
||||
# ── Step 2: Check if issue is CLOSED ────────────────────────
|
||||
if [ "$ISSUE_STATE" = "closed" ]; then
|
||||
echo "✗ BLOCKED: Issue #$ISSUE_NUM is CLOSED."
|
||||
echo " Do not work on closed issues."
|
||||
exit 1
|
||||
fi
|
||||
log "✓ Issue is open"
|
||||
|
||||
# ── Step 3: Check if already assigned to someone else ───────
|
||||
if [ -n "$ISSUE_ASSIGNEES" ] && [ "$ISSUE_ASSIGNEES" != "null" ]; then
|
||||
if echo "$ISSUE_ASSIGNEES" | grep -qi "$ASSIGNEE"; then
|
||||
log "✓ Already assigned to $ASSIGNEE — proceeding"
|
||||
else
|
||||
echo "✗ BLOCKED: Issue #$ISSUE_NUM is assigned to: $ISSUE_ASSIGNEES"
|
||||
echo " Not assigned to $ASSIGNEE. Do not work on others' issues."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log "✓ Issue is unassigned"
|
||||
fi
|
||||
|
||||
# ── Step 4: Check for existing open PRs ─────────────────────
|
||||
OPEN_PRS=$(curl -s -H "$AUTH" "$API/repos/$REPO/pulls?state=open&limit=100")
|
||||
|
||||
ISSUE_STR="#$ISSUE_NUM"
|
||||
DUPLICATES=$(echo "$OPEN_PRS" | jq -r ".[] | select(.title | test(\"$ISSUE_STR\"; \"i\") or (.body // \"\") | test(\"$ISSUE_STR\"; \"i\")) | \" PR #\\(.number): \\(.title) [\\(.head.ref)] (\\(.created_at[:10]))\"")
|
||||
|
||||
if [ -n "$DUPLICATES" ]; then
|
||||
echo "✗ BLOCKED: Open PRs already exist for issue #$ISSUE_NUM:"
|
||||
echo ""
|
||||
echo "$DUPLICATES"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " 1. Review and merge an existing PR"
|
||||
echo " 2. Close duplicates: ./scripts/cleanup-duplicate-prs.sh --close"
|
||||
echo " 3. Push to an existing branch"
|
||||
echo ""
|
||||
echo "Do NOT create a new PR. See #1492."
|
||||
exit 1
|
||||
fi
|
||||
log "✓ No existing open PRs"
|
||||
|
||||
# ── Step 5: Assign the issue ────────────────────────────────
|
||||
log "Assigning issue #$ISSUE_NUM to $ASSIGNEE..."
|
||||
|
||||
ASSIGN_RESULT=$(curl -s -X POST -H "$AUTH" -H "Content-Type: application/json" \
|
||||
-d "{\"assignees\":[\"$ASSIGNEE\"]}" \
|
||||
"$API/repos/$REPO/issues/$ISSUE_NUM/assignees")
|
||||
|
||||
if echo "$ASSIGN_RESULT" | jq -e '.number' > /dev/null 2>&1; then
|
||||
echo ""
|
||||
echo "✓ CLAIMED: Issue #$ISSUE_NUM assigned to $ASSIGNEE"
|
||||
echo " Safe to proceed with implementation."
|
||||
exit 0
|
||||
else
|
||||
ERROR=$(echo "$ASSIGN_RESULT" | jq -r '.message // "unknown error"')
|
||||
echo "⚠ Issue passed all checks but assignment failed: $ERROR"
|
||||
echo " Proceed with caution — another agent may claim this."
|
||||
exit 0
|
||||
fi
|
||||
135
scripts/claim_issue.py
Normal file
135
scripts/claim_issue.py
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
claim_issue.py — Claim a Gitea issue with duplicate-PR detection.
|
||||
|
||||
Before an agent starts work, checks:
|
||||
1. Is the issue open?
|
||||
2. Is it already assigned to someone else?
|
||||
3. Do open PRs already reference this issue?
|
||||
|
||||
Only assigns if all checks pass.
|
||||
|
||||
Usage:
|
||||
python3 scripts/claim_issue.py 1492
|
||||
python3 scripts/claim_issue.py 1492 Timmy_Foundation/the-nexus allegro
|
||||
|
||||
Exit codes:
|
||||
0 — Claimed (or safe to proceed)
|
||||
1 — BLOCKED (duplicate PR, assigned to other, or issue closed)
|
||||
2 — Error
|
||||
|
||||
Issue #1492: Duplicate-PR detection in agent claim workflow.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
|
||||
|
||||
def claim_issue(issue_num: int, repo: str = "Timmy_Foundation/the-nexus",
|
||||
assignee: str = "timmy", token: str = None) -> dict:
|
||||
"""Claim an issue with duplicate-PR detection.
|
||||
|
||||
Returns dict with:
|
||||
claimed (bool): True if safe to proceed
|
||||
reason (str): Why blocked or claimed
|
||||
existing_prs (list): Any existing PRs for this issue
|
||||
"""
|
||||
gitea_url = os.environ.get("GITEA_URL", "https://forge.alexanderwhitestone.com")
|
||||
token = token or os.environ.get("GITEA_TOKEN", "")
|
||||
if not token:
|
||||
token_path = os.path.expanduser("~/.config/gitea/token")
|
||||
if os.path.exists(token_path):
|
||||
token = open(token_path).read().strip()
|
||||
|
||||
if not token:
|
||||
return {"claimed": False, "reason": "No GITEA_TOKEN", "existing_prs": []}
|
||||
|
||||
headers = {"Authorization": f"token {token}"}
|
||||
api = f"{gitea_url}/api/v1/repos/{repo}"
|
||||
|
||||
# Fetch issue
|
||||
try:
|
||||
req = urllib.request.Request(f"{api}/issues/{issue_num}", headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
issue = json.loads(resp.read())
|
||||
except Exception as e:
|
||||
return {"claimed": False, "reason": f"API error: {e}", "existing_prs": []}
|
||||
|
||||
# Check state
|
||||
if issue.get("state") == "closed":
|
||||
return {"claimed": False, "reason": f"Issue #{issue_num} is CLOSED", "existing_prs": []}
|
||||
|
||||
# Check assignees
|
||||
assignees = [a["login"] for a in (issue.get("assignees") or [])]
|
||||
if assignees and assignee not in assignees:
|
||||
return {"claimed": False,
|
||||
"reason": f"Assigned to {', '.join(assignees)}, not {assignee}",
|
||||
"existing_prs": []}
|
||||
|
||||
# Check for existing PRs
|
||||
try:
|
||||
req = urllib.request.Request(f"{api}/pulls?state=open&limit=100", headers=headers)
|
||||
with urllib.request.urlopen(req, timeout=10) as resp:
|
||||
prs = json.loads(resp.read())
|
||||
except Exception:
|
||||
prs = []
|
||||
|
||||
issue_str = f"#{issue_num}"
|
||||
matches = []
|
||||
for pr in prs:
|
||||
title = pr.get("title", "")
|
||||
body = pr.get("body") or ""
|
||||
if issue_str in title or issue_str in body:
|
||||
matches.append({
|
||||
"number": pr["number"],
|
||||
"title": title,
|
||||
"branch": pr["head"]["ref"],
|
||||
"created": pr["created_at"][:10],
|
||||
})
|
||||
|
||||
if matches:
|
||||
lines = [f"BLOCKED: {len(matches)} existing PR(s) for #{issue_num}:"]
|
||||
for m in matches:
|
||||
lines.append(f" PR #{m['number']}: {m['title']} [{m['branch']}]")
|
||||
return {"claimed": False, "reason": "\n".join(lines), "existing_prs": matches}
|
||||
|
||||
# All checks passed — assign
|
||||
try:
|
||||
data = json.dumps({"assignees": [assignee]}).encode()
|
||||
req = urllib.request.Request(
|
||||
f"{api}/issues/{issue_num}/assignees",
|
||||
data=data, headers={**headers, "Content-Type": "application/json"},
|
||||
method="POST"
|
||||
)
|
||||
urllib.request.urlopen(req, timeout=10)
|
||||
return {"claimed": True,
|
||||
"reason": f"Issue #{issue_num} claimed by {assignee}",
|
||||
"existing_prs": []}
|
||||
except Exception as e:
|
||||
return {"claimed": True,
|
||||
"reason": f"Checks passed but assignment failed: {e}",
|
||||
"existing_prs": []}
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: claim_issue.py <issue_number> [repo] [assignee]")
|
||||
print("Example: claim_issue.py 1492")
|
||||
print(" claim_issue.py 1339 Timmy_Foundation/the-nexus allegro")
|
||||
sys.exit(2)
|
||||
|
||||
issue_num = int(sys.argv[1])
|
||||
repo = sys.argv[2] if len(sys.argv) > 2 else "Timmy_Foundation/the-nexus"
|
||||
assignee = sys.argv[3] if len(sys.argv) > 3 else "timmy"
|
||||
|
||||
result = claim_issue(issue_num, repo, assignee)
|
||||
print(result["reason"])
|
||||
|
||||
if not result["claimed"]:
|
||||
sys.exit(1)
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -4,61 +4,48 @@ 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"
|
||||
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
||||
CONFIG_DIR = PROJECT_ROOT / ".gitea" / "branch-protection"
|
||||
CONFIG_DIR = ".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 build_branch_protection_payload(branch: str, rules: dict) -> dict:
|
||||
return {
|
||||
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 = {
|
||||
"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", rules.get("block_force_pushes", True)),
|
||||
"block_force_push": rules.get("block_force_push", 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)
|
||||
@@ -66,8 +53,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 exc:
|
||||
print(f"❌ {repo}:{branch} failed: {exc}")
|
||||
except Exception as e:
|
||||
print(f"❌ {repo}:{branch} failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
@@ -75,18 +62,15 @@ 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 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):
|
||||
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", {})):
|
||||
ok += 1
|
||||
|
||||
print(f"\nSynced {ok} repo(s)")
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
/* Spatial Search */
|
||||
.spatial-search-overlay {
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
padding-top: 120px;
|
||||
z-index: 2000;
|
||||
}
|
||||
.spatial-search-overlay.hidden { display: none; }
|
||||
|
||||
.spatial-search-box {
|
||||
background: rgba(10, 15, 26, 0.95);
|
||||
border: 1px solid rgba(0, 255, 204, 0.3);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
width: 400px;
|
||||
max-width: 90vw;
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.spatial-search-box input {
|
||||
width: 100%;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(0, 255, 204, 0.2);
|
||||
border-radius: 4px;
|
||||
color: #e0e0e0;
|
||||
padding: 8px 12px;
|
||||
font-family: inherit;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
.spatial-search-box input:focus { border-color: rgba(0, 255, 204, 0.5); }
|
||||
|
||||
.spatial-result {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
color: #e0e0e0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.spatial-result:hover { background: rgba(0, 255, 204, 0.1); }
|
||||
.spatial-result-name { color: #00ffcc; flex: 1; }
|
||||
.spatial-result-type { color: #666; margin: 0 8px; font-size: 11px; }
|
||||
.spatial-result-dist { color: #ffcc00; }
|
||||
|
||||
.spatial-no-results { color: #666; padding: 8px; font-size: 13px; }
|
||||
@@ -1,223 +0,0 @@
|
||||
/**
|
||||
* Spatial Search Module for The Nexus
|
||||
*
|
||||
* Find nearest users/objects by name. Shows distance and direction.
|
||||
*
|
||||
* Usage:
|
||||
* SpatialSearch.init(scene, camera, playerPos);
|
||||
* SpatialSearch.register('portal', portalObj, 'Morrowind');
|
||||
* SpatialSearch.find('mor'); // returns nearest matches
|
||||
*
|
||||
* Command: /find <name> in chat
|
||||
*/
|
||||
|
||||
const SpatialSearch = (() => {
|
||||
let _scene = null;
|
||||
let _camera = null;
|
||||
let _playerPos = null;
|
||||
let _registry = new Map(); // name -> { object, type, position }
|
||||
let _searchOverlay = null;
|
||||
let _directionArrow = null;
|
||||
|
||||
function init(sceneRef, cameraRef, playerPosRef) {
|
||||
_scene = sceneRef;
|
||||
_camera = cameraRef;
|
||||
_playerPos = playerPosRef;
|
||||
|
||||
// Create search overlay
|
||||
_searchOverlay = document.createElement('div');
|
||||
_searchOverlay.id = 'spatial-search-overlay';
|
||||
_searchOverlay.className = 'spatial-search-overlay hidden';
|
||||
_searchOverlay.innerHTML = '<div class="spatial-search-box">' +
|
||||
'<input type="text" id="spatial-search-input" placeholder="Find user or object..." />' +
|
||||
'<div id="spatial-search-results"></div>' +
|
||||
'</div>';
|
||||
document.body.appendChild(_searchOverlay);
|
||||
|
||||
// Search input handler
|
||||
const input = _searchOverlay.querySelector('#spatial-search-input');
|
||||
input.addEventListener('input', (e) => {
|
||||
const results = find(e.target.value);
|
||||
renderResults(results);
|
||||
});
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') hide();
|
||||
if (e.key === 'Enter') {
|
||||
const results = find(e.target.value);
|
||||
if (results.length > 0) navigateTo(results[0]);
|
||||
hide();
|
||||
}
|
||||
});
|
||||
|
||||
// Create direction arrow (3D)
|
||||
const arrowGeo = new THREE.ConeGeometry(0.2, 0.6, 4);
|
||||
const arrowMat = new THREE.MeshBasicMaterial({ color: 0x00ffcc, transparent: true, opacity: 0.7 });
|
||||
_directionArrow = new THREE.Mesh(arrowGeo, arrowMat);
|
||||
_directionArrow.visible = false;
|
||||
_directionArrow.rotation.x = Math.PI / 2; // point forward
|
||||
_scene.add(_directionArrow);
|
||||
|
||||
console.log('[SpatialSearch] Initialized');
|
||||
}
|
||||
|
||||
function register(type, object, name) {
|
||||
_registry.set(name.toLowerCase(), {
|
||||
object: object,
|
||||
type: type,
|
||||
name: name,
|
||||
});
|
||||
}
|
||||
|
||||
function unregister(name) {
|
||||
_registry.delete(name.toLowerCase());
|
||||
}
|
||||
|
||||
function getPosition(entry) {
|
||||
if (entry.object && entry.object.position) {
|
||||
return entry.object.position;
|
||||
}
|
||||
if (entry.object && entry.object.group && entry.object.group.position) {
|
||||
return entry.object.group.position;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function find(query) {
|
||||
if (!query || query.length < 2) return [];
|
||||
|
||||
const q = query.toLowerCase();
|
||||
const results = [];
|
||||
|
||||
_registry.forEach((entry, key) => {
|
||||
if (key.includes(q) || entry.name.toLowerCase().includes(q)) {
|
||||
const pos = getPosition(entry);
|
||||
if (!pos || !_playerPos) return;
|
||||
|
||||
const distance = _playerPos.distanceTo(pos);
|
||||
const direction = new THREE.Vector3().subVectors(pos, _playerPos).normalize();
|
||||
|
||||
results.push({
|
||||
name: entry.name,
|
||||
type: entry.type,
|
||||
distance: distance,
|
||||
direction: direction,
|
||||
position: pos.clone(),
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by distance
|
||||
results.sort((a, b) => a.distance - b.distance);
|
||||
return results.slice(0, 5); // Max 5 results
|
||||
}
|
||||
|
||||
function renderResults(results) {
|
||||
const container = _searchOverlay.querySelector('#spatial-search-results');
|
||||
if (!results.length) {
|
||||
container.innerHTML = '<div class="spatial-no-results">No matches found</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = results.map((r, i) => {
|
||||
const dir = getDirectionLabel(r.direction);
|
||||
const dist = r.distance < 10 ? `${r.distance.toFixed(1)}m` : `${Math.round(r.distance)}m`;
|
||||
return '<div class="spatial-result" data-index="' + i + '">' +
|
||||
'<span class="spatial-result-name">' + r.name + '</span>' +
|
||||
'<span class="spatial-result-type">' + r.type + '</span>' +
|
||||
'<span class="spatial-result-dist">' + dir + ' ' + dist + '</span>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
|
||||
container.querySelectorAll('.spatial-result').forEach(el => {
|
||||
el.addEventListener('click', () => {
|
||||
const idx = parseInt(el.dataset.index);
|
||||
if (results[idx]) navigateTo(results[idx]);
|
||||
hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getDirectionLabel(dir) {
|
||||
if (!dir) return '?';
|
||||
// Simplify to 8 directions
|
||||
const angle = Math.atan2(dir.x, dir.z) * (180 / Math.PI);
|
||||
if (angle >= -22.5 && angle < 22.5) return 'N';
|
||||
if (angle >= 22.5 && angle < 67.5) return 'NE';
|
||||
if (angle >= 67.5 && angle < 112.5) return 'E';
|
||||
if (angle >= 112.5 && angle < 157.5) return 'SE';
|
||||
if (angle >= 157.5 || angle < -157.5) return 'S';
|
||||
if (angle >= -157.5 && angle < -112.5) return 'SW';
|
||||
if (angle >= -112.5 && angle < -67.5) return 'W';
|
||||
if (angle >= -67.5 && angle < -22.5) return 'NW';
|
||||
return '?';
|
||||
}
|
||||
|
||||
function navigateTo(result) {
|
||||
if (!_playerPos) return;
|
||||
// Teleport player near the target (offset so they don't land on top)
|
||||
const offset = new THREE.Vector3(0, 0, 3).applyQuaternion(
|
||||
new THREE.Quaternion().setFromUnitVectors(
|
||||
new THREE.Vector3(0, 0, 1),
|
||||
result.direction.clone().negate()
|
||||
)
|
||||
);
|
||||
_playerPos.copy(result.position).add(offset);
|
||||
_playerPos.y = 2; // Eye level
|
||||
|
||||
// Show direction arrow briefly
|
||||
showDirectionArrow(result);
|
||||
|
||||
console.log('[SpatialSearch] Navigated to:', result.name, 'distance:', result.distance.toFixed(1));
|
||||
}
|
||||
|
||||
function showDirectionArrow(result) {
|
||||
if (!_directionArrow) return;
|
||||
_directionArrow.position.copy(_playerPos);
|
||||
_directionArrow.position.y = 1.5;
|
||||
_directionArrow.lookAt(result.position);
|
||||
_directionArrow.visible = true;
|
||||
|
||||
// Hide after 3 seconds
|
||||
setTimeout(() => { _directionArrow.visible = false; }, 3000);
|
||||
}
|
||||
|
||||
function show() {
|
||||
if (_searchOverlay) {
|
||||
_searchOverlay.classList.remove('hidden');
|
||||
const input = _searchOverlay.querySelector('#spatial-search-input');
|
||||
if (input) { input.value = ''; input.focus(); }
|
||||
}
|
||||
}
|
||||
|
||||
function hide() {
|
||||
if (_searchOverlay) {
|
||||
_searchOverlay.classList.add('hidden');
|
||||
}
|
||||
if (_directionArrow) _directionArrow.visible = false;
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
if (_searchOverlay && _searchOverlay.classList.contains('hidden')) {
|
||||
show();
|
||||
} else {
|
||||
hide();
|
||||
}
|
||||
}
|
||||
|
||||
// Chat command handler
|
||||
function handleCommand(text) {
|
||||
if (!text.startsWith('/find ')) return false;
|
||||
const query = text.slice(6).trim();
|
||||
const results = find(query);
|
||||
if (results.length === 0) return { text: 'No matches found for "' + query + '"' };
|
||||
const best = results[0];
|
||||
navigateTo(best);
|
||||
const dir = getDirectionLabel(best.direction);
|
||||
const dist = best.distance < 10 ? best.distance.toFixed(1) + 'm' : Math.round(best.distance) + 'm';
|
||||
return { text: best.name + ' (' + best.type + '): ' + dir + ' ' + dist };
|
||||
}
|
||||
|
||||
return { init, register, unregister, find, show, hide, toggle, handleCommand, navigateTo };
|
||||
})();
|
||||
|
||||
window.SpatialSearch = SpatialSearch;
|
||||
@@ -1,25 +0,0 @@
|
||||
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
|
||||
@@ -1,45 +0,0 @@
|
||||
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
|
||||
Reference in New Issue
Block a user