Compare commits

...

4 Commits

Author SHA1 Message Date
Alexander Whitestone
ff3691e81e fix: closes #729
Some checks failed
CI / test (pull_request) Failing after 9s
CI / validate (pull_request) Failing after 13s
Review Approval Gate / verify-review (pull_request) Failing after 2s
2026-04-13 00:51:34 +00:00
6c67002161 Merge pull request 'fix: [PORTAL] Rebuild the portal stack as Timmy → Reflex → Pilot on clean backlog' (#1324) from mimo/build/issue-672 into main
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Staging Verification Gate / verify-staging (push) Has been cancelled
2026-04-13 00:51:10 +00:00
43699c83cf Merge pull request 'fix: [IDENTITY] Make SOUL / Oath panel part of the main interaction loop' (#1316) from mimo/create/issue-709 into main
Some checks failed
Deploy Nexus / deploy (push) Has been cancelled
Staging Verification Gate / verify-staging (push) Has been cancelled
2026-04-13 00:50:58 +00:00
Alexander Whitestone
91f0bcb034 fix: closes #672
Some checks failed
CI / test (pull_request) Failing after 9s
CI / validate (pull_request) Failing after 15s
Review Approval Gate / verify-review (pull_request) Failing after 3s
2026-04-13 00:50:38 +00:00
4 changed files with 282 additions and 3 deletions

87
app.js
View File

@@ -55,6 +55,11 @@ let _clickStartX = 0, _clickStartY = 0; // Mnemosyne: click-vs-drag detection
let loadProgress = 0;
let performanceTier = 'high';
/** Escape HTML entities for safe innerHTML insertion. */
function escHtml(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ═══ HERMES WS STATE ═══
let hermesWs = null;
let wsReconnectTimer = null;
@@ -65,6 +70,8 @@ let evenniaConnected = false;
let evenniaStaleTimer = null;
const EVENNIA_STALE_MS = 60000; // mark stale after 60s without update
let recentToolOutputs = [];
let actionStreamEntries = []; // Evennia command/result flow for action stream panel
let actionStreamRoom = ''; // Current room from movement events
let workshopPanelCtx = null;
let workshopPanelTexture = null;
let workshopPanelCanvas = null;
@@ -1579,15 +1586,22 @@ function createPortal(config) {
// Label
const labelCanvas = document.createElement('canvas');
labelCanvas.width = 512;
labelCanvas.height = 64;
labelCanvas.height = 96;
const lctx = labelCanvas.getContext('2d');
lctx.font = 'bold 32px "Orbitron", sans-serif';
lctx.fillStyle = '#' + portalColor.getHexString();
lctx.textAlign = 'center';
lctx.fillText(`${config.name.toUpperCase()}`, 256, 42);
lctx.fillText(`${config.name.toUpperCase()}`, 256, 36);
// Role tag (timmy/reflex/pilot) — defines portal ownership boundary
if (config.role) {
const roleColors = { timmy: '#4af0c0', reflex: '#ff4466', pilot: '#ffd700' };
lctx.font = 'bold 18px "Orbitron", sans-serif';
lctx.fillStyle = roleColors[config.role] || '#888888';
lctx.fillText(config.role.toUpperCase(), 256, 68);
}
const labelTex = new THREE.CanvasTexture(labelCanvas);
const labelMat = new THREE.MeshBasicMaterial({ map: labelTex, transparent: true, side: THREE.DoubleSide });
const labelMesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 0.5), labelMat);
const labelMesh = new THREE.Mesh(new THREE.PlaneGeometry(4, 0.75), labelMat);
labelMesh.position.y = 7.5;
group.add(labelMesh);
@@ -2209,6 +2223,71 @@ function handleHermesMessage(data) {
}
} else if (data.type && data.type.startsWith('evennia.')) {
handleEvenniaEvent(data);
// Evennia event bridge — process command/result/room fields if present
handleEvenniaEvent(data);
}
// ═══════════════════════════════════════════
// TIMMY ACTION STREAM — EVENNIA COMMAND FLOW
// ═══════════════════════════════════════════
const MAX_ACTION_STREAM = 8;
/**
* Add an entry to the action stream panel.
* @param {'cmd'|'result'|'room'} type
* @param {string} text
*/
function addActionStreamEntry(type, text) {
const entry = { type, text, ts: Date.now() };
actionStreamEntries.unshift(entry);
if (actionStreamEntries.length > MAX_ACTION_STREAM) actionStreamEntries.pop();
renderActionStream();
}
/**
* Update the current room display in the action stream.
* @param {string} room
*/
function setActionStreamRoom(room) {
actionStreamRoom = room;
const el = document.getElementById('action-stream-room');
if (el) el.textContent = room ? `${room}` : '';
}
/**
* Render the action stream panel entries.
*/
function renderActionStream() {
const el = document.getElementById('action-stream-content');
if (!el) return;
el.innerHTML = actionStreamEntries.map(e => {
const ts = new Date(e.ts).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const cls = e.type === 'cmd' ? 'as-cmd' : e.type === 'result' ? 'as-result' : 'as-room';
const prefix = e.type === 'cmd' ? '>' : e.type === 'result' ? '←' : '◈';
return `<div class="as-entry ${cls}"><span class="as-prefix">${prefix}</span> <span class="as-text">${escHtml(e.text)}</span> <span class="as-ts">${ts}</span></div>`;
}).join('');
}
/**
* Process Evennia-specific fields from Hermes WS messages.
* Called from handleHermesMessage for any message carrying evennia metadata.
*/
function handleEvenniaEvent(data) {
if (data.evennia_command) {
addActionStreamEntry('cmd', data.evennia_command);
}
if (data.evennia_result) {
const excerpt = typeof data.evennia_result === 'string'
? data.evennia_result.substring(0, 120)
: JSON.stringify(data.evennia_result).substring(0, 120);
addActionStreamEntry('result', excerpt);
}
if (data.evennia_room) {
setActionStreamRoom(data.evennia_room);
addActionStreamEntry('room', `Moved to: ${data.evennia_room}`);
}
}
@@ -3088,6 +3167,8 @@ function populateAtlas() {
<div class="atlas-card-footer">
<div class="atlas-card-coord">X:${config.position.x} Z:${config.position.z}</div>
<div class="atlas-card-action">${actionLabel} →</div>
${config.role ? `<div class="atlas-card-role role-${config.role}">${config.role.toUpperCase()}</div>` : ''}
<div class="atlas-card-type">${config.destination?.type?.toUpperCase() || 'UNKNOWN'}</div>
</div>
`;

View File

@@ -170,6 +170,15 @@
</div>
</div>
<!-- Timmy Action Stream (Evennia command/result flow) -->
<div id="action-stream" class="action-stream">
<div class="action-stream-header">
<span class="action-stream-icon"></span> TIMMY ACTION STREAM
</div>
<div id="action-stream-room" class="action-stream-room"></div>
<div id="action-stream-content" class="action-stream-content"></div>
</div>
<!-- Bottom: Chat Interface -->
<div id="chat-panel" class="chat-panel">
<div class="chat-header">
@@ -375,6 +384,107 @@
</ul>
</div>
</div>
<div class="branch-policy" style="margin-top: 10px; font-size: 12px; color: #aaa;">
<strong>BRANCH PROTECTION POLICY</strong><br>
<ul style="margin:0; padding-left:15px;">
<li>• Require PR for merge ✅</li>
<li>• Require 1 approval ✅</li>
<li>• Dismiss stale approvals ✅</li>
<li>• Require CI ✅ (where available)</li>
<li>• Block force push ✅</li>
<li>• Block branch deletion ✅</li>
<li>• Weekly audit for unreviewed merges ✅</li>
</ul>
</div>
<div id="mem-palace-container" class="mem-palace-ui">
<div class="mem-palace-header">
<span id="mem-palace-status">MEMPALACE</span>
<button onclick="mineMemPalaceContent()" class="mem-palace-btn">Mine Chat</button>
</div>
<div class="mem-palace-stats">
<div>Compression: <span id="compression-ratio">--</span>x</div>
<div>Docs mined: <span id="docs-mined">0</span></div>
<div>AAAK size: <span id="aaak-size">0B</span></div>
</div>
<div class="mem-palace-logs" id="mem-palace-logs"></div>
</div>
<div class="default-reviewers" style="margin-top: 8px; font-size: 12px; color: #aaa;">
<strong>DEFAULT REVIEWERS</strong><br>
<ul style="margin:0; padding-left:15px;">
<li><span style="color:#4af0c0;">@perplexity</span> (QA gate on all repos)</li>
<li><span style="color:#7b5cff;">@Timmy</span> (owner gate on hermes-agent)</li>
</ul>
</div>
<div class="implementation-status" style="margin-top: 10px; font-size: 12px; color: #aaa;">
<strong>IMPLEMENTATION STATUS</strong><br>
<div style="margin-top: 5px; display: flex; flex-direction: column; gap: 2px;">
<div><span style="color:#4af0c0;">hermes-agent</span>: Require PR + 1 approval + CI ✅</div>
<div><span style="color:#7b5cff;">the-nexus</span>: Require PR + 1 approval ⚠️ (CI disabled)</div>
</div>
</div>
<div id="mem-palace-status" style="position:fixed; right:24px; top:64px; background:rgba(74,240,192,0.1); color:#4af0c0; padding:6px 12px; border-radius:4px; font-family:'Orbitron', sans-serif; font-size:10px; letter-spacing:0.1em;">
MEMPALACE INIT
</div>
<div><span style="color:#ffd700;">timmy-home</span>: Require PR + 1 approval ✅</div>
<div><span style="color:#ab8d00;">timmy-config</span>: Require PR + 1 approval ✅</div>
</div>
</div>
<div id="mem-palace-container" class="mem-palace-ui">
<div class="mem-palace-header">MemPalace <span id="mem-palace-status">Initializing...</span></div>
<div class="mem-palace-stats">
<div>Compression: <span id="compression-ratio">--</span>x</div>
<div>Docs mined: <span id="docs-mined">0</span></div>
<div>AAAK size: <span id="aaak-size">0B</span></div>
</div>
<div class="mem-palace-actions">
<button id="mine-now-btn" class="mem-palace-btn" onclick="mineChatToMemPalace()">Mine Chat</button>
<button class="mem-palace-btn" onclick="searchMemPalace()">Search</button>
</div>
<div id="mem-palace-logs" class="mem-palace-logs"></div>
</div>
<div id="mem-palace-controls" style="position:fixed; right:24px; top:54px; background:rgba(74,240,192,0.05); padding:4px 8px; font-family:'JetBrains Mono',monospace; font-size:11px; border-left:2px solid #4af0c0;">
<button onclick="mineMemPalace()">Mine Chat</button>
<button onclick="searchMemPalace()">Search</button>
</div>
<div id="mempalace-results" style="position:fixed; right:24px; top:84px; max-height:200px; overflow-y:auto; background:rgba(0,0,0,0.3); padding:8px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#e0f0ff; border-left:2px solid #4af0c0;"></div>
<div id="mem-palace-controls" style="position:fixed; right:24px; top:54px; background:rgba(74,240,192,0.05); padding:4px 8px; font-family:'JetBrains Mono',monospace; font-size:10px; border-left:2px solid #4af0c0;">
<button class="mem-palace-mining-btn" onclick="mineChatToMemPalace()">Mine Chat</button>
<button onclick="searchMemPalace()">Search</button>
</div>
<div id="mempalace-results" style="position:fixed; right:24px; top:84px; max-height:200px; overflow-y:auto; background:rgba(0,0,0,0.3); padding:8px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#e0f0ff; border-left:2px solid #4af0c0;"></div>
```
index.html
```html
<div class="branch-policy" style="margin-top: 10px; font-size: 12px; color: #aaa;">
<strong>BRANCH PROTECTION POLICY</strong><br>
<ul style="margin:0; padding-left:15px;">
<li>• Require PR for merge ✅</li>
<li>• Require 1 approval ✅</li>
<li>• Dismiss stale approvals ✅</li>
<li>• Require CI ✅ (where available)</li>
<li>• Block force push ✅</li>
<li>• Block branch deletion ✅</li>
</ul>
</div>
<div class="default-reviewers" style="margin-top: 8px;">
<strong>DEFAULT REVIEWERS</strong><br>
<ul style="margin:0; padding-left:15px;">
<li><span style="color:#4af0c0;">@perplexity</span> (QA gate on all repos)</li>
<li><span style="color:#7b5cff;">@Timmy</span> (owner gate on hermes-agent)</li>
</ul>
</div>
<div class="implementation-status" style="margin-top: 10px;">
<strong>IMPLEMENTATION STATUS</strong><br>
<div style="margin-top: 5px; display: flex; flex-direction: column; gap: 2px;">
<div><span style="color:#4af0c0;">hermes-agent</span>: Require PR + 1 approval + CI ✅</div>
<div><span style="color:#7b5cff;">the-nexus</span>: Require PR + 1 approval ⚠<> (CI disabled)</div>
<div><span style="color:#ffd700;">timmy-home</span>: Require PR + 1 approval ✅</div>
<div><span style="color:#ab8d00;">timmy-config</span>: Require PR + 1 approval ✅</div>
</div>
</div>
</footer>
<script type="module" src="./app.js"></script>

View File

@@ -5,6 +5,7 @@
"description": "The Vvardenfell harness. Ash storms and ancient mysteries.",
"status": "online",
"color": "#ff6600",
"role": "pilot",
"position": { "x": 15, "y": 0, "z": -10 },
"rotation": { "y": -0.5 },
"portal_type": "game-world",
@@ -36,6 +37,7 @@
"description": "Calradia battle harness. Massive armies, tactical command.",
"status": "downloaded",
"color": "#ffd700",
"role": "pilot",
"position": { "x": -15, "y": 0, "z": -10 },
"rotation": { "y": 0.5 },
"portal_type": "game-world",
@@ -67,6 +69,7 @@
"description": "The creative harness. Build, script, and manifest.",
"status": "online",
"color": "#4af0c0",
"role": "timmy",
"position": { "x": 0, "y": 0, "z": -20 },
"rotation": { "y": 0 },
"destination": {
@@ -81,6 +84,7 @@
"description": "The repository of all knowledge. History, logs, and ancient data.",
"status": "online",
"color": "#0066ff",
"role": "timmy",
"position": { "x": 25, "y": 0, "z": 0 },
"rotation": { "y": -1.57 },
"destination": {
@@ -95,6 +99,7 @@
"description": "A sanctuary for reflection and digital peace.",
"status": "online",
"color": "#ffd700",
"role": "timmy",
"position": { "x": -25, "y": 0, "z": 0 },
"rotation": { "y": 1.57 },
"destination": {
@@ -109,6 +114,7 @@
"description": "The open nexus. A place for agents to gather and connect.",
"status": "online",
"color": "#4af0c0",
"role": "reflex",
"position": { "x": 15, "y": 0, "z": 10 },
"rotation": { "y": -2.5 },
"destination": {
@@ -123,6 +129,7 @@
"description": "The transition point. Entry and exit from the Nexus core.",
"status": "standby",
"color": "#ff4466",
"role": "reflex",
"position": { "x": -15, "y": 0, "z": 10 },
"rotation": { "y": 2.5 },
"destination": {

View File

@@ -384,6 +384,19 @@ canvas#nexus-canvas {
color: rgba(160, 184, 208, 0.6);
}
.atlas-card-role {
font-family: var(--font-display);
font-size: 9px;
font-weight: 700;
letter-spacing: 1px;
padding: 2px 6px;
border-radius: 3px;
text-transform: uppercase;
}
.atlas-card-role.role-timmy { color: #4af0c0; background: rgba(74, 240, 192, 0.12); border: 1px solid rgba(74, 240, 192, 0.3); }
.atlas-card-role.role-reflex { color: #ff4466; background: rgba(255, 68, 102, 0.12); border: 1px solid rgba(255, 68, 102, 0.3); }
.atlas-card-role.role-pilot { color: #ffd700; background: rgba(255, 215, 0, 0.12); border: 1px solid rgba(255, 215, 0, 0.3); }
.atlas-footer {
padding: 15px 30px;
border-top: 1px solid var(--color-border);
@@ -836,6 +849,70 @@ canvas#nexus-canvas {
color: var(--color-text-muted);
}
/* Timmy Action Stream (Evennia command/result flow) — issue #729 */
.action-stream {
position: absolute;
bottom: 200px;
right: var(--space-3);
width: 320px;
max-height: 260px;
background: rgba(0, 0, 0, 0.65);
backdrop-filter: blur(8px);
border-left: 2px solid var(--color-gold);
padding: var(--space-3);
font-size: 10px;
font-family: var(--font-mono);
pointer-events: none;
overflow: hidden;
display: flex;
flex-direction: column;
}
.action-stream-header {
font-family: var(--font-display);
color: var(--color-gold);
letter-spacing: 0.1em;
font-size: 10px;
margin-bottom: var(--space-2);
opacity: 0.9;
}
.action-stream-icon {
margin-right: 4px;
}
.action-stream-room {
color: var(--color-primary);
font-size: 11px;
font-weight: 600;
margin-bottom: var(--space-1);
opacity: 0.9;
}
.action-stream-content {
display: flex;
flex-direction: column;
gap: 3px;
overflow-y: auto;
flex: 1;
}
.as-entry {
animation: log-fade-in 0.4s ease-out forwards;
opacity: 0;
line-height: 1.4;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.as-cmd .as-prefix { color: var(--color-gold); font-weight: 700; }
.as-cmd .as-text { color: var(--color-gold); opacity: 0.85; }
.as-result .as-prefix { color: var(--color-primary); font-weight: 700; }
.as-result .as-text { color: var(--color-text-muted); }
.as-room .as-prefix { color: var(--color-secondary); font-weight: 700; }
.as-room .as-text { color: var(--color-secondary); opacity: 0.8; }
.as-ts {
color: var(--color-text-muted);
opacity: 0.4;
font-size: 9px;
float: right;
}
/* Vision Hint */
.vision-hint {
position: absolute;
@@ -1239,6 +1316,10 @@ canvas#nexus-canvas {
.hud-agent-log {
width: 220px;
}
.action-stream {
width: 240px;
bottom: 180px;
}
}
@media (max-width: 768px) {