mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-06 23:49:41 +02:00
chore: version packages
This commit is contained in:
@@ -1,14 +1,13 @@
|
||||
/**
|
||||
* Planner Prompt - Single agent for TDD plan generation
|
||||
*
|
||||
* Combines what was previously 5 separate agents:
|
||||
* - Requirements Analyst (redundant)
|
||||
* - Architecture Planner (redundant)
|
||||
* - Testing Specialist (kept - TDD focus)
|
||||
* - Risk Analyst (redundant)
|
||||
* - Verification Expert (kept - structure)
|
||||
* @fileoverview Planner Prompt — single TDD plan generator combining
|
||||
* requirements analysis, architecture, testing, risk, and verification
|
||||
* into one agent (previously 5 separate agents).
|
||||
*
|
||||
* Placeholders: {TASK}, {RESEARCH_CONTEXT}
|
||||
*
|
||||
* @dependencies none (pure template)
|
||||
* @consumedby prompts/index (re-export), plan-orchestrator
|
||||
* @module prompts/planner
|
||||
*/
|
||||
|
||||
export const PLANNER_PROMPT = `You are a TDD Plan Generator. Create a complete implementation plan with test-first approach.
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
/**
|
||||
* Research Agent Prompt
|
||||
*
|
||||
* Gathers external resources, codebase patterns, and technical context
|
||||
* before other agents analyze the task.
|
||||
* @fileoverview Research Agent Prompt — gathers codebase patterns, external
|
||||
* resources, and technical context before the planner analyzes the task.
|
||||
*
|
||||
* Placeholders: {TASK}, {WORKING_DIR}
|
||||
*
|
||||
* @dependencies none (pure template)
|
||||
* @consumedby prompts/index (re-export), plan-orchestrator
|
||||
* @module prompts/research-agent
|
||||
*/
|
||||
|
||||
export const RESEARCH_AGENT_PROMPT = `You are a Research Specialist preparing context for an implementation task. Your job is to gather all relevant information that will help the development team succeed.
|
||||
|
||||
+20
-1
@@ -54,6 +54,7 @@ import {
|
||||
todoContentHash,
|
||||
stringSimilarity,
|
||||
Debouncer,
|
||||
CleanupManager,
|
||||
} from './utils/index.js';
|
||||
import { MAX_LINE_BUFFER_SIZE } from './config/buffer-limits.js';
|
||||
import { MAX_TODOS_PER_SESSION } from './config/map-limits.js';
|
||||
@@ -76,12 +77,19 @@ export type { EnhancedPlanTask, CheckpointReview } from './ralph-plan-tracker.js
|
||||
const TODO_EXPIRY_MS = 60 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Minimum interval between cleanup checks (in milliseconds).
|
||||
* Minimum interval between on-demand cleanup checks (in milliseconds).
|
||||
* Prevents running cleanup on every data chunk.
|
||||
* Default: 30 seconds
|
||||
*/
|
||||
const CLEANUP_THROTTLE_MS = 30 * 1000;
|
||||
|
||||
/**
|
||||
* Interval for periodic todo expiry cleanup (in milliseconds).
|
||||
* Actively purges expired todos even when no terminal data is flowing.
|
||||
* Default: 5 minutes
|
||||
*/
|
||||
const TODO_CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
|
||||
|
||||
/**
|
||||
* Similarity threshold for todo deduplication.
|
||||
* Todos with similarity >= this value are considered duplicates.
|
||||
@@ -517,6 +525,9 @@ export class RalphTracker extends EventEmitter {
|
||||
/** Last calculated completion confidence */
|
||||
private _lastCompletionConfidence: CompletionConfidence | undefined;
|
||||
|
||||
/** Manages periodic cleanup timers (todo expiry) */
|
||||
private cleanup = new CleanupManager();
|
||||
|
||||
/** Confidence threshold for triggering completion (0-100) */
|
||||
private static readonly COMPLETION_CONFIDENCE_THRESHOLD = 70;
|
||||
|
||||
@@ -536,6 +547,12 @@ export class RalphTracker extends EventEmitter {
|
||||
|
||||
// Wire sub-module events
|
||||
this._wireSubModuleEvents();
|
||||
|
||||
// Periodic cleanup of expired todos — ensures stale entries are purged
|
||||
// even when no terminal data is flowing (e.g., idle sessions)
|
||||
this.cleanup.setInterval(() => this.cleanupExpiredTodos(), TODO_CLEANUP_INTERVAL_MS, {
|
||||
description: 'ralph todo expiry cleanup',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2204,6 +2221,7 @@ export class RalphTracker extends EventEmitter {
|
||||
if (toDelete.length > 0) {
|
||||
for (const id of toDelete) {
|
||||
this._todos.delete(id);
|
||||
this._todoStartTimes.delete(id);
|
||||
}
|
||||
this.emit('todoUpdate', this.todos);
|
||||
}
|
||||
@@ -2348,6 +2366,7 @@ export class RalphTracker extends EventEmitter {
|
||||
* Clean up all resources and release memory.
|
||||
*/
|
||||
destroy(): void {
|
||||
this.cleanup.dispose();
|
||||
this._todoDeb.dispose();
|
||||
this._loopDeb.dispose();
|
||||
this.fixPlanWatcher.destroy();
|
||||
|
||||
+61
-24
@@ -196,6 +196,7 @@ const _SSE_HANDLER_MAP = [
|
||||
class CodemanApp {
|
||||
constructor() {
|
||||
this.sessions = new Map();
|
||||
this._shortIdCache = new Map(); // Cache session ID .slice(0, 8) results
|
||||
this.sessionOrder = []; // Track tab order for drag-and-drop reordering
|
||||
this.draggedTabId = null; // Currently dragged tab session ID
|
||||
this.cases = [];
|
||||
@@ -1558,16 +1559,25 @@ class CodemanApp {
|
||||
this.sseReconnectTimeout = setTimeout(() => this.connectSSE(), delay);
|
||||
};
|
||||
|
||||
// Create stable handler wrappers once (reused across reconnects so
|
||||
// removeEventListener always matches the original reference)
|
||||
if (!this._sseHandlerWrappers) {
|
||||
this._sseHandlerWrappers = new Map();
|
||||
for (const [event, method] of _SSE_HANDLER_MAP) {
|
||||
const fn = this[method];
|
||||
this._sseHandlerWrappers.set(event, (e) => {
|
||||
try {
|
||||
fn.call(this, e.data ? JSON.parse(e.data) : {});
|
||||
} catch (err) {
|
||||
console.error(`[SSE] Error handling ${event}:`, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Register all SSE event handlers via centralized map
|
||||
for (const [event, method] of _SSE_HANDLER_MAP) {
|
||||
const fn = this[method];
|
||||
addListener(event, (e) => {
|
||||
try {
|
||||
fn.call(this, e.data ? JSON.parse(e.data) : {});
|
||||
} catch (err) {
|
||||
console.error(`[SSE] Error handling ${event}:`, err);
|
||||
}
|
||||
});
|
||||
for (const [event] of _SSE_HANDLER_MAP) {
|
||||
addListener(event, this._sseHandlerWrappers.get(event));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1711,7 +1721,7 @@ class CodemanApp {
|
||||
urgency: 'critical',
|
||||
category: 'session-error',
|
||||
sessionId: data.id,
|
||||
sessionName: session?.name || data.id?.slice(0, 8),
|
||||
sessionName: session?.name || this.getShortId(data.id),
|
||||
title: 'Session Error',
|
||||
message: data.error || 'Unknown error',
|
||||
});
|
||||
@@ -1730,7 +1740,7 @@ class CodemanApp {
|
||||
urgency: 'critical',
|
||||
category: 'session-crash',
|
||||
sessionId: data.id,
|
||||
sessionName: session?.name || data.id?.slice(0, 8),
|
||||
sessionName: session?.name || this.getShortId(data.id),
|
||||
title: 'Session Crashed',
|
||||
message: `Exited with code ${data.code}`,
|
||||
});
|
||||
@@ -1755,7 +1765,7 @@ class CodemanApp {
|
||||
urgency: 'warning',
|
||||
category: 'session-stuck',
|
||||
sessionId: data.id,
|
||||
sessionName: s?.name || data.id?.slice(0, 8),
|
||||
sessionName: s?.name || this.getShortId(data.id),
|
||||
title: 'Session Idle',
|
||||
message: `Idle for ${Math.round(threshold / 60000)}+ minutes`,
|
||||
});
|
||||
@@ -1794,7 +1804,7 @@ class CodemanApp {
|
||||
urgency: 'info',
|
||||
category: 'auto-clear',
|
||||
sessionId: data.sessionId,
|
||||
sessionName: session?.name || data.sessionId?.slice(0, 8),
|
||||
sessionName: session?.name || this.getShortId(data.sessionId),
|
||||
title: 'Auto-Cleared',
|
||||
message: `Context reset at ${(data.tokens || 0).toLocaleString()} tokens`,
|
||||
});
|
||||
@@ -1880,7 +1890,7 @@ class CodemanApp {
|
||||
urgency: 'critical',
|
||||
category: 'respawn-blocked',
|
||||
sessionId: data.sessionId,
|
||||
sessionName: session?.name || data.sessionId?.slice(0, 8),
|
||||
sessionName: session?.name || this.getShortId(data.sessionId),
|
||||
title,
|
||||
message: data.details,
|
||||
});
|
||||
@@ -1900,7 +1910,7 @@ class CodemanApp {
|
||||
urgency: 'info',
|
||||
category: 'auto-accept',
|
||||
sessionId: data.sessionId,
|
||||
sessionName: session?.name || data.sessionId?.slice(0, 8),
|
||||
sessionName: session?.name || this.getShortId(data.sessionId),
|
||||
title: 'Plan Accepted',
|
||||
message: `Accepted plan mode for ${session?.name || 'session'}`,
|
||||
});
|
||||
@@ -2031,7 +2041,7 @@ class CodemanApp {
|
||||
_onMuxDied(data) {
|
||||
this.muxSessions = this.muxSessions.filter(s => s.sessionId !== data.sessionId);
|
||||
this.renderMuxSessions();
|
||||
this.showToast('Mux session died: ' + data.sessionId.slice(0, 8), 'warning');
|
||||
this.showToast('Mux session died: ' + this.getShortId(data.sessionId), 'warning');
|
||||
}
|
||||
|
||||
_onMuxStatsUpdated(data) {
|
||||
@@ -2081,7 +2091,7 @@ class CodemanApp {
|
||||
urgency: 'warning',
|
||||
category: 'ralph-complete',
|
||||
sessionId: data.sessionId,
|
||||
sessionName: session?.name || data.sessionId?.slice(0, 8),
|
||||
sessionName: session?.name || this.getShortId(data.sessionId),
|
||||
title: 'Loop Complete',
|
||||
message: `Completion: ${data.phrase || 'unknown'}`,
|
||||
});
|
||||
@@ -2104,7 +2114,7 @@ class CodemanApp {
|
||||
urgency: 'critical',
|
||||
category: 'circuit-breaker',
|
||||
sessionId: data.sessionId,
|
||||
sessionName: session?.name || data.sessionId?.slice(0, 8),
|
||||
sessionName: session?.name || this.getShortId(data.sessionId),
|
||||
title: 'Circuit Breaker Open',
|
||||
message: data.status.reason || 'Loop stuck - no progress detected',
|
||||
});
|
||||
@@ -2117,7 +2127,7 @@ class CodemanApp {
|
||||
urgency: 'warning',
|
||||
category: 'exit-gate',
|
||||
sessionId: data.sessionId,
|
||||
sessionName: session?.name || data.sessionId?.slice(0, 8),
|
||||
sessionName: session?.name || this.getShortId(data.sessionId),
|
||||
title: 'Exit Gate Met',
|
||||
message: `Loop ready to exit (indicators: ${data.completionIndicators})`,
|
||||
});
|
||||
@@ -3273,6 +3283,16 @@ class CodemanApp {
|
||||
// Session Lifecycle — select, close, navigate
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
|
||||
getShortId(id) {
|
||||
if (!id) return '';
|
||||
let short = this._shortIdCache.get(id);
|
||||
if (!short) {
|
||||
short = id.slice(0, 8);
|
||||
this._shortIdCache.set(id, short);
|
||||
}
|
||||
return short;
|
||||
}
|
||||
|
||||
getSessionName(session) {
|
||||
// Use custom name if set
|
||||
if (session.name) {
|
||||
@@ -3282,7 +3302,7 @@ class CodemanApp {
|
||||
if (session.workingDir) {
|
||||
return session.workingDir.split('/').pop() || session.workingDir;
|
||||
}
|
||||
return session.id.slice(0, 8);
|
||||
return this.getShortId(session.id);
|
||||
}
|
||||
|
||||
async selectSession(sessionId) {
|
||||
@@ -6619,7 +6639,7 @@ class CodemanApp {
|
||||
tbody.innerHTML = data.entries.map(e => {
|
||||
const time = new Date(e.ts).toLocaleString();
|
||||
const color = eventColors[e.event] || '#888';
|
||||
const name = e.name || (e.sessionId === '*' ? '—' : e.sessionId.slice(0, 8));
|
||||
const name = e.name || (e.sessionId === '*' ? '—' : this.getShortId(e.sessionId));
|
||||
const extra = [];
|
||||
if (e.exitCode !== undefined && e.exitCode !== null) extra.push(`code=${e.exitCode}`);
|
||||
if (e.mode) extra.push(e.mode);
|
||||
@@ -9386,7 +9406,7 @@ class CodemanApp {
|
||||
if (windowData.resizeObserver) {
|
||||
windowData.resizeObserver.disconnect();
|
||||
}
|
||||
// Clean up global drag event listeners (prevents memory leak)
|
||||
// Clean up drag event listeners (both document-level and handle-level)
|
||||
if (windowData.dragListeners) {
|
||||
document.removeEventListener('mousemove', windowData.dragListeners.move);
|
||||
document.removeEventListener('mouseup', windowData.dragListeners.up);
|
||||
@@ -9395,6 +9415,11 @@ class CodemanApp {
|
||||
document.removeEventListener('touchend', windowData.dragListeners.up);
|
||||
document.removeEventListener('touchcancel', windowData.dragListeners.up);
|
||||
}
|
||||
// Remove handle-level listeners before DOM removal
|
||||
if (windowData.dragListeners.handle) {
|
||||
windowData.dragListeners.handle.removeEventListener('mousedown', windowData.dragListeners.handleMouseDown);
|
||||
windowData.dragListeners.handle.removeEventListener('touchstart', windowData.dragListeners.handleTouchStart);
|
||||
}
|
||||
}
|
||||
windowData.element.remove();
|
||||
this.subagentWindows.delete(agentId);
|
||||
@@ -9986,6 +10011,10 @@ class CodemanApp {
|
||||
document.removeEventListener('touchend', this.teamTasksDragListeners.up);
|
||||
document.removeEventListener('touchcancel', this.teamTasksDragListeners.up);
|
||||
}
|
||||
if (this.teamTasksDragListeners.handle) {
|
||||
this.teamTasksDragListeners.handle.removeEventListener('mousedown', this.teamTasksDragListeners.handleMouseDown);
|
||||
this.teamTasksDragListeners.handle.removeEventListener('touchstart', this.teamTasksDragListeners.handleTouchStart);
|
||||
}
|
||||
this.teamTasksDragListeners = null;
|
||||
}
|
||||
}
|
||||
@@ -10743,10 +10772,14 @@ class CodemanApp {
|
||||
windowData.eventSource.close();
|
||||
}
|
||||
|
||||
// Clean up global drag event listeners (prevents memory leak)
|
||||
// Clean up drag event listeners (both document-level and handle-level)
|
||||
if (windowData.dragListeners) {
|
||||
document.removeEventListener('mousemove', windowData.dragListeners.move);
|
||||
document.removeEventListener('mouseup', windowData.dragListeners.up);
|
||||
if (windowData.dragListeners.handle) {
|
||||
windowData.dragListeners.handle.removeEventListener('mousedown', windowData.dragListeners.handleMouseDown);
|
||||
windowData.dragListeners.handle.removeEventListener('touchstart', windowData.dragListeners.handleTouchStart);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove element
|
||||
@@ -10870,7 +10903,7 @@ class CodemanApp {
|
||||
const popupData = this.imagePopups.get(imageId);
|
||||
if (!popupData) return;
|
||||
|
||||
// Clean up global drag event listeners
|
||||
// Clean up drag event listeners (both document-level and handle-level)
|
||||
if (popupData.dragListeners) {
|
||||
document.removeEventListener('mousemove', popupData.dragListeners.move);
|
||||
document.removeEventListener('mouseup', popupData.dragListeners.up);
|
||||
@@ -10879,6 +10912,10 @@ class CodemanApp {
|
||||
document.removeEventListener('touchend', popupData.dragListeners.up);
|
||||
document.removeEventListener('touchcancel', popupData.dragListeners.up);
|
||||
}
|
||||
if (popupData.dragListeners.handle) {
|
||||
popupData.dragListeners.handle.removeEventListener('mousedown', popupData.dragListeners.handleMouseDown);
|
||||
popupData.dragListeners.handle.removeEventListener('touchstart', popupData.dragListeners.handleTouchStart);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove element
|
||||
|
||||
@@ -1012,19 +1012,20 @@ Object.assign(CodemanApp.prototype, {
|
||||
}
|
||||
};
|
||||
|
||||
// Mouse events
|
||||
handle.addEventListener('mousedown', (e) => {
|
||||
// Named handle-level listeners (stored for explicit cleanup on window close)
|
||||
const handleMouseDown = (e) => {
|
||||
if (e.target.tagName === 'BUTTON') return;
|
||||
startDrag(e.clientX, e.clientY);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
// Touch events
|
||||
handle.addEventListener('touchstart', (e) => {
|
||||
};
|
||||
const handleTouchStart = (e) => {
|
||||
if (e.target.tagName === 'BUTTON') return;
|
||||
const touch = e.touches[0];
|
||||
startDrag(touch.clientX, touch.clientY);
|
||||
}, { passive: true });
|
||||
};
|
||||
|
||||
handle.addEventListener('mousedown', handleMouseDown);
|
||||
handle.addEventListener('touchstart', handleTouchStart, { passive: true });
|
||||
|
||||
// Store references to document-level listeners so they can be removed on window close
|
||||
const moveListener = (e) => {
|
||||
@@ -1048,8 +1049,15 @@ Object.assign(CodemanApp.prototype, {
|
||||
document.addEventListener('touchend', upListener);
|
||||
document.addEventListener('touchcancel', upListener);
|
||||
|
||||
// Return listener references for cleanup
|
||||
return { move: moveListener, up: upListener, touchMove: touchMoveListener };
|
||||
// Return all listener references for cleanup (both handle-level and document-level)
|
||||
return {
|
||||
move: moveListener,
|
||||
up: upListener,
|
||||
touchMove: touchMoveListener,
|
||||
handle,
|
||||
handleMouseDown,
|
||||
handleTouchStart,
|
||||
};
|
||||
},
|
||||
|
||||
// Show subagent dropdown on hover
|
||||
|
||||
+16
-11
@@ -270,6 +270,8 @@ export class WebServer extends EventEmitter {
|
||||
error: (error: Error, sessionId?: string) => void;
|
||||
} | null = null;
|
||||
private tunnelManager: TunnelManager = new TunnelManager();
|
||||
/** Cached tunnel active state — updated on TunnelStarted/TunnelStopped to avoid getUrl() on every broadcast */
|
||||
private _isTunnelActive: boolean = false;
|
||||
private authSessions: StaleExpirationMap<string, import('./ports/auth-port.js').AuthSessionRecord> | null = null;
|
||||
private authFailures: StaleExpirationMap<string, number> | null = null;
|
||||
private qrAuthFailures: StaleExpirationMap<string, number> | null = null;
|
||||
@@ -326,9 +328,11 @@ export class WebServer extends EventEmitter {
|
||||
|
||||
// Set up tunnel manager listeners
|
||||
this.tunnelManager.on('started', (data: { url: string }) => {
|
||||
this._isTunnelActive = true;
|
||||
this.broadcast(SseEvent.TunnelStarted, data);
|
||||
});
|
||||
this.tunnelManager.on('stopped', () => {
|
||||
this._isTunnelActive = false;
|
||||
this.broadcast(SseEvent.TunnelStopped, {});
|
||||
});
|
||||
this.tunnelManager.on('error', (message: string) => {
|
||||
@@ -601,7 +605,7 @@ export class WebServer extends EventEmitter {
|
||||
this.sendSSE(reply, SseEvent.Init, this.getLightState());
|
||||
// Flush Cloudflare tunnel buffer with padding — ensures the init event
|
||||
// (and any immediately following events) are delivered without proxy delay.
|
||||
if (this.tunnelManager.getUrl()) {
|
||||
if (this._isTunnelActive) {
|
||||
try {
|
||||
reply.raw.write(SSE_PADDING);
|
||||
} catch {
|
||||
@@ -1931,21 +1935,22 @@ export class WebServer extends EventEmitter {
|
||||
}
|
||||
|
||||
private broadcast(event: string, data: unknown): void {
|
||||
// Invalidate caches only on structurally significant events — ones that
|
||||
// change session list content (creation, deletion, or full state refresh).
|
||||
// High-frequency non-structural events (working/idle transitions, completion,
|
||||
// error, respawn state changes) are NOT worth invalidating for because:
|
||||
// 1. The debounced session:updated follows within 500ms with the new state
|
||||
// 2. These caches serve /api/sessions and SSE init — neither is polled rapidly
|
||||
// 3. Invalidating on every working/idle transition makes the 1s TTL useless
|
||||
if (event === SseEvent.SessionCreated || event === SseEvent.SessionDeleted || event === SseEvent.SessionUpdated) {
|
||||
// Skip serialization entirely when no clients are listening
|
||||
if (this.sseClients.size === 0) return;
|
||||
|
||||
// Invalidate caches only on structural changes (creation/deletion).
|
||||
// SessionUpdated fires too frequently (working/idle transitions, completion)
|
||||
// and makes the 1s TTL cache useless — the debounced session:updated follows
|
||||
// within 500ms anyway, and these caches serve /api/sessions and SSE init
|
||||
// which aren't polled rapidly.
|
||||
if (event === SseEvent.SessionCreated || event === SseEvent.SessionDeleted) {
|
||||
this.cachedLightState = null;
|
||||
this.cachedSessionsList = null;
|
||||
}
|
||||
// Performance optimization: serialize JSON once for all clients.
|
||||
// Only append Cloudflare tunnel padding when tunnel is actually active —
|
||||
// direct/Tailscale clients don't need 8KB padding on every event.
|
||||
const padding = this.tunnelManager.getUrl() ? SSE_PADDING : '';
|
||||
const padding = this._isTunnelActive ? SSE_PADDING : '';
|
||||
let message: string;
|
||||
try {
|
||||
message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n` + padding;
|
||||
@@ -2221,7 +2226,7 @@ export class WebServer extends EventEmitter {
|
||||
// Send SSE comment as keep-alive. Only add padding when tunnel is
|
||||
// active — it flushes Cloudflare proxy buffers but wastes bandwidth
|
||||
// for direct/Tailscale connections.
|
||||
const ka = this.tunnelManager.getUrl() ? ':keepalive\n' + SSE_PADDING : ':keepalive\n\n';
|
||||
const ka = this._isTunnelActive ? ':keepalive\n' + SSE_PADDING : ':keepalive\n\n';
|
||||
client.raw.write(ka);
|
||||
}
|
||||
} catch {
|
||||
|
||||
Reference in New Issue
Block a user