Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
5d878179d1 feat: duplicate PR prevention system (#1480)
Some checks failed
CI / test (pull_request) Failing after 58s
Review Approval Gate / verify-review (pull_request) Failing after 9s
CI / validate (pull_request) Failing after 1m21s
Adds:
- scripts/preflight-pr-check.sh — blocks PR creation if duplicates exist
- scripts/cleanup-duplicate-prs.sh — closes duplicate PRs (keeps oldest)
- docs/duplicate-pr-prevention.md — usage and integration guide

Closes #1480
2026-04-14 22:29:51 -04:00
24 changed files with 263 additions and 2011 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -285,49 +285,6 @@ class AgentMemory:
logger.warning(f"Failed to store memory: {e}")
return None
def remember_alexander_request_response(
self,
*,
request_text: str,
response_text: str,
requester: str = "Alexander Whitestone",
source: str = "",
metadata: Optional[dict] = None,
) -> Optional[str]:
"""Store an Alexander request + wizard response artifact in the sovereign room."""
if not self._check_available():
logger.warning("Cannot store Alexander artifact — MemPalace unavailable")
return None
try:
from nexus.mempalace.searcher import add_memory
from nexus.mempalace.conversation_artifacts import build_request_response_artifact
artifact = build_request_response_artifact(
requester=requester,
responder=self.agent_name,
request_text=request_text,
response_text=response_text,
source=source,
)
extra_metadata = dict(artifact.metadata)
if metadata:
extra_metadata.update(metadata)
doc_id = add_memory(
text=artifact.text,
room=artifact.room,
wing=self.wing,
palace_path=self.palace_path,
source_file=source,
extra_metadata=extra_metadata,
)
logger.debug("Stored Alexander request/response artifact in sovereign room")
return doc_id
except Exception as e:
logger.warning(f"Failed to store Alexander artifact: {e}")
return None
def write_diary(
self,
summary: Optional[str] = None,

8
app.js
View File

@@ -714,10 +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);
updateLoad(20);
createSkybox();
@@ -3561,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

View File

@@ -0,0 +1,72 @@
# Duplicate PR Prevention
## Problem
The burn loop creates duplicate PRs for the same issue because it doesn't check for existing PRs before creating new ones.
## Solution
Two scripts:
### 1. Preflight Check (`scripts/preflight-pr-check.sh`)
Run BEFORE creating a PR:
```bash
./scripts/preflight-pr-check.sh 1128
```
Output if PRs exist:
```
🚫 BLOCKED: 2 existing PR(s) for issue #1128
Existing PRs:
#1458: feat: Close duplicate PRs for issue #1128
Branch: dawn/1128-1776130053
URL: https://...
Options:
1. Review and merge an existing PR
2. Close duplicates and proceed
3. Use --force to bypass (NOT RECOMMENDED)
```
Exit code 1 = blocked. Exit code 0 = safe to proceed.
### 2. Cleanup Script (`scripts/cleanup-duplicate-prs.sh`)
Close duplicate PRs:
```bash
# Dry run (show what would be closed)
./scripts/cleanup-duplicate-prs.sh 1128
# Actually close duplicates (keeps oldest)
./scripts/cleanup-duplicate-prs.sh 1128 --close
```
## Integration
### In burn loop
Add preflight check before PR creation:
```bash
# Before: git push && curl ... /pulls
./scripts/preflight-pr-check.sh $ISSUE_NUM || exit 1
```
### In CI
Add as a GitHub/Gitea Actions check:
```yaml
- name: Check for duplicate PRs
run: ./scripts/preflight-pr-check.sh ${{ github.event.issue.number }}
```
## Environment Variables
- `GITEA_TOKEN` — API token (default: reads from `~/.config/gitea/token`)
- `GITEA_URL` — Forge URL (default: `https://forge.alexanderwhitestone.com`)
- `GITEA_REPO` — Repository (default: `Timmy_Foundation/the-nexus`)

View File

@@ -395,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="./js/nexus-telegram-bridge.js"></script>
<script src="./avatar-customization.js"></script>
<script src="./lod-system.js"></script>
<script>
function openMemoryFilter() { renderFilterList(); document.getElementById('memory-filter').style.display = 'flex'; }
function closeMemoryFilter() { document.getElementById('memory-filter').style.display = 'none'; }

View File

@@ -1,339 +0,0 @@
/**
* Nexus-Telegram Bridge
* Issue #1537: feat: bridge Nexus chat to Hermes Telegram gateway
*
* Bidirectional bridge between Nexus world chat and Telegram.
* - Nexus chat messages forwarded to Telegram
* - Telegram messages appear in Nexus chat
* - Bidirectional, near-realtime (<5s latency)
*/
class NexusTelegramBridge {
constructor(options = {}) {
this.telegramToken = options.telegramToken || process.env.TELEGRAM_BOT_TOKEN;
this.telegramChatId = options.telegramChatId || process.env.TELEGRAM_CHAT_ID;
this.nexusWsUrl = options.nexusWsUrl || 'ws://localhost:8765';
this.pollInterval = options.pollInterval || 5000; // 5 seconds
this.nexusWs = null;
this.telegramPollingInterval = null;
this.isConnected = false;
this.lastTelegramUpdateId = 0;
// Callbacks
this.onNexusMessage = options.onNexusMessage || (() => {});
this.onTelegramMessage = options.onTelegramMessage || (() => {});
this.onError = options.onError || console.error;
// Message queue for offline handling
this.messageQueue = [];
// Bind methods
this.connectToNexus = this.connectToNexus.bind(this);
this.disconnect = this.disconnect.bind(this);
this.sendToTelegram = this.sendToTelegram.bind(this);
this.sendToNexus = this.sendToNexus.bind(this);
this.pollTelegram = this.pollTelegram.bind(this);
}
/**
* Initialize the bridge
*/
async init() {
console.log('Initializing Nexus-Telegram Bridge...');
// Validate configuration
if (!this.telegramToken) {
throw new Error('Telegram bot token required. Set TELEGRAM_BOT_TOKEN environment variable.');
}
if (!this.telegramChatId) {
throw new Error('Telegram chat ID required. Set TELEGRAM_CHAT_ID environment variable.');
}
// Connect to Nexus WebSocket
await this.connectToNexus();
// Start polling Telegram
this.startTelegramPolling();
console.log('Nexus-Telegram Bridge initialized');
}
/**
* Connect to Nexus WebSocket
*/
async connectToNexus() {
return new Promise((resolve, reject) => {
try {
console.log(`Connecting to Nexus at ${this.nexusWsUrl}...`);
this.nexusWs = new WebSocket(this.nexusWsUrl);
this.nexusWs.onopen = () => {
console.log('Connected to Nexus WebSocket');
this.isConnected = true;
// Send any queued messages
this.processMessageQueue();
resolve();
};
this.nexusWs.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleNexusMessage(data);
} catch (error) {
this.onError('Failed to parse Nexus message:', error);
}
};
this.nexusWs.onclose = (event) => {
console.log('Nexus WebSocket closed:', event.code, event.reason);
this.isConnected = false;
// Attempt reconnect after delay
setTimeout(() => {
if (!this.isConnected) {
console.log('Attempting to reconnect to Nexus...');
this.connectToNexus().catch(this.onError);
}
}, 5000);
};
this.nexusWs.onerror = (error) => {
this.onError('Nexus WebSocket error:', error);
reject(error);
};
} catch (error) {
this.onError('Failed to connect to Nexus:', error);
reject(error);
}
});
}
/**
* Handle message from Nexus
*/
handleNexusMessage(data) {
// Filter for chat messages
if (data.type === 'chat' && data.text) {
console.log('Nexus message:', data.text);
// Forward to Telegram
this.sendToTelegram(data.text, data.agent || 'Nexus');
// Call callback
this.onNexusMessage(data);
}
}
/**
* Start polling Telegram for new messages
*/
startTelegramPolling() {
if (this.telegramPollingInterval) {
clearInterval(this.telegramPollingInterval);
}
console.log(`Starting Telegram polling every ${this.pollInterval / 1000}s...`);
// Initial poll
this.pollTelegram();
// Set up interval
this.telegramPollingInterval = setInterval(this.pollTelegram, this.pollInterval);
}
/**
* Poll Telegram for new messages
*/
async pollTelegram() {
try {
const url = `https://api.telegram.org/bot${this.telegramToken}/getUpdates?offset=${this.lastTelegramUpdateId + 1}&timeout=10`;
const response = await fetch(url);
const data = await response.json();
if (data.ok && data.result) {
for (const update of data.result) {
this.handleTelegramUpdate(update);
this.lastTelegramUpdateId = update.update_id;
}
}
} catch (error) {
this.onError('Failed to poll Telegram:', error);
}
}
/**
* Handle update from Telegram
*/
handleTelegramUpdate(update) {
if (update.message && update.message.text) {
const message = update.message;
const text = message.text;
const from = message.from.first_name || message.from.username || 'Telegram User';
console.log(`Telegram message from ${from}:`, text);
// Forward to Nexus
this.sendToNexus(text, from);
// Call callback
this.onTelegramMessage({
text: text,
from: from,
timestamp: new Date(message.date * 1000).toISOString()
});
}
}
/**
* Send message to Telegram
*/
async sendToTelegram(text, sender = 'Nexus') {
try {
const url = `https://api.telegram.org/bot${this.telegramToken}/sendMessage`;
const payload = {
chat_id: this.telegramChatId,
text: `[${sender}]: ${text}`,
parse_mode: 'HTML'
};
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
const result = await response.json();
if (!result.ok) {
this.onError('Failed to send to Telegram:', result);
}
} catch (error) {
this.onError('Error sending to Telegram:', error);
}
}
/**
* Send message to Nexus
*/
sendToNexus(text, sender = 'Telegram') {
if (!this.isConnected || !this.nexusWs) {
// Queue message for later
this.messageQueue.push({ text, sender });
console.log('Message queued (not connected to Nexus)');
return;
}
try {
const message = {
type: 'chat',
text: text,
agent: sender,
timestamp: Date.now(),
source: 'telegram'
};
this.nexusWs.send(JSON.stringify(message));
console.log(`Sent to Nexus: [${sender}] ${text}`);
} catch (error) {
this.onError('Failed to send to Nexus:', error);
// Queue for retry
this.messageQueue.push({ text, sender });
}
}
/**
* Process queued messages
*/
processMessageQueue() {
if (this.messageQueue.length === 0) return;
console.log(`Processing ${this.messageQueue.length} queued messages...`);
while (this.messageQueue.length > 0 && this.isConnected) {
const { text, sender } = this.messageQueue.shift();
this.sendToNexus(text, sender);
}
}
/**
* Disconnect from Nexus
*/
disconnect() {
console.log('Disconnecting Nexus-Telegram Bridge...');
// Stop Telegram polling
if (this.telegramPollingInterval) {
clearInterval(this.telegramPollingInterval);
this.telegramPollingInterval = null;
}
// Close Nexus WebSocket
if (this.nexusWs) {
this.nexusWs.close();
this.nexusWs = null;
}
this.isConnected = false;
console.log('Nexus-Telegram Bridge disconnected');
}
/**
* Get bridge status
*/
getStatus() {
return {
connected: this.isConnected,
nexusWsUrl: this.nexusWsUrl,
telegramConfigured: !!this.telegramToken && !!this.telegramChatId,
lastTelegramUpdateId: this.lastTelegramUpdateId,
queuedMessages: this.messageQueue.length
};
}
/**
* Test Telegram connection
*/
async testTelegramConnection() {
try {
const url = `https://api.telegram.org/bot${this.telegramToken}/getMe`;
const response = await fetch(url);
const data = await response.json();
if (data.ok) {
console.log('Telegram bot connected:', data.result.username);
return true;
} else {
this.onError('Telegram connection test failed:', data);
return false;
}
} catch (error) {
this.onError('Telegram connection test error:', error);
return false;
}
}
}
// Export for use in other modules
if (typeof module !== 'undefined' && module.exports) {
module.exports = NexusTelegramBridge;
}
// Global instance for browser use
if (typeof window !== 'undefined') {
window.NexusTelegramBridge = NexusTelegramBridge;
}

View File

@@ -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;

View File

@@ -62,15 +62,6 @@ core_rooms:
- proof-of-concept code snippets
- benchmark data
- key: sovereign
label: Sovereign
purpose: Artifacts of Alexander Whitestone's requests, directives, and wizard responses
examples:
- dated request/response artifacts
- conversation summaries with speaker tags
- directive ledgers
- response follow-through notes
optional_rooms:
- key: evennia
label: Evennia
@@ -107,6 +98,15 @@ optional_rooms:
purpose: Catch-all for artefacts not yet assigned to a named room
wizards: ["*"]
- key: sovereign
label: Sovereign
purpose: Artifacts of Alexander Whitestone's requests, directives, and conversation history
wizards: ["*"]
conventions:
naming: "YYYY-MM-DD_HHMMSS_<topic>.md"
index: "INDEX.md"
description: "Each artifact is a dated record of a request from Alexander and the wizard's response. The running INDEX.md provides a chronological catalog."
# Tunnel routing table
# Defines which room pairs are connected across wizard wings.
# A tunnel lets `recall <query> --fleet` search both wings at once.

View File

@@ -14,7 +14,6 @@ from nexus.perception_adapter import (
)
from nexus.experience_store import ExperienceStore
from nexus.trajectory_logger import TrajectoryLogger
from nexus.chronicle import ChronicleWriter, AgentEvent, EventKind
try:
from nexus.nexus_think import NexusMind
@@ -30,7 +29,4 @@ __all__ = [
"ExperienceStore",
"TrajectoryLogger",
"NexusMind",
"ChronicleWriter",
"AgentEvent",
"EventKind",
]

View File

@@ -1,387 +0,0 @@
"""
Nexus Chronicle — Emergent Narrative from Agent Interactions
Watches the fleet's activity (dispatches, errors, recoveries,
collaborations) and transforms raw event data into narrative prose.
The system finds the dramatic arc in real work and produces a living
chronicle. The story writes itself from the data.
Usage:
from nexus.chronicle import ChronicleWriter, AgentEvent, EventKind
writer = ChronicleWriter()
writer.ingest(AgentEvent(kind=EventKind.DISPATCH, agent="claude", detail="took issue #42"))
writer.ingest(AgentEvent(kind=EventKind.ERROR, agent="claude", detail="rate limit hit"))
writer.ingest(AgentEvent(kind=EventKind.RECOVERY, agent="claude", detail="retried after backoff"))
writer.ingest(AgentEvent(kind=EventKind.COMMIT, agent="claude", detail="feat: add narrative engine"))
prose = writer.render()
print(prose)
"""
from __future__ import annotations
import json
import time
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import Optional
# ---------------------------------------------------------------------------
# Event model
# ---------------------------------------------------------------------------
class EventKind(str, Enum):
"""The kinds of agent events the chronicle recognises."""
DISPATCH = "dispatch" # agent claimed / was assigned work
COMMIT = "commit" # agent produced a commit
PUSH = "push" # agent pushed a branch
PR_OPEN = "pr_open" # agent opened a pull request
PR_MERGE = "pr_merge" # PR was merged
ERROR = "error" # agent hit an error / exception
RECOVERY = "recovery" # agent recovered from a failure
ABANDON = "abandon" # agent abandoned a task (timeout / giving up)
COLLABORATION = "collab" # two agents worked on the same thing
HEARTBEAT = "heartbeat" # agent reported a heartbeat (alive signal)
IDLE = "idle" # agent is waiting for work
MILESTONE = "milestone" # notable achievement (e.g. 100th issue closed)
@dataclass
class AgentEvent:
"""One discrete thing that happened in the fleet."""
kind: EventKind
agent: str # who did this (e.g. "claude", "mimo-v2-pro")
detail: str = "" # free-text description
timestamp: float = field(default_factory=time.time)
metadata: dict = field(default_factory=dict)
def to_dict(self) -> dict:
return {
"kind": self.kind.value,
"agent": self.agent,
"detail": self.detail,
"timestamp": self.timestamp,
"metadata": self.metadata,
}
@classmethod
def from_dict(cls, data: dict) -> "AgentEvent":
return cls(
kind=EventKind(data["kind"]),
agent=data["agent"],
detail=data.get("detail", ""),
timestamp=data.get("timestamp", time.time()),
metadata=data.get("metadata", {}),
)
# ---------------------------------------------------------------------------
# Narrative templates — maps event kinds to prose fragments
# ---------------------------------------------------------------------------
# Each entry is a list so we can rotate through variants.
_TEMPLATES: dict[EventKind, list[str]] = {
EventKind.DISPATCH: [
"{agent} stepped forward and claimed the work: {detail}.",
"{agent} took on the challenge — {detail}.",
"The task landed on {agent}'s desk: {detail}.",
],
EventKind.COMMIT: [
'{agent} sealed a commit into the record: "{detail}".',
'{agent} committed "{detail}" — progress crystallised.',
"{agent} carved a new ring into the trunk: {detail}.",
],
EventKind.PUSH: [
"{agent} pushed the work upstream.",
"The branch rose into the forge — {agent}'s changes were live.",
"{agent} sent their work into the wider current.",
],
EventKind.PR_OPEN: [
"{agent} opened a pull request: {detail}.",
"A proposal surfaced — {agent} asked the fleet to review {detail}.",
"{agent} laid their work before the reviewers: {detail}.",
],
EventKind.PR_MERGE: [
"{agent}'s branch folded into the whole: {detail}.",
"Consensus reached — {agent}'s changes were merged: {detail}.",
"{detail} joined the canon. {agent}'s contribution lives on.",
],
EventKind.ERROR: [
"{agent} ran into an obstacle: {detail}.",
"Trouble. {agent} encountered {detail} and had to pause.",
"The path grew difficult — {agent} hit {detail}.",
],
EventKind.RECOVERY: [
"{agent} regrouped and pressed on: {detail}.",
"After the setback, {agent} found a way through: {detail}.",
"{agent} recovered — {detail}.",
],
EventKind.ABANDON: [
"{agent} released the task, unable to finish: {detail}.",
"Sometimes wisdom is knowing when to let go. {agent} abandoned {detail}.",
"{agent} stepped back from {detail}. Another will carry it forward.",
],
EventKind.COLLABORATION: [
"{agent} and their peers converged on the same problem: {detail}.",
"Two minds touched the same work — {agent} in collaboration: {detail}.",
"The fleet coordinated — {agent} joined the effort on {detail}.",
],
EventKind.HEARTBEAT: [
"{agent} checked in — still thinking, still present.",
"A pulse from {agent}: the mind is alive.",
"{agent} breathed through another cycle.",
],
EventKind.IDLE: [
"{agent} rested, waiting for the next call.",
"Quiet descended — {agent} held still between tasks.",
"{agent} stood ready, watchful in the lull.",
],
EventKind.MILESTONE: [
"A moment worth noting — {agent}: {detail}.",
"The chronicle marks a milestone. {agent}: {detail}.",
"History ticked over — {agent} reached {detail}.",
],
}
# Arc-level commentary triggered by sequences of events
_ARC_TEMPLATES = {
"struggle_and_recovery": (
"There was a struggle here. {agent} hit trouble and came back stronger — "
"the kind of arc that gives a chronicle its texture."
),
"silent_grind": (
"No drama, just steady work. {agents} moved through the backlog with quiet persistence."
),
"abandon_then_retry": (
"{agent} let go once. But the work called again, and this time it was answered."
),
"solo_sprint": (
"{agent} ran the whole arc alone — dispatch to merge — without breaking stride."
),
"fleet_convergence": (
"The fleet converged. Multiple agents touched the same thread and wove it tighter."
),
}
# ---------------------------------------------------------------------------
# Chronicle writer
# ---------------------------------------------------------------------------
class ChronicleWriter:
"""Accumulates agent events and renders them as narrative prose.
The writer keeps a running log of events. Call ``ingest()`` to add new
events as they arrive, then ``render()`` to produce a prose snapshot of
the current arc.
Events are also persisted to JSONL so the chronicle survives restarts.
"""
def __init__(self, log_path: Optional[Path] = None):
today = time.strftime("%Y-%m-%d")
self.log_path = log_path or (
Path.home() / ".nexus" / "chronicle" / f"chronicle_{today}.jsonl"
)
self.log_path.parent.mkdir(parents=True, exist_ok=True)
self._events: list[AgentEvent] = []
self._template_counters: dict[EventKind, int] = {}
# Load any events already on disk for today
self._load_existing()
# ------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------
def ingest(self, event: AgentEvent) -> None:
"""Add an event to the chronicle and persist it to disk."""
self._events.append(event)
with open(self.log_path, "a") as f:
f.write(json.dumps(event.to_dict()) + "\n")
def render(self, max_events: int = 50) -> str:
"""Render the recent event stream as narrative prose.
Returns a multi-paragraph string suitable for display or logging.
"""
events = self._events[-max_events:]
if not events:
return "The chronicle is empty. No events have been recorded yet."
paragraphs: list[str] = []
# Opening line with timestamp range
first_ts = time.strftime("%H:%M", time.localtime(events[0].timestamp))
last_ts = time.strftime("%H:%M", time.localtime(events[-1].timestamp))
paragraphs.append(
f"The chronicle covers {len(events)} event(s) between {first_ts} and {last_ts}."
)
# Event-by-event prose
sentences: list[str] = []
for evt in events:
sentences.append(self._render_event(evt))
paragraphs.append(" ".join(sentences))
# Arc-level commentary
arc = self._detect_arc(events)
if arc:
paragraphs.append(arc)
return "\n\n".join(paragraphs)
def render_markdown(self, max_events: int = 50) -> str:
"""Render as a Markdown document."""
events = self._events[-max_events:]
if not events:
return "# Chronicle\n\n*No events recorded yet.*"
today = time.strftime("%Y-%m-%d")
lines = [f"# Chronicle — {today}", ""]
for evt in events:
ts = time.strftime("%H:%M:%S", time.localtime(evt.timestamp))
prose = self._render_event(evt)
lines.append(f"**{ts}** — {prose}")
arc = self._detect_arc(events)
if arc:
lines += ["", "---", "", f"*{arc}*"]
return "\n".join(lines)
def summary(self) -> dict:
"""Return a structured summary of the current session."""
agents: dict[str, dict] = {}
kind_counts: dict[str, int] = {}
for evt in self._events:
agents.setdefault(evt.agent, {"events": 0, "kinds": []})
agents[evt.agent]["events"] += 1
agents[evt.agent]["kinds"].append(evt.kind.value)
kind_counts[evt.kind.value] = kind_counts.get(evt.kind.value, 0) + 1
return {
"total_events": len(self._events),
"agents": agents,
"kind_counts": kind_counts,
"log_path": str(self.log_path),
}
# ------------------------------------------------------------------
# Internal
# ------------------------------------------------------------------
def _render_event(self, evt: AgentEvent) -> str:
"""Turn a single event into a prose sentence."""
templates = _TEMPLATES.get(evt.kind, ["{agent}: {detail}"])
counter = self._template_counters.get(evt.kind, 0)
template = templates[counter % len(templates)]
self._template_counters[evt.kind] = counter + 1
return template.format(agent=evt.agent, detail=evt.detail or evt.kind.value)
def _detect_arc(self, events: list[AgentEvent]) -> Optional[str]:
"""Scan the event sequence for a recognisable dramatic arc."""
if not events:
return None
kinds = [e.kind for e in events]
agents = list({e.agent for e in events})
# struggle → recovery
if EventKind.ERROR in kinds and EventKind.RECOVERY in kinds:
err_idx = kinds.index(EventKind.ERROR)
rec_idx = kinds.index(EventKind.RECOVERY)
if rec_idx > err_idx:
agent = events[err_idx].agent
return _ARC_TEMPLATES["struggle_and_recovery"].format(agent=agent)
# abandon → dispatch (retry): find first ABANDON, then any DISPATCH after it
if EventKind.ABANDON in kinds and EventKind.DISPATCH in kinds:
ab_idx = kinds.index(EventKind.ABANDON)
retry_idx = next(
(i for i, k in enumerate(kinds) if k == EventKind.DISPATCH and i > ab_idx),
None,
)
if retry_idx is not None:
agent = events[retry_idx].agent
return _ARC_TEMPLATES["abandon_then_retry"].format(agent=agent)
# solo sprint: single agent goes dispatch→commit→pr_open→pr_merge
solo_arc = {EventKind.DISPATCH, EventKind.COMMIT, EventKind.PR_OPEN, EventKind.PR_MERGE}
if solo_arc.issubset(set(kinds)) and len(agents) == 1:
return _ARC_TEMPLATES["solo_sprint"].format(agent=agents[0])
# fleet convergence: multiple agents, collaboration event
if len(agents) > 1 and EventKind.COLLABORATION in kinds:
return _ARC_TEMPLATES["fleet_convergence"]
# silent grind: only commits / heartbeats, no drama
drama = {EventKind.ERROR, EventKind.ABANDON, EventKind.RECOVERY, EventKind.COLLABORATION}
if not drama.intersection(set(kinds)) and EventKind.COMMIT in kinds:
return _ARC_TEMPLATES["silent_grind"].format(agents=", ".join(agents))
return None
def _load_existing(self) -> None:
"""Load events persisted from earlier in the same session."""
if not self.log_path.exists():
return
with open(self.log_path) as f:
for line in f:
line = line.strip()
if not line:
continue
try:
self._events.append(AgentEvent.from_dict(json.loads(line)))
except (json.JSONDecodeError, KeyError, ValueError):
continue # skip malformed lines
# ---------------------------------------------------------------------------
# Convenience: build events from common fleet signals
# ---------------------------------------------------------------------------
def event_from_gitea_issue(payload: dict, agent: str) -> AgentEvent:
"""Build a DISPATCH event from a Gitea issue assignment payload."""
issue_num = payload.get("number", "?")
title = payload.get("title", "")
return AgentEvent(
kind=EventKind.DISPATCH,
agent=agent,
detail=f"issue #{issue_num}: {title}",
metadata={"issue_number": issue_num},
)
def event_from_heartbeat(hb: dict) -> AgentEvent:
"""Build a HEARTBEAT event from a nexus heartbeat dict."""
agent = hb.get("model", "unknown")
status = hb.get("status", "thinking")
cycle = hb.get("cycle", 0)
return AgentEvent(
kind=EventKind.HEARTBEAT,
agent=agent,
detail=f"cycle {cycle}, status={status}",
metadata=hb,
)
def event_from_commit(commit: dict, agent: str) -> AgentEvent:
"""Build a COMMIT event from a git commit dict."""
message = commit.get("message", "").split("\n")[0] # subject line only
sha = commit.get("sha", "")[:8]
return AgentEvent(
kind=EventKind.COMMIT,
agent=agent,
detail=message,
metadata={"sha": sha},
)

View File

@@ -13,12 +13,6 @@ from __future__ import annotations
from nexus.mempalace.config import MEMPALACE_PATH, FLEET_WING
from nexus.mempalace.searcher import search_memories, add_memory, MemPalaceResult
from nexus.mempalace.conversation_artifacts import (
ConversationArtifact,
build_request_response_artifact,
extract_alexander_request_pairs,
normalize_speaker,
)
__all__ = [
"MEMPALACE_PATH",
@@ -26,8 +20,4 @@ __all__ = [
"search_memories",
"add_memory",
"MemPalaceResult",
"ConversationArtifact",
"build_request_response_artifact",
"extract_alexander_request_pairs",
"normalize_speaker",
]

View File

@@ -40,7 +40,6 @@ CORE_ROOMS: list[str] = [
"nexus", # reports, docs, KT
"issues", # tickets, backlog
"experiments", # prototypes, spikes
"sovereign", # Alexander request/response artifacts
]
# ── ChromaDB collection name ──────────────────────────────────────────────────

View File

@@ -1,122 +0,0 @@
"""Helpers for preserving Alexander request/response artifacts in MemPalace.
This module provides a small, typed bridge between raw conversation turns and
MemPalace drawers stored in the shared `sovereign` room. The goal is not to
solve all future speaker-tagging needs at once; it gives the Nexus one
canonical artifact shape that other miners and bridges can reuse.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Iterable
_ALEXANDER_ALIASES = {
"alexander",
"alexander whitestone",
"rockachopa",
"triptimmy",
}
@dataclass(frozen=True)
class ConversationArtifact:
requester: str
responder: str
request_text: str
response_text: str
room: str = "sovereign"
timestamp: str = field(default_factory=lambda: datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"))
metadata: dict = field(default_factory=dict)
@property
def text(self) -> str:
return (
f"# Conversation Artifact\n\n"
f"## Alexander Request\n{self.request_text.strip()}\n\n"
f"## Wizard Response\n{self.response_text.strip()}\n"
)
def normalize_speaker(name: str | None) -> str:
cleaned = " ".join((name or "").strip().lower().split())
if cleaned in _ALEXANDER_ALIASES:
return "alexander"
return cleaned.replace(" ", "_") or "unknown"
def build_request_response_artifact(
*,
requester: str,
responder: str,
request_text: str,
response_text: str,
source: str = "",
timestamp: str | None = None,
request_timestamp: str | None = None,
response_timestamp: str | None = None,
) -> ConversationArtifact:
requester_slug = normalize_speaker(requester)
responder_slug = normalize_speaker(responder)
ts = timestamp or datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
metadata = {
"artifact_type": "alexander_request_response",
"requester": requester_slug,
"responder": responder_slug,
"speaker_tags": [f"speaker:{requester_slug}", f"speaker:{responder_slug}"],
"source": source,
"timestamp": ts,
}
if request_timestamp:
metadata["request_timestamp"] = request_timestamp
if response_timestamp:
metadata["response_timestamp"] = response_timestamp
return ConversationArtifact(
requester=requester_slug,
responder=responder_slug,
request_text=request_text,
response_text=response_text,
timestamp=ts,
metadata=metadata,
)
def extract_alexander_request_pairs(
turns: Iterable[dict],
*,
responder: str,
source: str = "",
) -> list[ConversationArtifact]:
responder_slug = normalize_speaker(responder)
pending_request: dict | None = None
artifacts: list[ConversationArtifact] = []
for turn in turns:
speaker = normalize_speaker(
turn.get("speaker") or turn.get("username") or turn.get("author") or turn.get("name")
)
text = (turn.get("text") or turn.get("content") or "").strip()
if not text:
continue
if speaker == "alexander":
pending_request = turn
continue
if speaker == responder_slug and pending_request is not None:
artifacts.append(
build_request_response_artifact(
requester="alexander",
responder=responder_slug,
request_text=(pending_request.get("text") or pending_request.get("content") or "").strip(),
response_text=text,
source=source,
request_timestamp=pending_request.get("timestamp"),
response_timestamp=turn.get("timestamp"),
timestamp=turn.get("timestamp") or pending_request.get("timestamp"),
)
)
pending_request = None
return artifacts

View File

@@ -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)*

View File

@@ -1,170 +1,101 @@
#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════
# cleanup-duplicate-prs.sh — Identify and close duplicate open PRs
#
# This script identifies PRs that are duplicates (same issue number
# or very similar titles) and closes the older ones.
# cleanup-duplicate-prs.sh — Close duplicate PRs for a given issue
#
# Usage:
# ./scripts/cleanup-duplicate-prs.sh [--dry-run] [--close]
# ./scripts/cleanup-duplicate-prs.sh <issue_number> [--close]
#
# Options:
# --dry-run Show what would be done without making changes
# --close Actually close duplicate PRs (default is dry-run)
#
# Designed for issue #1128: Forge Cleanup
# ═══════════════════════════════════════════════════════════════
# Without --close: dry run (show what would be closed)
# With --close: actually close the duplicates
set -euo pipefail
# ─── Configuration ──────────────────────────────────────────
ISSUE_NUM="${1:?Usage: cleanup-duplicate-prs.sh <issue_number> [--close]}"
CLOSE_MODE="${2:-}"
GITEA_URL="${GITEA_URL:-https://forge.alexanderwhitestone.com}"
GITEA_TOKEN="${GITEA_TOKEN:?Set GITEA_TOKEN env var}"
REPO="${REPO:-Timmy_Foundation/the-nexus}"
DRY_RUN="${DRY_RUN:-true}"
GITEA_TOKEN="${GITEA_TOKEN:-$(cat ~/.config/gitea/token 2>/dev/null || echo '')}"
REPO="${GITEA_REPO:-Timmy_Foundation/the-nexus}"
# Parse command line arguments
for arg in "$@"; do
case $arg in
--dry-run)
DRY_RUN="true"
;;
--close)
DRY_RUN="false"
;;
esac
done
if [ -z "$GITEA_TOKEN" ]; then
echo "ERROR: GITEA_TOKEN not set"
exit 1
fi
API="$GITEA_URL/api/v1"
AUTH="token $GITEA_TOKEN"
REPO_API="${GITEA_URL}/api/v1/repos/${REPO}"
log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*"; }
# Fetch open PRs
PRS=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" "${REPO_API}/pulls?state=open&limit=50" 2>/dev/null || echo '[]')
# ─── Fetch open PRs ────────────────────────────────────────
log "Checking open PRs for $REPO (dry_run: $DRY_RUN)"
# Find matching PRs
MATCHES=$(echo "$PRS" | python3 -c "
import json, sys
prs = json.load(sys.stdin)
issue = '${ISSUE_NUM}'
matches = []
for pr in prs:
title = pr.get('title', '')
body = pr.get('body', '')
ref = pr.get('head', {}).get('ref', '')
if f'#{issue}' in title or f'#{issue}' in body or issue in ref:
matches.append(pr)
json.dump(matches, sys.stdout)
" 2>/dev/null || echo '[]')
OPEN_PRS=$(curl -s -H "$AUTH" "$API/repos/$REPO/pulls?state=open&limit=50")
COUNT=$(echo "$MATCHES" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null || echo '0')
if [ -z "$OPEN_PRS" ] || [ "$OPEN_PRS" = "null" ]; then
log "No open PRs found or API error"
if [ "$COUNT" -eq 0 ]; then
echo "No PRs found for issue #$ISSUE_NUM"
exit 0
fi
# Count PRs
PR_COUNT=$(echo "$OPEN_PRS" | jq length)
log "Found $PR_COUNT open PRs"
echo "Found $COUNT PR(s) for issue #$ISSUE_NUM:"
echo "$MATCHES" | python3 -c "
import json, sys
prs = json.load(sys.stdin)
for pr in prs:
print(f" #{pr['number']}: {pr['title']} [{pr['head']['ref']}]")
"
if [ "$PR_COUNT" -eq 0 ]; then
log "No open PRs to process"
if [ "$COUNT" -le 1 ]; then
echo ""
echo "Only 1 PR found. No cleanup needed."
exit 0
fi
# ─── Extract issue numbers from PR titles ──────────────────
# Create a temporary file for PR data
TEMP_FILE=$(mktemp)
echo "$OPEN_PRS" | jq -r '.[] | "\(.number)\t\(.title)\t\(.created_at)\t\(.head.ref)"' > "$TEMP_FILE"
# Keep the oldest PR, close the rest
echo "$MATCHES" | python3 -c "
import json, sys
prs = json.load(sys.stdin)
prs.sort(key=lambda p: p['number'])
keep = prs[0]
close = prs[1:]
print(f'KEEP: #{keep["number"]}: {keep["title"]}')
for pr in close:
print(f'CLOSE: #{pr["number"]}: {pr["title"]}')
"
# Group PRs by issue number using temporary files
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR" EXIT
while IFS=$'\t' read -r pr_number pr_title pr_created pr_branch; do
# Extract issue number from title (look for #123 pattern)
if [[ $pr_title =~ \#([0-9]+) ]]; then
issue_num="${BASH_REMATCH[1]}"
echo "$pr_number,$pr_created,$pr_branch" >> "$TEMP_DIR/issue_$issue_num.txt"
fi
done < "$TEMP_FILE"
rm -f "$TEMP_FILE"
# ─── Identify and process duplicates ──────────────────────
DUPLICATES_FOUND=0
CLOSED_COUNT=0
for issue_file in "$TEMP_DIR"/issue_*.txt; do
[ -f "$issue_file" ] || continue
issue_num=$(basename "$issue_file" .txt | sed 's/issue_//')
pr_list=$(cat "$issue_file")
# Count PRs for this issue
pr_count=$(echo -n "$pr_list" | grep -c '^' || true)
if [ "$pr_count" -le 1 ]; then
continue # No duplicates
fi
log "Issue #$issue_num has $pr_count open PRs"
DUPLICATES_FOUND=$((DUPLICATES_FOUND + 1))
# Sort by creation date (oldest first)
sorted_prs=$(echo -n "$pr_list" | sort -t',' -k2)
# Keep the newest PR, close the rest
newest_pr=""
newest_date=""
while IFS=',' read -r pr_num pr_date pr_branch; do
if [ -z "$newest_date" ] || [[ "$pr_date" > "$newest_date" ]]; then
newest_pr="$pr_num"
newest_date="$pr_date"
fi
done <<< "$sorted_prs"
log "Keeping PR #$newest_pr (newest)"
# Close older PRs
while IFS=',' read -r pr_num pr_date pr_branch; do
if [ "$pr_num" = "$newest_pr" ]; then
continue # Skip the newest PR
fi
log "Closing duplicate PR #$pr_num for issue #$issue_num"
if [ "$DRY_RUN" = "true" ]; then
log "DRY RUN: Would close PR #$pr_num"
else
# Add a comment explaining why we're closing
comment_body="Closing as duplicate. PR #$newest_pr is newer and addresses the same issue (#$issue_num)."
curl -s -X POST -H "$AUTH" -H "Content-Type: application/json" -d "{\"body\": \"$comment_body\"}" "$API/repos/$REPO/issues/$pr_num/comments" > /dev/null
# Close the PR
curl -s -X PATCH -H "$AUTH" -H "Content-Type: application/json" -d '{"state": "closed"}' "$API/repos/$REPO/pulls/$pr_num" > /dev/null
log "Closed PR #$pr_num"
CLOSED_COUNT=$((CLOSED_COUNT + 1))
fi
done <<< "$sorted_prs"
done
# ─── Summary ──────────────────────────────────────────────
log "Cleanup complete:"
log " Duplicate issue groups found: $DUPLICATES_FOUND"
log " PRs closed: $CLOSED_COUNT"
log " Dry run: $DRY_RUN"
if [ "$DUPLICATES_FOUND" -eq 0 ]; then
log "No duplicate PRs found"
if [ "$CLOSE_MODE" != "--close" ]; then ""
echo ""
echo "DRY RUN: Add --close to actually close duplicates"
exit 0
fi
# ─── Additional cleanup: Stale PRs ────────────────────────
# Check for PRs older than 30 days with no activity
log "Checking for stale PRs (older than 30 days)..."
# Close duplicates
echo "$MATCHES" | python3 -c "
import json, sys, urllib.request, os
prs = json.load(sys.stdin)
prs.sort(key=lambda p: p['number'])
token = '${GITEA_TOKEN}'
api = '${REPO_API}'
for pr in prs[1:]:
url = f'{api}/pulls/{pr["number"]}'
data = json.dumps({'state': 'closed'}).encode()
req = urllib.request.Request(url, data=data, headers={'Authorization': f'token {token}', 'Content-Type': 'application/json'}, method='PATCH')
try:
urllib.request.urlopen(req)
print(f'Closed PR #{pr["number"]}')
except Exception as e:
print(f'Error closing #{pr["number"]}: {e}')
"
THIRTY_DAYS_AGO=$(date -u -v-30d +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u -d "30 days ago" +%Y-%m-%dT%H:%M:%SZ)
STALE_PRS=$(echo "$OPEN_PRS" | jq -r --arg cutoff "$THIRTY_DAYS_AGO" '.[] | select(.created_at < $cutoff) | "\(.number)\t\(.title)\t\(.created_at)"')
if [ -n "$STALE_PRS" ]; then
STALE_COUNT=$(echo -n "$STALE_PRS" | grep -c '^' || true)
log "Found $STALE_COUNT stale PRs (older than 30 days)"
echo "$STALE_PRS" | while IFS=$'\t' read -r pr_num pr_title pr_created; do
log "Stale PR #$pr_num: $pr_title (created: $pr_created)"
done
else
log "No stale PRs found"
fi
log "Script complete"
echo ""
echo "Cleanup complete."

82
scripts/preflight-pr-check.sh Executable file
View File

@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# preflight-pr-check.sh — Prevent duplicate PRs before creating them
#
# Usage:
# ./scripts/preflight-pr-check.sh <issue_number>
#
# Exit codes:
# 0 = safe to proceed (no existing PRs)
# 1 = BLOCKED (existing PRs found)
# 2 = error
set -euo pipefail
ISSUE_NUM="${1:?Usage: preflight-pr-check.sh <issue_number>}"
GITEA_URL="${GITEA_URL:-https://forge.alexanderwhitestone.com}"
GITEA_TOKEN="${GITEA_TOKEN:-$(cat ~/.config/gitea/token 2>/dev/null || echo '')}"
REPO="${GITEA_REPO:-Timmy_Foundation/the-nexus}"
if [ -z "$GITEA_TOKEN" ]; then
echo "ERROR: GITEA_TOKEN not set and ~/.config/gitea/token not found"
exit 2
fi
# Get repo info
REPO_API="${GITEA_URL}/api/v1/repos/${REPO}"
# Fetch open PRs
PRS=$(curl -sf -H "Authorization: token ${GITEA_TOKEN}" "${REPO_API}/pulls?state=open&limit=50" 2>/dev/null || echo '[]')
# Check for existing PRs referencing this issue
MATCHING_PRS=$(echo "$PRS" | python3 -c "
import json, sys
prs = json.load(sys.stdin)
issue = '${ISSUE_NUM}'
matches = []
for pr in prs:
title = pr.get('title', '')
body = pr.get('body', '')
ref = pr.get('head', {}).get('ref', '')
if f'#{issue}' in title or f'#{issue}' in body or issue in ref:
matches.append({
'number': pr['number'],
'title': title,
'branch': ref,
'url': pr.get('html_url', '')
})
json.dump(matches, sys.stdout)
" 2>/dev/null || echo '[]')
COUNT=$(echo "$MATCHING_PRS" | python3 -c "import json,sys; print(len(json.load(sys.stdin)))" 2>/dev/null || echo '0')
if [ "$COUNT" -gt 0 ]; then
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ 🚫 BLOCKED: $COUNT existing PR(s) for issue #$ISSUE_NUM"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo "Existing PRs:"
echo "$MATCHING_PRS" | python3 -c "
import json, sys
prs = json.load(sys.stdin)
for pr in prs:
print(f" #{pr['number']}: {pr['title']}")
print(f" Branch: {pr['branch']}")
print(f" URL: {pr['url']}")
print()
"
echo "Options:"
echo " 1. Review and merge an existing PR"
echo " 2. Close duplicates and proceed"
echo " 3. Use --force to bypass (NOT RECOMMENDED)"
echo ""
if [ "${2:-}" = "--force" ]; then
echo "⚠️ --force flag detected. Bypassing duplicate check."
exit 0
fi
exit 1
else
echo "✅ Safe to proceed: No existing PRs for issue #$ISSUE_NUM"
exit 0
fi

View File

@@ -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)")

View File

@@ -20,7 +20,6 @@ from agent.memory import (
SessionTranscript,
create_agent_memory,
)
from nexus.mempalace.conversation_artifacts import ConversationArtifact
from agent.memory_hooks import MemoryHooks
@@ -185,24 +184,6 @@ class TestAgentMemory:
doc_id = mem.write_diary()
assert doc_id is None # MemPalace unavailable
def test_remember_alexander_request_response_uses_sovereign_room(self):
mem = AgentMemory(agent_name="allegro")
mem._available = True
with patch("nexus.mempalace.searcher.add_memory", return_value="doc-123") as add_memory:
doc_id = mem.remember_alexander_request_response(
request_text="Catalog my requests.",
response_text="I will preserve them as artifacts.",
requester="Alexander Whitestone",
source="telegram:timmy-time",
)
assert doc_id == "doc-123"
kwargs = add_memory.call_args.kwargs
assert kwargs["room"] == "sovereign"
assert kwargs["wing"] == mem.wing
assert kwargs["extra_metadata"]["artifact_type"] == "alexander_request_response"
assert kwargs["extra_metadata"]["speaker_tags"] == ["speaker:alexander", "speaker:allegro"]
# ---------------------------------------------------------------------------
# MemoryHooks tests

View File

@@ -1,211 +0,0 @@
"""
Tests for nexus.chronicle — emergent narrative from agent interactions.
"""
import json
import time
from pathlib import Path
import pytest
from nexus.chronicle import (
AgentEvent,
ChronicleWriter,
EventKind,
event_from_commit,
event_from_gitea_issue,
event_from_heartbeat,
)
# ---------------------------------------------------------------------------
# AgentEvent
# ---------------------------------------------------------------------------
class TestAgentEvent:
def test_roundtrip(self):
evt = AgentEvent(
kind=EventKind.DISPATCH,
agent="claude",
detail="took issue #42",
)
assert AgentEvent.from_dict(evt.to_dict()).kind == EventKind.DISPATCH
assert AgentEvent.from_dict(evt.to_dict()).agent == "claude"
assert AgentEvent.from_dict(evt.to_dict()).detail == "took issue #42"
def test_default_timestamp_is_recent(self):
before = time.time()
evt = AgentEvent(kind=EventKind.IDLE, agent="mimo")
after = time.time()
assert before <= evt.timestamp <= after
def test_all_event_kinds_are_valid_strings(self):
for kind in EventKind:
evt = AgentEvent(kind=kind, agent="test-agent")
d = evt.to_dict()
assert d["kind"] == kind.value
restored = AgentEvent.from_dict(d)
assert restored.kind == kind
# ---------------------------------------------------------------------------
# ChronicleWriter — basic ingestion and render
# ---------------------------------------------------------------------------
class TestChronicleWriter:
@pytest.fixture
def writer(self, tmp_path):
return ChronicleWriter(log_path=tmp_path / "chronicle.jsonl")
def test_empty_render(self, writer):
text = writer.render()
assert "empty" in text.lower()
def test_single_event_render(self, writer):
writer.ingest(AgentEvent(kind=EventKind.DISPATCH, agent="claude", detail="issue #1"))
text = writer.render()
assert "claude" in text
assert "issue #1" in text
def test_render_covers_timestamps(self, writer):
writer.ingest(AgentEvent(kind=EventKind.DISPATCH, agent="a", detail="start"))
writer.ingest(AgentEvent(kind=EventKind.COMMIT, agent="a", detail="done"))
text = writer.render()
assert "chronicle covers" in text.lower()
def test_events_persisted_to_disk(self, writer, tmp_path):
writer.ingest(AgentEvent(kind=EventKind.COMMIT, agent="claude", detail="feat: x"))
lines = (tmp_path / "chronicle.jsonl").read_text().strip().splitlines()
assert len(lines) == 1
data = json.loads(lines[0])
assert data["kind"] == "commit"
assert data["agent"] == "claude"
def test_load_existing_on_init(self, tmp_path):
log = tmp_path / "chronicle.jsonl"
evt = AgentEvent(kind=EventKind.PUSH, agent="mimo", detail="pushed branch")
log.write_text(json.dumps(evt.to_dict()) + "\n")
writer2 = ChronicleWriter(log_path=log)
assert len(writer2._events) == 1
assert writer2._events[0].kind == EventKind.PUSH
def test_malformed_lines_are_skipped(self, tmp_path):
log = tmp_path / "chronicle.jsonl"
log.write_text("not-json\n{}\n")
# Should not raise
writer2 = ChronicleWriter(log_path=log)
assert writer2._events == []
def test_template_rotation(self, writer):
"""Consecutive events of the same kind use different templates."""
sentences = set()
for _ in range(3):
writer.ingest(AgentEvent(kind=EventKind.HEARTBEAT, agent="claude"))
text = writer.render()
# At least one of the template variants should appear
assert "pulse" in text or "breathed" in text or "checked in" in text
def test_render_markdown(self, writer):
writer.ingest(AgentEvent(kind=EventKind.PR_OPEN, agent="claude", detail="PR #99"))
md = writer.render_markdown()
assert md.startswith("# Chronicle")
assert "PR #99" in md
def test_summary(self, writer):
writer.ingest(AgentEvent(kind=EventKind.DISPATCH, agent="claude", detail="x"))
writer.ingest(AgentEvent(kind=EventKind.COMMIT, agent="claude", detail="y"))
s = writer.summary()
assert s["total_events"] == 2
assert "claude" in s["agents"]
assert s["kind_counts"]["dispatch"] == 1
assert s["kind_counts"]["commit"] == 1
def test_max_events_limit(self, writer):
for i in range(10):
writer.ingest(AgentEvent(kind=EventKind.IDLE, agent="a", detail=str(i)))
text = writer.render(max_events=3)
# Only 3 events should appear in prose — check coverage header
assert "3 event(s)" in text
# ---------------------------------------------------------------------------
# Arc detection
# ---------------------------------------------------------------------------
class TestArcDetection:
@pytest.fixture
def writer(self, tmp_path):
return ChronicleWriter(log_path=tmp_path / "chronicle.jsonl")
def _ingest(self, writer, *kinds, agent="claude"):
for k in kinds:
writer.ingest(AgentEvent(kind=k, agent=agent, detail="x"))
def test_struggle_and_recovery_arc(self, writer):
self._ingest(writer, EventKind.DISPATCH, EventKind.ERROR, EventKind.RECOVERY)
text = writer.render()
assert "struggle" in text.lower() or "trouble" in text.lower()
def test_no_arc_when_no_pattern(self, writer):
self._ingest(writer, EventKind.IDLE)
text = writer.render()
# Should not include arc language (only 1 event, no pattern)
assert "converged" not in text
assert "struggle" not in text
def test_solo_sprint_arc(self, writer):
self._ingest(
writer,
EventKind.DISPATCH,
EventKind.COMMIT,
EventKind.PR_OPEN,
EventKind.PR_MERGE,
)
text = writer.render()
assert "solo" in text.lower() or "alone" in text.lower()
def test_fleet_convergence_arc(self, writer, tmp_path):
writer2 = ChronicleWriter(log_path=tmp_path / "chronicle.jsonl")
writer2.ingest(AgentEvent(kind=EventKind.DISPATCH, agent="claude", detail="x"))
writer2.ingest(AgentEvent(kind=EventKind.COLLABORATION, agent="mimo", detail="x"))
writer2.ingest(AgentEvent(kind=EventKind.COMMIT, agent="claude", detail="x"))
text = writer2.render()
assert "converged" in text.lower() or "fleet" in text.lower()
def test_silent_grind_arc(self, writer):
self._ingest(writer, EventKind.COMMIT, EventKind.COMMIT, EventKind.COMMIT)
text = writer.render()
assert "steady" in text.lower() or "quiet" in text.lower() or "grind" in text.lower()
def test_abandon_then_retry_arc(self, writer):
self._ingest(writer, EventKind.DISPATCH, EventKind.ABANDON, EventKind.DISPATCH)
text = writer.render()
assert "let go" in text.lower() or "abandon" in text.lower() or "called again" in text.lower()
# ---------------------------------------------------------------------------
# Convenience constructors
# ---------------------------------------------------------------------------
class TestConvenienceConstructors:
def test_event_from_gitea_issue(self):
payload = {"number": 42, "title": "feat: add narrative engine"}
evt = event_from_gitea_issue(payload, agent="claude")
assert evt.kind == EventKind.DISPATCH
assert "42" in evt.detail
assert evt.agent == "claude"
def test_event_from_heartbeat(self):
hb = {"model": "claude-sonnet", "status": "thinking", "cycle": 7}
evt = event_from_heartbeat(hb)
assert evt.kind == EventKind.HEARTBEAT
assert evt.agent == "claude-sonnet"
assert "7" in evt.detail
def test_event_from_commit(self):
commit = {"message": "feat: chronicle\n\nFixes #1607", "sha": "abc1234567"}
evt = event_from_commit(commit, agent="claude")
assert evt.kind == EventKind.COMMIT
assert evt.detail == "feat: chronicle" # subject line only
assert evt.metadata["sha"] == "abc12345"

View File

@@ -1,58 +0,0 @@
from pathlib import Path
import yaml
from nexus.mempalace.config import CORE_ROOMS
from nexus.mempalace.conversation_artifacts import (
ConversationArtifact,
build_request_response_artifact,
extract_alexander_request_pairs,
normalize_speaker,
)
def test_sovereign_room_is_core_room() -> None:
assert "sovereign" in CORE_ROOMS
rooms_yaml = yaml.safe_load(Path("mempalace/rooms.yaml").read_text())
assert any(room["key"] == "sovereign" for room in rooms_yaml["core_rooms"])
def test_normalize_speaker_maps_alexander_variants() -> None:
assert normalize_speaker("Alexander Whitestone") == "alexander"
assert normalize_speaker("Rockachopa") == "alexander"
assert normalize_speaker(" ALEXANDER ") == "alexander"
assert normalize_speaker("Bezalel") == "bezalel"
def test_build_request_response_artifact_creates_sovereign_metadata() -> None:
artifact = build_request_response_artifact(
requester="Alexander Whitestone",
responder="Allegro",
request_text="Please organize my conversation artifacts.",
response_text="I will catalog them under a sovereign room.",
source="telegram:timmy-time",
timestamp="2026-04-16T01:30:00Z",
)
assert isinstance(artifact, ConversationArtifact)
assert artifact.room == "sovereign"
assert artifact.metadata["speaker_tags"] == ["speaker:alexander", "speaker:allegro"]
assert artifact.metadata["artifact_type"] == "alexander_request_response"
assert artifact.metadata["responder"] == "allegro"
assert "## Alexander Request" in artifact.text
assert "## Wizard Response" in artifact.text
def test_extract_alexander_request_pairs_finds_following_wizard_response() -> None:
turns = [
{"speaker": "Alexander Whitestone", "text": "Catalog my requests as artifacts.", "timestamp": "2026-04-16T01:00:00Z"},
{"speaker": "Allegro", "text": "I'll build a sovereign room contract.", "timestamp": "2026-04-16T01:01:00Z"},
{"speaker": "Alexander", "text": "Make sure my words are easy to recall.", "timestamp": "2026-04-16T01:02:00Z"},
{"speaker": "Allegro", "text": "I will tag them with speaker metadata.", "timestamp": "2026-04-16T01:03:00Z"},
]
artifacts = extract_alexander_request_pairs(turns, responder="Allegro", source="telegram")
assert len(artifacts) == 2
assert artifacts[0].metadata["request_timestamp"] == "2026-04-16T01:00:00Z"
assert artifacts[1].metadata["response_timestamp"] == "2026-04-16T01:03:00Z"

View File

@@ -1,243 +0,0 @@
/**
* Tests for Nexus-Telegram Bridge
* Issue #1537: feat: bridge Nexus chat to Hermes Telegram gateway
*/
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 fetch
global.fetch = async (url, options) => {
if (url.includes('/getMe')) {
return {
ok: true,
json: async () => ({
ok: true,
result: { username: 'test_bot' }
})
};
} else if (url.includes('/getUpdates')) {
return {
ok: true,
json: async () => ({
ok: true,
result: []
})
};
} else if (url.includes('/sendMessage')) {
return {
ok: true,
json: async () => ({ ok: true })
};
}
throw new Error(`Unexpected URL: ${url}`);
};
// Mock WebSocket
class MockWebSocket {
constructor(url) {
this.url = url;
this.readyState = 1; // OPEN
this.onopen = null;
this.onmessage = null;
this.onclose = null;
this.onerror = null;
// Simulate connection
setTimeout(() => {
if (this.onopen) this.onopen();
}, 10);
}
send(data) {
// Mock send
}
close() {
this.readyState = 3; // CLOSED
if (this.onclose) this.onclose({ code: 1000, reason: 'Normal closure' });
}
}
global.WebSocket = MockWebSocket;
// Load nexus-telegram-bridge.js
const bridgePath = path.join(ROOT, 'js', 'nexus-telegram-bridge.js');
const bridgeCode = fs.readFileSync(bridgePath, 'utf8');
// Create VM context
const context = {
module: { exports: {} },
exports: {},
console,
window: { location: { protocol: 'http:', hostname: 'localhost' } },
fetch: global.fetch,
WebSocket: global.WebSocket,
setInterval: () => {},
clearInterval: () => {},
setTimeout: (fn, delay) => setTimeout(fn, delay),
Date: Date
};
// Execute in context
const vm = require('node:vm');
vm.runInNewContext(bridgeCode, context);
// Get NexusTelegramBridge
const NexusTelegramBridge = context.module.exports;
test('NexusTelegramBridge loads correctly', () => {
assert.ok(NexusTelegramBridge, 'NexusTelegramBridge should be defined');
assert.ok(typeof NexusTelegramBridge === 'function', 'NexusTelegramBridge should be a constructor');
});
test('NexusTelegramBridge can be instantiated', () => {
const bridge = new NexusTelegramBridge({
telegramToken: 'test_token',
telegramChatId: 'test_chat_id'
});
assert.ok(bridge, 'NexusTelegramBridge instance should be created');
assert.equal(bridge.telegramToken, 'test_token', 'Should have telegram token');
assert.equal(bridge.telegramChatId, 'test_chat_id', 'Should have telegram chat ID');
assert.equal(bridge.pollInterval, 5000, 'Should have default poll interval');
assert.ok(!bridge.isConnected, 'Should not be connected initially');
});
test('NexusTelegramBridge validates configuration', () => {
// Should throw without token
assert.throws(
() => new NexusTelegramBridge({}),
{ message: /Telegram bot token required/ }
);
// Should throw without chat ID
assert.throws(
() => new NexusTelegramBridge({ telegramToken: 'test' }),
{ message: /Telegram chat ID required/ }
);
});
test('NexusTelegramBridge can connect to Nexus', async () => {
const bridge = new NexusTelegramBridge({
telegramToken: 'test_token',
telegramChatId: 'test_chat_id',
nexusWsUrl: 'ws://localhost:8765'
});
// Mock WebSocket connection
let connected = false;
global.WebSocket = class extends MockWebSocket {
constructor(url) {
super(url);
connected = true;
}
};
await bridge.connectToNexus();
assert.ok(connected, 'Should attempt to connect to Nexus');
assert.ok(bridge.nexusWs, 'Should have WebSocket connection');
});
test('NexusTelegramBridge handles Nexus messages', () => {
const bridge = new NexusTelegramBridge({
telegramToken: 'test_token',
telegramChatId: 'test_chat_id'
});
let messageReceived = false;
bridge.onNexusMessage = (data) => {
messageReceived = true;
assert.equal(data.type, 'chat');
assert.equal(data.text, 'Hello from Nexus');
};
// Simulate message from Nexus
bridge.handleNexusMessage({
type: 'chat',
text: 'Hello from Nexus',
agent: 'User'
});
assert.ok(messageReceived, 'Should call onNexusMessage callback');
});
test('NexusTelegramBridge handles Telegram messages', () => {
const bridge = new NexusTelegramBridge({
telegramToken: 'test_token',
telegramChatId: 'test_chat_id'
});
let messageReceived = false;
bridge.onTelegramMessage = (data) => {
messageReceived = true;
assert.equal(data.text, 'Hello from Telegram');
assert.equal(data.from, 'Test User');
};
// Simulate update from Telegram
bridge.handleTelegramUpdate({
update_id: 123,
message: {
text: 'Hello from Telegram',
from: { first_name: 'Test User' },
date: Math.floor(Date.now() / 1000)
}
});
assert.ok(messageReceived, 'Should call onTelegramMessage callback');
});
test('NexusTelegramBridge queues messages when disconnected', () => {
const bridge = new NexusTelegramBridge({
telegramToken: 'test_token',
telegramChatId: 'test_chat_id'
});
// Not connected
assert.ok(!bridge.isConnected, 'Should not be connected');
// Send message (should queue)
bridge.sendToNexus('Test message', 'Sender');
assert.equal(bridge.messageQueue.length, 1, 'Should queue message');
assert.equal(bridge.messageQueue[0].text, 'Test message', 'Should queue correct message');
});
test('NexusTelegramBridge gets status', () => {
const bridge = new NexusTelegramBridge({
telegramToken: 'test_token',
telegramChatId: 'test_chat_id'
});
const status = bridge.getStatus();
assert.ok(status, 'Should return status object');
assert.equal(status.connected, false, 'Should not be connected');
assert.equal(status.telegramConfigured, true, 'Should be configured');
assert.equal(status.queuedMessages, 0, 'Should have 0 queued messages');
});
test('NexusTelegramBridge can be disconnected', () => {
const bridge = new NexusTelegramBridge({
telegramToken: 'test_token',
telegramChatId: 'test_chat_id'
});
// Mock WebSocket
bridge.nexusWs = { close: () => {} };
bridge.isConnected = true;
bridge.telegramPollingInterval = setInterval(() => {}, 1000);
bridge.disconnect();
assert.ok(!bridge.isConnected, 'Should not be connected after disconnect');
assert.equal(bridge.nexusWs, null, 'Should clear WebSocket');
});
console.log('All NexusTelegramBridge tests passed!');

View File

@@ -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

View File

@@ -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