Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.cjs
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
module.exports = { extends: "eslint:recommended", env: { node: true, es6: true }, parserOptions: { ecmaVersion: 2022, sourceType: "module" } };

CopilotAIDec 1, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ESLint configuration is written on a single line, making it hard to read and maintain. Consider formatting it properly:

module.exports={extends: "eslint:recommended",env: {node: true,es6: true},parserOptions: {ecmaVersion: 2022,sourceType: "module"}};
Suggested change
module.exports={extends: "eslint:recommended",env: {node: true,es6: true},parserOptions: {ecmaVersion: 2022,sourceType: "module"}};
module.exports={
extends: "eslint:recommended",
env: {
node: true,
es6: true
},
parserOptions: {
ecmaVersion: 2022,
sourceType: "module"
}
};

Copilot uses AI. Check for mistakes.
4 changes: 4 additions & 0 deletions bin/coding
Original file line numberDiff line numberDiff line change
Expand Up@@ -106,6 +106,10 @@ 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
Expand Down
146 changes: 146 additions & 0 deletions lib/consensus/board-meeting.js
Original file line numberDiff line numberDiff line change
@@ -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

CopilotAIDec 1, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ID generation using String.fromCharCode(65 + index) assumes there will always be 5 or fewer proposals. If the system is extended to support more directors in the future, this could produce unexpected results (e.g., '[', '\', etc. after 'Z').

Consider adding a check or using a more scalable ID generation approach:

if(index>25){thrownewError('Too many proposals for single-letter IDs');}

Or use a different scheme like: 'P1', 'P2', etc.

Suggested change
id: String.fromCharCode(65+index),// A, B, C, D, E
id: `P${index+1}`,// P1, P2, P3, ...

Copilot uses AI. Check for mistakes.
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);

CopilotAIDec 1, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code instructs directors not to vote for their own proposal (line 79), but doesn't enforce this constraint. A director could still vote for their own proposal if the LLM doesn't follow instructions properly.

Consider adding validation after parsing the vote:

constvote=this.parseVote(response,director.name);if(vote.choice===ownId){console.warn(`${director.name} attempted to vote for their own proposal. Invalidating vote.`);returnnull;}returnvote;
Suggested change
returnthis.parseVote(response,director.name);
constvote=this.parseVote(response,director.name);
if(vote.choice===ownId){
console.warn(`${director.name} attempted to vote for their own proposal. Invalidating vote.`);
returnnull;
}
returnvote;

Copilot uses AI. Check for mistakes.
} 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: "",

CopilotAIDec 1, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The topic field in the return object is an empty string, but it should be populated with the actual topic that was passed to the method. This could be useful for debugging or logging purposes.

Consider changing:

return{topic: topic,// or pass topic as parameter to tallyVoteswinner: winner,scores: scores,votes: validVotes,proposals: anonymizedProposals};

Copilot uses AI. Check for mistakes.
winner: winner,
scores: scores,
votes: validVotes,
proposals: anonymizedProposals
Comment on lines +123 to +141

CopilotAIDec 1, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The winner determination logic doesn't handle ties properly. When multiple proposals have the same maximum score, the first one encountered in the iteration order will always win. This could lead to unexpected results in tie situations.

Consider either:

  1. Detecting and reporting ties explicitly
  2. Implementing a tie-breaking mechanism (e.g., random selection, reporting all tied proposals)
Suggested change
// Find winner
letmaxScore=-1;
letwinnerId=null;
Object.entries(scores).forEach(([id,score])=>{
if(score>maxScore){
maxScore=score;
winnerId=id;
}
});
constwinner=anonymizedProposals.find(p=>p.id===winnerId);
return{
topic: "",
winner: winner,
scores: scores,
votes: validVotes,
proposals: anonymizedProposals
// Find winners (handle ties)
letmaxScore=-1;
Object.values(scores).forEach(score=>{
if(score>maxScore){
maxScore=score;
}
});
// Collect all proposal IDs with maxScore
constwinnerIds=Object.entries(scores)
.filter(([id,score])=>score===maxScore)
.map(([id,score])=>id);
constwinners=anonymizedProposals.filter(p=>winnerIds.includes(p.id));
return{
topic: "",
winners: winners,
scores: scores,
votes: validVotes,
proposals: anonymizedProposals,
tie: winners.length>1

Copilot uses AI. Check for mistakes.
};
}
}

export default BoardMeeting;
49 changes: 49 additions & 0 deletions lib/consensus/directors.js
Original file line numberDiff line numberDiff line change
@@ -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;
78 changes: 78 additions & 0 deletions lib/consensus/llm-client.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
import { spawn } from 'child_process';

Comment on lines +1 to +2

CopilotAIDec 1, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The spawn import from 'child_process' is not used anywhere in this file and should be removed.

Suggested change
import { spawn } from 'child_process';

Copilot uses AI. Check for mistakes.
/**
* 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 }
]
})
Comment on lines +59 to +65

CopilotAIDec 1, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The OpenAI API call doesn't specify a max_tokens parameter, while the Anthropic call sets it to 4000. For consistency and to ensure predictable behavior, consider adding a max_tokens limit to the OpenAI request:

body: JSON.stringify({model: 'gpt-4o',max_tokens: 4000,messages: [{role: 'system',content: systemPrompt},{role: 'user',content: userPrompt}]})

Copilot uses AI. Check for mistakes.
});

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;
80 changes: 80 additions & 0 deletions scripts/run-board-meeting.js
Original file line numberDiff line numberDiff line change
@@ -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();
1 change: 1 addition & 0 deletions test/setup.js
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
console.log('Test setup loaded');
Loading