diff --git a/.activate b/.activate deleted file mode 100755 index 9b6dc774c..000000000 --- a/.activate +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -# Activate Agent-Agnostic Coding Tools environment -export CODING_REPO="/Users/q284340/Agentic/coding" -export PATH="/Users/q284340/Agentic/coding/bin:$PATH" -echo "āœ… Agent-Agnostic Coding Tools environment activated!" -echo "Commands 'ukb', 'vkb', and 'coding' are now available." -echo "" -echo "Usage:" -echo " coding # Use best available agent" -echo " coding --copilot # Force CoPilot" -echo " coding --claude # Force Claude Code" diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 000000000..1d4ff7b8c --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1 @@ +module.exports = { extends: "eslint:recommended", env: { node: true, es6: true }, parserOptions: { ecmaVersion: 2022, sourceType: "module" } }; diff --git a/.gitmodules b/.gitmodules index e59d23cd2..ebb938d8d 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,12 +1,12 @@ [submodule "integrations/mcp-constraint-monitor"] path = integrations/mcp-constraint-monitor - url = git@github.com:fwornle/mcp-constraint-monitor.git + url = https://github.com/fwornle/mcp-constraint-monitor.git [submodule "integrations/memory-visualizer"] path = integrations/memory-visualizer - url = git@github.com:fwornle/memory-visualizer.git + url = https://github.com/fwornle/memory-visualizer.git [submodule "integrations/mcp-server-semantic-analysis"] path = integrations/mcp-server-semantic-analysis - url = git@github.com:fwornle/mcp-server-semantic-analysis.git + url = https://github.com/fwornle/mcp-server-semantic-analysis.git [submodule "integrations/serena"] path = integrations/serena url = https://github.com/oraios/serena.git diff --git a/.lsl/config.json b/.lsl/config.json index 621792c65..1ab6c3fe8 100644 --- a/.lsl/config.json +++ b/.lsl/config.json @@ -41,8 +41,8 @@ "log_level": "info" }, "deployment": { - "deployment_id": "lsl-20251115-105627", - "deployed_at": "2025-11-15T09:56:27Z", + "deployment_id": "lsl-20251201-123841", + "deployed_at": "2025-12-01T11:38:41Z", "version": "2.0.0", "components": [ "enhanced-redaction-system", diff --git a/bin/coding b/bin/coding index c408af3b2..5566462b6 100755 --- a/bin/coding +++ b/bin/coding @@ -9,6 +9,7 @@ set -e AGENT="" FORCE_AGENT="" ARGS=() +SERVICE_ARGS=() VERBOSE=false CONFIG_FILE="" PROJECT_DIR="" @@ -105,6 +106,14 @@ while [[ $# -gt 0 ]]; do --lsl-validate) exec node "$SCRIPT_DIR/../tests/integration/full-system-validation.test.js" ;; + board|consensus) + shift + exec node "$SCRIPT_DIR/../scripts/run-board-meeting.js" "$@" + ;; + --no-vkb|--no-constraints|--no-transcript|--no-logging|--no-health) + SERVICE_ARGS+=("$1") + shift + ;; --help|-h) show_help exit 0 @@ -177,6 +186,8 @@ export CODING_AGENT="$AGENT" export CODING_TOOLS_PATH="$SCRIPT_DIR/.." export CODING_REPO="$SCRIPT_DIR/.." export CODING_PROJECT_DIR="$PROJECT_DIR" +# Pass service args as a space-separated string +export SERVICE_ARGS_STR="${SERVICE_ARGS[*]}" # Launch appropriate agent case "$AGENT" in diff --git a/enhanced-lsl-deployment-report.md b/enhanced-lsl-deployment-report.md index f6fcdd750..4c034bb66 100644 --- a/enhanced-lsl-deployment-report.md +++ b/enhanced-lsl-deployment-report.md @@ -1,7 +1,7 @@ # Enhanced LSL System Deployment Report -**Deployment ID:** lsl-20251115-105627 -**Deployment Date:** 2025-11-15 09:56:28 UTC +**Deployment ID:** lsl-20251201-123841 +**Deployment Date:** 2025-12-01 11:38:41 UTC **Deployment Mode:** Development ## Executive Summary diff --git a/integrations/browser-access/package-lock.json b/integrations/browser-access/package-lock.json index deb4f6f29..79dbe2908 100644 --- a/integrations/browser-access/package-lock.json +++ b/integrations/browser-access/package-lock.json @@ -418,6 +418,7 @@ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.52.0.tgz", "integrity": "sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==", "license": "Apache-2.0", + "peer": true, "dependencies": { "playwright": "1.52.0" }, @@ -994,6 +995,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", @@ -2616,6 +2618,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.46.tgz", "integrity": "sha512-IqRxcHEIjqLd4LNS/zKffB3Jzg3NwqJxQQ0Ns7pdrvgGkwQsEBdEQcOHaBVqvvZArShRzI39+aMST3FBGmTrLQ==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/lib/consensus/board-meeting.js b/lib/consensus/board-meeting.js new file mode 100644 index 000000000..2c2b47379 --- /dev/null +++ b/lib/consensus/board-meeting.js @@ -0,0 +1,146 @@ +import LLMClient from './llm-client.js'; +import DIRECTORS from './directors.js'; + +class BoardMeeting { + constructor() { + this.llm = new LLMClient(); + this.directors = Object.values(DIRECTORS); + } + + async conductMeeting(topic) { + console.log(`\nšŸ“¢ Board Meeting Called: "${topic}"\n`); + + // Phase 1: Proposal Generation + console.log("Phase 1: Generating Proposals..."); + const proposals = await this.generateProposals(topic); + + // Phase 2: Anonymization + console.log("Phase 2: Anonymizing Proposals..."); + const anonymizedProposals = this.anonymizeProposals(proposals); + + // Phase 3: Voting + console.log("Phase 3: Voting..."); + const votes = await this.collectVotes(topic, anonymizedProposals); + + // Phase 4: Tally + const result = this.tallyVotes(votes, anonymizedProposals); + + return result; + } + + async generateProposals(topic) { + const proposalPromises = this.directors.map(async (director) => { + try { + console.log(` - ${director.name} is thinking...`); + const response = await this.llm.complete( + director.systemPrompt, + `Please provide your perspective and a concrete proposal on the following topic:\n"${topic}"\nKeep your response concise (under 200 words).` + ); + return { director: director.name, content: response }; + } catch (error) { + console.error(`Error from ${director.name}:`, error.message); + return { director: director.name, content: "Abstained due to technical difficulties." }; + } + }); + + return Promise.all(proposalPromises); + } + + anonymizeProposals(proposals) { + return proposals.map((p, index) => ({ + id: String.fromCharCode(65 + index), // A, B, C, D, E + originalDirector: p.director, + content: p.content + })); + } + + async collectVotes(topic, anonymizedProposals) { + // Format proposals for reading + const proposalsText = anonymizedProposals + .map(p => `Proposal ${p.id}:\n${p.content}\n---`) + .join('\n'); + + const votePromises = this.directors.map(async (director) => { + try { + // Find their own proposal ID to exclude (optional, but good for fairness) + const ownProposal = anonymizedProposals.find(p => p.originalDirector === director.name); + const ownId = ownProposal ? ownProposal.id : null; + + const prompt = ` +Topic: "${topic}" + +Here are 5 anonymous proposals from the board: + +${proposalsText} + +Your Task: +1. Evaluate these proposals based on your core values (${director.role}). +2. Vote for the single best proposal. +3. You CANNOT vote for Proposal ${ownId} (which is your own). +4. Provide a brief 1-sentence reason. + +Format your response exactly like this: +VOTE: [Proposal ID] +REASON: [Your reason] +`; + console.log(` - ${director.name} is voting...`); + const response = await this.llm.complete(director.systemPrompt, prompt); + return this.parseVote(response, director.name); + } catch (error) { + console.error(`Error collecting vote from ${director.name}:`, error.message); + return null; + } + }); + + return Promise.all(votePromises); + } + + parseVote(response, voterName) { + const voteMatch = response.match(/VOTE:\s*([A-E])/i); + const reasonMatch = response.match(/REASON:\s*(.*)/i); + + return { + voter: voterName, + choice: voteMatch ? voteMatch[1].toUpperCase() : null, + reason: reasonMatch ? reasonMatch[1] : "No reason provided." + }; + } + + tallyVotes(votes, anonymizedProposals) { + const validVotes = votes.filter(v => v && v.choice); + const scores = {}; + + // Initialize scores + anonymizedProposals.forEach(p => scores[p.id] = 0); + + // Tally + validVotes.forEach(v => { + if (scores[v.choice] !== undefined) { + scores[v.choice]++; + } + }); + + // Find winner + let maxScore = -1; + let winnerId = null; + + Object.entries(scores).forEach(([id, score]) => { + if (score > maxScore) { + maxScore = score; + winnerId = id; + } + }); + + const winner = anonymizedProposals.find(p => p.id === winnerId); + + return { + topic: "", + winner: winner, + scores: scores, + votes: validVotes, + proposals: anonymizedProposals + }; + } +} + +export default BoardMeeting; diff --git a/lib/consensus/directors.js b/lib/consensus/directors.js new file mode 100644 index 000000000..e170e461c --- /dev/null +++ b/lib/consensus/directors.js @@ -0,0 +1,49 @@ +export const DIRECTORS = { + PRAGMATIST: { + name: "The Pragmatist", + role: "Senior Engineer focused on delivery", + systemPrompt: `You are The Pragmatist. +Your core values are: Simplicity, Speed, MVP, YAGNI (You Ain't Gonna Need It). +You dislike over-engineering, complex abstractions, and premature optimization. +When analyzing a problem, propose the simplest, most direct solution that works. +Focus on "boring" technology and proven patterns.` + }, + ARCHITECT: { + name: "The Architect", + role: "Principal Software Architect", + systemPrompt: `You are The Architect. +Your core values are: Scalability, Maintainability, Design Patterns, SOLID principles. +You think in terms of systems, interfaces, and long-term evolution. +When analyzing a problem, propose a solution that is robust, decoupled, and extensible. +You are willing to accept initial complexity for future flexibility.` + }, + SECURITY: { + name: "The Security Officer", + role: "Security Engineer", + systemPrompt: `You are The Security Officer. +Your core values are: Zero Trust, Data Privacy, Input Validation, Secure Defaults. +You view every feature as a potential attack vector. +When analyzing a problem, focus on how it could be exploited and how to prevent it. +Prioritize safety over convenience or speed.` + }, + PERFORMANCE: { + name: "The Performance Zealot", + role: "Performance Engineer", + systemPrompt: `You are The Performance Zealot. +Your core values are: Latency, Throughput, Memory Usage, O(n). +You obsess over CPU cycles and database queries. +When analyzing a problem, propose the solution that is most efficient. +You are willing to sacrifice readability for raw speed if necessary.` + }, + USER_ADVOCATE: { + name: "The User Advocate", + role: "Product Engineer / DX Specialist", + systemPrompt: `You are The User Advocate. +Your core values are: User Experience (UX), Developer Experience (DX), Clarity, Documentation. +You care about how the code feels to use and read. +When analyzing a problem, propose the solution that is most intuitive and well-documented. +You believe code is for humans first, machines second.` + } +}; + +export default DIRECTORS; diff --git a/lib/consensus/llm-client.js b/lib/consensus/llm-client.js new file mode 100644 index 000000000..129c79fe2 --- /dev/null +++ b/lib/consensus/llm-client.js @@ -0,0 +1,78 @@ +import { spawn } from 'child_process'; + +/** + * Lightweight wrapper to call an LLM. + * Since specific SDKs might not be installed, we'll try to use fetch if API keys are present. + * If not, we'll warn the user. + */ +export class LLMClient { + constructor() { + this.anthropicKey = process.env.ANTHROPIC_API_KEY; + this.openaiKey = process.env.OPENAI_API_KEY; + this.provider = this.anthropicKey ? 'anthropic' : (this.openaiKey ? 'openai' : null); + } + + async complete(systemPrompt, userPrompt) { + if (!this.provider) { + throw new Error('No API keys found. Please set ANTHROPIC_API_KEY or OPENAI_API_KEY.'); + } + + if (this.provider === 'anthropic') { + return this.callAnthropic(systemPrompt, userPrompt); + } else { + return this.callOpenAI(systemPrompt, userPrompt); + } + } + + async callAnthropic(systemPrompt, userPrompt) { + const response = await fetch('https://api.anthropic.com/v1/messages', { + method: 'POST', + headers: { + 'x-api-key': this.anthropicKey, + 'anthropic-version': '2023-06-01', + 'content-type': 'application/json' + }, + body: JSON.stringify({ + model: 'claude-3-5-sonnet-20241022', + max_tokens: 4000, + system: systemPrompt, + messages: [{ role: 'user', content: userPrompt }] + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Anthropic API error: ${response.status} ${response.statusText} - ${errorText}`); + } + + const data = await response.json(); + return data.content[0].text; + } + + async callOpenAI(systemPrompt, userPrompt) { + const response = await fetch('https://api.openai.com/v1/chat/completions', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.openaiKey}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + model: 'gpt-4o', + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: userPrompt } + ] + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`OpenAI API error: ${response.status} ${response.statusText} - ${errorText}`); + } + + const data = await response.json(); + return data.choices[0].message.content; + } +} + +export default LLMClient; diff --git a/scripts/cleanup-aliases.sh b/scripts/cleanup-aliases.sh index a398354e6..21ba5be9c 100755 --- a/scripts/cleanup-aliases.sh +++ b/scripts/cleanup-aliases.sh @@ -1,8 +1,6 @@ #!/bin/bash # Cleanup aliases from current shell session -unalias ukb 2>/dev/null || true unalias vkb 2>/dev/null || true unalias claude-mcp 2>/dev/null || true -unset -f ukb 2>/dev/null || true unset -f vkb 2>/dev/null || true unset -f claude-mcp 2>/dev/null || true diff --git a/scripts/launch-claude.sh b/scripts/launch-claude.sh index 6bb5039ba..806f5b60a 100755 --- a/scripts/launch-claude.sh +++ b/scripts/launch-claude.sh @@ -58,6 +58,9 @@ verify_monitoring_systems() { } +# Reconstruct SERVICE_ARGS array from env var +SERVICE_ARGS=($SERVICE_ARGS_STR) + # Use target project directory if specified, otherwise use coding repo if [ -n "$CODING_PROJECT_DIR" ]; then TARGET_PROJECT_DIR="$CODING_PROJECT_DIR" @@ -136,7 +139,8 @@ if ! command -v node &> /dev/null; then fi # Start services using the simple startup script (from coding repo) -if ! "$CODING_REPO/start-services.sh"; then +# Forward any service-related arguments +if ! "$CODING_REPO/start-services.sh" "${SERVICE_ARGS[@]}"; then log "Error: Failed to start services" exit 1 fi diff --git a/scripts/launch-copilot.sh b/scripts/launch-copilot.sh index 32a470082..282b649f4 100755 --- a/scripts/launch-copilot.sh +++ b/scripts/launch-copilot.sh @@ -111,6 +111,9 @@ start_http_adapter() { fi } +# Reconstruct SERVICE_ARGS array from env var +SERVICE_ARGS=($SERVICE_ARGS_STR) + # Use target project directory if specified, otherwise use coding repo if [ -n "$CODING_PROJECT_DIR" ]; then TARGET_PROJECT_DIR="$CODING_PROJECT_DIR" @@ -148,7 +151,8 @@ if ! command -v node &> /dev/null; then fi # Start services using the simple startup script (from coding repo) -if ! "$CODING_REPO/start-services.sh"; then +# Forward any service-related arguments +if ! "$CODING_REPO/start-services.sh" "${SERVICE_ARGS[@]}"; then log "Error: Failed to start services" exit 1 fi diff --git a/scripts/run-board-meeting.js b/scripts/run-board-meeting.js new file mode 100755 index 000000000..a40c3de89 --- /dev/null +++ b/scripts/run-board-meeting.js @@ -0,0 +1,80 @@ +#!/usr/bin/env node + +import BoardMeeting from '../lib/consensus/board-meeting.js'; +import readline from 'readline'; + +async function main() { + const args = process.argv.slice(2); + let topic = args.join(' '); + + if (!topic) { + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + topic = await new Promise(resolve => { + rl.question('Enter the topic for the Board Meeting: ', (answer) => { + rl.close(); + resolve(answer); + }); + }); + } + + if (!topic) { + console.error("Error: Topic is required."); + process.exit(1); + } + + // Check for API keys + if (!process.env.ANTHROPIC_API_KEY && !process.env.OPENAI_API_KEY) { + console.error("Error: ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable is required."); + process.exit(1); + } + + const meeting = new BoardMeeting(); + + try { + const result = await meeting.conductMeeting(topic); + + console.log("\n" + "=".repeat(50)); + console.log("šŸ MEETING ADJOURNED šŸ"); + console.log("=".repeat(50)); + + if (result.winner) { + console.log(`\nšŸ† WINNER: Proposal ${result.winner.id} (by ${result.winner.originalDirector})`); + console.log(`\nšŸ“„ THE WINNING PROPOSAL:\n${result.winner.content}`); + } else { + console.log("\nšŸ† WINNER: None (No votes cast or tie with no votes)"); + } + + console.log("\n" + "-".repeat(50)); + console.log("šŸ“Š VOTE TALLY"); + console.log("-".repeat(50)); + + // Sort scores + const sortedScores = Object.entries(result.scores).sort((a, b) => b[1] - a[1]); + + sortedScores.forEach(([id, score]) => { + const p = result.proposals.find(prop => prop.id === id); + console.log(`Proposal ${id} (${p.originalDirector}): ${score} votes`); + }); + + console.log("\n" + "-".repeat(50)); + console.log("šŸ—³ļø VOTING RECORDS"); + console.log("-".repeat(50)); + + result.votes.forEach(vote => { + if (vote) { + console.log(`${vote.voter} voted for ${vote.choice}`); + console.log(`Reason: ${vote.reason}\n`); + } + }); + + } catch (error) { + console.error("Board meeting failed:", error); + process.exit(1); + } +} + +main(); diff --git a/scripts/start-services-robust.js b/scripts/start-services-robust.js index a02ebaedd..ff9350e88 100755 --- a/scripts/start-services-robust.js +++ b/scripts/start-services-robust.js @@ -40,6 +40,26 @@ const CODING_DIR = path.resolve(SCRIPT_DIR, '..'); const execAsync = promisify(exec); const psm = new ProcessStateManager(); +// Parse CLI arguments for service control +const args = process.argv.slice(2); +const SERVICE_FLAGS = { + skipVkb: args.includes('--no-vkb'), + skipConstraints: args.includes('--no-constraints'), + skipTranscript: args.includes('--no-transcript'), + skipLogging: args.includes('--no-logging'), + skipHealth: args.includes('--no-health') +}; + +if (Object.values(SERVICE_FLAGS).some(Boolean)) { + console.log('šŸ”§ Service Configuration:'); + if (SERVICE_FLAGS.skipVkb) console.log(' - VKB Server disabled (--no-vkb)'); + if (SERVICE_FLAGS.skipConstraints) console.log(' - Constraint Monitor disabled (--no-constraints)'); + if (SERVICE_FLAGS.skipTranscript) console.log(' - Transcript Monitor disabled (--no-transcript)'); + if (SERVICE_FLAGS.skipLogging) console.log(' - Live Logging disabled (--no-logging)'); + if (SERVICE_FLAGS.skipHealth) console.log(' - Health Monitoring disabled (--no-health)'); + console.log(''); +} + // Service configurations const SERVICE_CONFIGS = { transcriptMonitor: { @@ -208,7 +228,7 @@ const SERVICE_CONFIGS = { } try { - await execAsync('docker-compose up -d', { + await execAsync('docker compose up -d', { cwd: constraintDir, timeout: 60000 }); @@ -219,7 +239,7 @@ const SERVICE_CONFIGS = { console.log('[ConstraintMonitor] Docker containers started successfully'); console.log('[ConstraintMonitor] Web services (API + Dashboard) managed by Global Service Coordinator'); - return { + return { service: 'constraint-monitor-docker', mode: 'docker-compose', containers: ['redis', 'qdrant'] @@ -507,46 +527,54 @@ async function startAllServices() { console.log('šŸ“‹ Starting REQUIRED services (Live Logging System)...'); console.log(''); - try { - const transcriptResult = await startServiceWithRetry( - SERVICE_CONFIGS.transcriptMonitor.name, - SERVICE_CONFIGS.transcriptMonitor.startFn, - SERVICE_CONFIGS.transcriptMonitor.healthCheckFn, - { - required: SERVICE_CONFIGS.transcriptMonitor.required, - maxRetries: SERVICE_CONFIGS.transcriptMonitor.maxRetries, - timeout: SERVICE_CONFIGS.transcriptMonitor.timeout - } - ); - results.successful.push(transcriptResult); - await registerWithPSM(transcriptResult, 'scripts/enhanced-transcript-monitor.js'); - } catch (error) { - results.failed.push({ - serviceName: SERVICE_CONFIGS.transcriptMonitor.name, - error: error.message, - required: true - }); + if (!SERVICE_FLAGS.skipTranscript) { + try { + const transcriptResult = await startServiceWithRetry( + SERVICE_CONFIGS.transcriptMonitor.name, + SERVICE_CONFIGS.transcriptMonitor.startFn, + SERVICE_CONFIGS.transcriptMonitor.healthCheckFn, + { + required: SERVICE_CONFIGS.transcriptMonitor.required, + maxRetries: SERVICE_CONFIGS.transcriptMonitor.maxRetries, + timeout: SERVICE_CONFIGS.transcriptMonitor.timeout + } + ); + results.successful.push(transcriptResult); + await registerWithPSM(transcriptResult, 'scripts/enhanced-transcript-monitor.js'); + } catch (error) { + results.failed.push({ + serviceName: SERVICE_CONFIGS.transcriptMonitor.name, + error: error.message, + required: true + }); + } + } else { + console.log('ā­ļø Skipping Transcript Monitor (--no-transcript)'); } - try { - const coordinatorResult = await startServiceWithRetry( - SERVICE_CONFIGS.liveLoggingCoordinator.name, - SERVICE_CONFIGS.liveLoggingCoordinator.startFn, - SERVICE_CONFIGS.liveLoggingCoordinator.healthCheckFn, - { - required: SERVICE_CONFIGS.liveLoggingCoordinator.required, - maxRetries: SERVICE_CONFIGS.liveLoggingCoordinator.maxRetries, - timeout: SERVICE_CONFIGS.liveLoggingCoordinator.timeout - } - ); - results.successful.push(coordinatorResult); - await registerWithPSM(coordinatorResult, 'scripts/live-logging-coordinator.js'); - } catch (error) { - results.failed.push({ - serviceName: SERVICE_CONFIGS.liveLoggingCoordinator.name, - error: error.message, - required: true - }); + if (!SERVICE_FLAGS.skipLogging) { + try { + const coordinatorResult = await startServiceWithRetry( + SERVICE_CONFIGS.liveLoggingCoordinator.name, + SERVICE_CONFIGS.liveLoggingCoordinator.startFn, + SERVICE_CONFIGS.liveLoggingCoordinator.healthCheckFn, + { + required: SERVICE_CONFIGS.liveLoggingCoordinator.required, + maxRetries: SERVICE_CONFIGS.liveLoggingCoordinator.maxRetries, + timeout: SERVICE_CONFIGS.liveLoggingCoordinator.timeout + } + ); + results.successful.push(coordinatorResult); + await registerWithPSM(coordinatorResult, 'scripts/live-logging-coordinator.js'); + } catch (error) { + results.failed.push({ + serviceName: SERVICE_CONFIGS.liveLoggingCoordinator.name, + error: error.message, + required: true + }); + } + } else { + console.log('ā­ļø Skipping Live Logging Coordinator (--no-logging)'); } console.log(''); @@ -555,85 +583,97 @@ async function startAllServices() { console.log('šŸ”µ Starting OPTIONAL services (graceful degradation enabled)...'); console.log(''); - const vkbResult = await startServiceWithRetry( - SERVICE_CONFIGS.vkbServer.name, - SERVICE_CONFIGS.vkbServer.startFn, - SERVICE_CONFIGS.vkbServer.healthCheckFn, - { - required: SERVICE_CONFIGS.vkbServer.required, - maxRetries: SERVICE_CONFIGS.vkbServer.maxRetries, - timeout: SERVICE_CONFIGS.vkbServer.timeout - } - ); + if (!SERVICE_FLAGS.skipVkb) { + const vkbResult = await startServiceWithRetry( + SERVICE_CONFIGS.vkbServer.name, + SERVICE_CONFIGS.vkbServer.startFn, + SERVICE_CONFIGS.vkbServer.healthCheckFn, + { + required: SERVICE_CONFIGS.vkbServer.required, + maxRetries: SERVICE_CONFIGS.vkbServer.maxRetries, + timeout: SERVICE_CONFIGS.vkbServer.timeout + } + ); - if (vkbResult.status === 'success') { - results.successful.push(vkbResult); - await registerWithPSM(vkbResult, 'lib/vkb-server/cli.js'); + if (vkbResult.status === 'success') { + results.successful.push(vkbResult); + await registerWithPSM(vkbResult, 'lib/vkb-server/cli.js'); + } else { + results.degraded.push(vkbResult); + } } else { - results.degraded.push(vkbResult); + console.log('ā­ļø Skipping VKB Server (--no-vkb)'); } console.log(''); // 3. OPTIONAL: Constraint Monitor - const constraintResult = await startServiceWithRetry( - SERVICE_CONFIGS.constraintMonitor.name, - SERVICE_CONFIGS.constraintMonitor.startFn, - SERVICE_CONFIGS.constraintMonitor.healthCheckFn, - { - required: SERVICE_CONFIGS.constraintMonitor.required, - maxRetries: SERVICE_CONFIGS.constraintMonitor.maxRetries, - timeout: SERVICE_CONFIGS.constraintMonitor.timeout - } - ); + if (!SERVICE_FLAGS.skipConstraints) { + const constraintResult = await startServiceWithRetry( + SERVICE_CONFIGS.constraintMonitor.name, + SERVICE_CONFIGS.constraintMonitor.startFn, + SERVICE_CONFIGS.constraintMonitor.healthCheckFn, + { + required: SERVICE_CONFIGS.constraintMonitor.required, + maxRetries: SERVICE_CONFIGS.constraintMonitor.maxRetries, + timeout: SERVICE_CONFIGS.constraintMonitor.timeout + } + ); - if (constraintResult.status === 'success') { - results.successful.push(constraintResult); - // No PSM registration for Docker-based service + if (constraintResult.status === 'success') { + results.successful.push(constraintResult); + // No PSM registration for Docker-based service + } else { + results.degraded.push(constraintResult); + } } else { - results.degraded.push(constraintResult); + console.log('ā­ļø Skipping Constraint Monitor (--no-constraints)'); } console.log(''); // 4. OPTIONAL: Health Verifier - const healthVerifierResult = await startServiceWithRetry( - SERVICE_CONFIGS.healthVerifier.name, - SERVICE_CONFIGS.healthVerifier.startFn, - SERVICE_CONFIGS.healthVerifier.healthCheckFn, - { - required: SERVICE_CONFIGS.healthVerifier.required, - maxRetries: SERVICE_CONFIGS.healthVerifier.maxRetries, - timeout: SERVICE_CONFIGS.healthVerifier.timeout + if (!SERVICE_FLAGS.skipHealth) { + const healthVerifierResult = await startServiceWithRetry( + SERVICE_CONFIGS.healthVerifier.name, + SERVICE_CONFIGS.healthVerifier.startFn, + SERVICE_CONFIGS.healthVerifier.healthCheckFn, + { + required: SERVICE_CONFIGS.healthVerifier.required, + maxRetries: SERVICE_CONFIGS.healthVerifier.maxRetries, + timeout: SERVICE_CONFIGS.healthVerifier.timeout + } + ); + + if (healthVerifierResult.status === 'success') { + results.successful.push(healthVerifierResult); + await registerWithPSM(healthVerifierResult, 'scripts/health-verifier.js'); + } else { + results.degraded.push(healthVerifierResult); } - ); - if (healthVerifierResult.status === 'success') { - results.successful.push(healthVerifierResult); - await registerWithPSM(healthVerifierResult, 'scripts/health-verifier.js'); - } else { - results.degraded.push(healthVerifierResult); - } + console.log(''); - console.log(''); + // 5. OPTIONAL: StatusLine Health Monitor + const statuslineHealthResult = await startServiceWithRetry( + SERVICE_CONFIGS.statuslineHealthMonitor.name, + SERVICE_CONFIGS.statuslineHealthMonitor.startFn, + SERVICE_CONFIGS.statuslineHealthMonitor.healthCheckFn, + { + required: SERVICE_CONFIGS.statuslineHealthMonitor.required, + maxRetries: SERVICE_CONFIGS.statuslineHealthMonitor.maxRetries, + timeout: SERVICE_CONFIGS.statuslineHealthMonitor.timeout + } + ); - // 5. OPTIONAL: StatusLine Health Monitor - const statuslineHealthResult = await startServiceWithRetry( - SERVICE_CONFIGS.statuslineHealthMonitor.name, - SERVICE_CONFIGS.statuslineHealthMonitor.startFn, - SERVICE_CONFIGS.statuslineHealthMonitor.healthCheckFn, - { - required: SERVICE_CONFIGS.statuslineHealthMonitor.required, - maxRetries: SERVICE_CONFIGS.statuslineHealthMonitor.maxRetries, - timeout: SERVICE_CONFIGS.statuslineHealthMonitor.timeout + if (statuslineHealthResult.status === 'success') { + results.successful.push(statuslineHealthResult); + await registerWithPSM(statuslineHealthResult, 'scripts/statusline-health-monitor.js'); + } else { + results.degraded.push(statuslineHealthResult); } - ); - - if (statuslineHealthResult.status === 'success') { - results.successful.push(statuslineHealthResult); - await registerWithPSM(statuslineHealthResult, 'scripts/statusline-health-monitor.js'); } else { - results.degraded.push(statuslineHealthResult); + console.log('ā­ļø Skipping Health Monitoring (--no-health)'); } console.log(''); diff --git a/start-services.sh b/start-services.sh index e5339d365..b3a4848c0 100755 --- a/start-services.sh +++ b/start-services.sh @@ -18,7 +18,7 @@ if [ "$ROBUST_MODE" = "true" ]; then echo "" # Use the Node.js-based robust service starter - exec node "$SCRIPT_DIR/scripts/start-services-robust.js" + exec node "$SCRIPT_DIR/scripts/start-services-robust.js" "$@" fi # LEGACY MODE (kept for backward compatibility, disable with ROBUST_MODE=false) diff --git a/test/setup.js b/test/setup.js new file mode 100644 index 000000000..f36f7d1c6 --- /dev/null +++ b/test/setup.js @@ -0,0 +1 @@ +console.log('Test setup loaded'); diff --git a/tests/unit/BoardMeeting.test.js b/tests/unit/BoardMeeting.test.js new file mode 100644 index 000000000..3a720fec4 --- /dev/null +++ b/tests/unit/BoardMeeting.test.js @@ -0,0 +1,76 @@ +import { jest } from '@jest/globals'; +import BoardMeeting from '../../lib/consensus/board-meeting.js'; +import LLMClient from '../../lib/consensus/llm-client.js'; + +// Mock LLMClient +jest.mock('../../lib/consensus/llm-client.js'); + +describe('BoardMeeting', () => { + let meeting; + let mockLLM; + + beforeEach(() => { + // Setup mock implementation + mockLLM = { + complete: jest.fn() + }; + + // In ESM mocking with Jest, we might need to handle the mock differently + // if LLMClient is the default export. + // However, since we are mocking the module, we assume the constructor calls will rely on the mocked class. + + // For this specific test, we can just spy on the methods or ensure the class is mocked correctly. + // Given the error "mockClear is not a function", it seems LLMClient isn't being treated as a Jest mock function automatically in this ESM context. + + // Workaround: Mock the instance method on the prototype if strict mocking fails + // But better: Use the mocked module instance. + }); + + test('generateProposals should call LLM for each director', async () => { + // Manually injecting mock into the meeting instance for this test + // to bypass module mocking complexity in ESM/Jest for now. + meeting = new BoardMeeting(); + meeting.llm = mockLLM; + mockLLM.complete.mockResolvedValue('A proposal'); + + const proposals = await meeting.generateProposals('Topic'); + + expect(proposals.length).toBe(5); + expect(mockLLM.complete).toHaveBeenCalledTimes(5); + expect(proposals[0].content).toBe('A proposal'); + }); + + test('anonymizeProposals should assign IDs', () => { + meeting = new BoardMeeting(); + const proposals = [ + { director: 'Dir 1', content: 'Content 1' }, + { director: 'Dir 2', content: 'Content 2' } + ]; + + const result = meeting.anonymizeProposals(proposals); + + expect(result[0].id).toBe('A'); + expect(result[1].id).toBe('B'); + expect(result[0].originalDirector).toBe('Dir 1'); + }); + + test('tallyVotes should determine the winner', () => { + meeting = new BoardMeeting(); + const anonymizedProposals = [ + { id: 'A', originalDirector: 'Dir 1', content: 'Content 1' }, + { id: 'B', originalDirector: 'Dir 2', content: 'Content 2' } + ]; + + const votes = [ + { voter: 'Voter 1', choice: 'A', reason: 'Good' }, + { voter: 'Voter 2', choice: 'A', reason: 'Better' }, + { voter: 'Voter 3', choice: 'B', reason: 'Okay' } + ]; + + const result = meeting.tallyVotes(votes, anonymizedProposals); + + expect(result.winner.id).toBe('A'); + expect(result.scores['A']).toBe(2); + expect(result.scores['B']).toBe(1); + }); +});