create-yonderclaw 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +44 -0
- package/README.md +288 -0
- package/bin/create-yonderclaw.mjs +43 -0
- package/docs/assets/favicon.png +0 -0
- package/docs/assets/metaclaw-banner.svg +86 -0
- package/docs/assets/qis-logo.png +0 -0
- package/docs/assets/yz-favicon.png +0 -0
- package/docs/assets/yz-logo.png +0 -0
- package/docs/index.html +1155 -0
- package/installer/assets/favicon.png +0 -0
- package/installer/auto-start.ts +330 -0
- package/installer/brand.ts +115 -0
- package/installer/core-scaffold.ts +448 -0
- package/installer/dashboard-generator.ts +657 -0
- package/installer/detect.ts +129 -0
- package/installer/index.ts +355 -0
- package/installer/module-loader.ts +412 -0
- package/installer/modules/boardroom/boardroom/client.ts.txt +201 -0
- package/installer/modules/boardroom/boardroom/db.ts.txt +322 -0
- package/installer/modules/boardroom/boardroom/meeting-agent.ts.txt +129 -0
- package/installer/modules/boardroom/boardroom/meeting-scheduler.ts.txt +194 -0
- package/installer/modules/boardroom/boardroom/server.ts.txt +473 -0
- package/installer/modules/boardroom/boardroom/start-boardroom.bat.txt +26 -0
- package/installer/modules/boardroom/boardroom/summons.ts.txt +76 -0
- package/installer/modules/boardroom/boardroom/turn-v2.ts.txt +172 -0
- package/installer/modules/boardroom/boardroom/turn.ts.txt +208 -0
- package/installer/modules/boardroom/boardroom/types.ts.txt +100 -0
- package/installer/modules/boardroom/metaclaw-module.json +35 -0
- package/installer/modules/boardroom/scripts/meeting-check.bat.txt +38 -0
- package/installer/modules/core/metaclaw-module.json +51 -0
- package/installer/modules/core/src/db.ts.txt +277 -0
- package/installer/modules/core/src/health-check.ts.txt +128 -0
- package/installer/modules/core/src/observability.ts.txt +20 -0
- package/installer/modules/core/src/safety.ts.txt +26 -0
- package/installer/modules/core/src/scan-capabilities.ts.txt +196 -0
- package/installer/modules/core/src/self-improve.ts.txt +48 -0
- package/installer/modules/core/src/self-update.ts.txt +345 -0
- package/installer/modules/core/src/sync-context.ts.txt +133 -0
- package/installer/modules/core/src/tasks.ts.txt +159 -0
- package/installer/modules/custom/metaclaw-module.json +15 -0
- package/installer/modules/custom/src/agent-custom.ts.txt +100 -0
- package/installer/modules/dashboard/metaclaw-module.json +23 -0
- package/installer/modules/dashboard/scripts/build-dashboard.cjs.txt +51 -0
- package/installer/modules/dashboard/src/update-dashboard.ts.txt +126 -0
- package/installer/modules/outreach/metaclaw-module.json +29 -0
- package/installer/modules/outreach/src/agent-outreach.ts.txt +193 -0
- package/installer/modules/outreach/src/inbox-agent.ts.txt +283 -0
- package/installer/modules/outreach/src/morning-report.ts.txt +124 -0
- package/installer/modules/research/metaclaw-module.json +15 -0
- package/installer/modules/research/src/agent-research.ts.txt +127 -0
- package/installer/modules/scheduler/metaclaw-module.json +27 -0
- package/installer/modules/scheduler/scripts/agent-cycle.bat.txt +85 -0
- package/installer/modules/scheduler/scripts/detect-session.bat.txt +41 -0
- package/installer/modules/scheduler/scripts/launch.bat.txt +120 -0
- package/installer/modules/scheduler/src/cron-manager.ts.txt +273 -0
- package/installer/modules/social/metaclaw-module.json +15 -0
- package/installer/modules/social/src/agent-social.ts.txt +110 -0
- package/installer/modules/support/metaclaw-module.json +15 -0
- package/installer/modules/support/src/agent-support.ts.txt +60 -0
- package/installer/modules/swarm/metaclaw-module.json +25 -0
- package/installer/modules/swarm/swarm/dht-client.ts.txt +376 -0
- package/installer/modules/swarm/swarm/relay-server.ts.txt +348 -0
- package/installer/modules/swarm/swarm/swarm-client.ts.txt +303 -0
- package/installer/modules/swarm/swarm/types.ts.txt +51 -0
- package/installer/modules/voice/metaclaw-module.json +16 -0
- package/installer/questionnaire.ts +277 -0
- package/installer/research.ts +258 -0
- package/installer/scaffold-from-config.ts +270 -0
- package/installer/task-generator.ts +324 -0
- package/installer/templates/agent-custom.ts.txt +100 -0
- package/installer/templates/agent-cycle.bat.txt +19 -0
- package/installer/templates/agent-outreach.ts.txt +193 -0
- package/installer/templates/agent-research.ts.txt +127 -0
- package/installer/templates/agent-social.ts.txt +110 -0
- package/installer/templates/agent-support.ts.txt +60 -0
- package/installer/templates/build-dashboard.cjs.txt +51 -0
- package/installer/templates/cron-manager.ts.txt +273 -0
- package/installer/templates/dashboard.html.txt +450 -0
- package/installer/templates/db.ts.txt +277 -0
- package/installer/templates/detect-session.bat.txt +41 -0
- package/installer/templates/health-check.ts.txt +128 -0
- package/installer/templates/inbox-agent.ts.txt +283 -0
- package/installer/templates/launch.bat.txt +120 -0
- package/installer/templates/morning-report.ts.txt +124 -0
- package/installer/templates/observability.ts.txt +20 -0
- package/installer/templates/safety.ts.txt +26 -0
- package/installer/templates/self-improve.ts.txt +48 -0
- package/installer/templates/self-update.ts.txt +345 -0
- package/installer/templates/state.json.txt +33 -0
- package/installer/templates/system-context.json.txt +33 -0
- package/installer/templates/update-dashboard.ts.txt +126 -0
- package/package.json +31 -0
- package/setup.bat +178 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MetaClaw Boardroom — Turn-Taking v2
|
|
3
|
+
*
|
|
4
|
+
* Dynamic priority queue with 5 signals:
|
|
5
|
+
* 1. Self-assessment (25%) — agent's own relevance judgment
|
|
6
|
+
* 2. Direct address (25%) — were they called on by name
|
|
7
|
+
* 3. Freshness (20%) — are they responding to recent conversation
|
|
8
|
+
* 4. Equity (15%) — balance participation
|
|
9
|
+
* 5. Urgency (15%) — critical > high > normal
|
|
10
|
+
* × Staleness decay — old hand-raises lose weight
|
|
11
|
+
*
|
|
12
|
+
* After every message, ALL waiting agents re-evaluate.
|
|
13
|
+
* The queue reorders dynamically based on current conversation state.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type Database from "better-sqlite3";
|
|
17
|
+
import { getRecentMessages, getMessageCount, getTurnStates, getParticipants, getPendingHandRaises, expireStaleHandRaises } from "./db.js";
|
|
18
|
+
|
|
19
|
+
const V2_WEIGHTS = {
|
|
20
|
+
self_score: 0.25,
|
|
21
|
+
address: 0.25,
|
|
22
|
+
freshness: 0.20,
|
|
23
|
+
equity: 0.15,
|
|
24
|
+
urgency: 0.15,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const THRESHOLD = 20;
|
|
28
|
+
|
|
29
|
+
export interface QueueEntry {
|
|
30
|
+
agent_name: string;
|
|
31
|
+
final_score: number;
|
|
32
|
+
components: {
|
|
33
|
+
self_score: number;
|
|
34
|
+
address_score: number;
|
|
35
|
+
freshness_score: number;
|
|
36
|
+
equity_score: number;
|
|
37
|
+
urgency_bonus: number;
|
|
38
|
+
staleness_decay: number;
|
|
39
|
+
};
|
|
40
|
+
reason: string;
|
|
41
|
+
should_speak: boolean;
|
|
42
|
+
intent_hash: string;
|
|
43
|
+
in_response_to: number | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function computePriorityQueue(meetingId: string, db: Database.Database): QueueEntry[] {
|
|
47
|
+
// Expire old hand-raises
|
|
48
|
+
expireStaleHandRaises(db, meetingId, 300);
|
|
49
|
+
|
|
50
|
+
const handRaises = getPendingHandRaises(db, meetingId);
|
|
51
|
+
if (handRaises.length === 0) return [];
|
|
52
|
+
|
|
53
|
+
const recentMessages = getRecentMessages(db, meetingId, 5);
|
|
54
|
+
const currentMsgCount = getMessageCount(db, meetingId);
|
|
55
|
+
const turnStates = getTurnStates(db, meetingId);
|
|
56
|
+
const maxSpeakCount = Math.max(1, ...turnStates.map((t: any) => t.speak_count || 0));
|
|
57
|
+
const now = Date.now();
|
|
58
|
+
|
|
59
|
+
const queue: QueueEntry[] = [];
|
|
60
|
+
|
|
61
|
+
for (const hr of handRaises as any[]) {
|
|
62
|
+
// 1. SELF_SCORE — agent's own assessment
|
|
63
|
+
const selfScore = (hr.self_score || 0.5) * 100;
|
|
64
|
+
|
|
65
|
+
// 2. ADDRESS_SCORE — were they called on
|
|
66
|
+
const addressScore = scoreDirectAddress(hr.agent_name, recentMessages);
|
|
67
|
+
|
|
68
|
+
// 3. FRESHNESS_SCORE — responding to recent conversation?
|
|
69
|
+
let freshnessScore = 100;
|
|
70
|
+
if (hr.in_response_to) {
|
|
71
|
+
const pos = db.prepare("SELECT COUNT(*) as pos FROM messages WHERE meeting_id = ? AND id <= ?")
|
|
72
|
+
.get(meetingId, hr.in_response_to) as any;
|
|
73
|
+
const gap = currentMsgCount - (pos?.pos || 0);
|
|
74
|
+
freshnessScore = Math.max(0, 100 - gap * 30);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 4. EQUITY_SCORE — balance participation
|
|
78
|
+
const myState = turnStates.find((t: any) => t.agent_name === hr.agent_name);
|
|
79
|
+
const mySpeakCount = myState?.speak_count || 0;
|
|
80
|
+
const equityScore = Math.max(10, 100 - (mySpeakCount / maxSpeakCount) * 90);
|
|
81
|
+
|
|
82
|
+
// 5. URGENCY_BONUS
|
|
83
|
+
const urgencyMap: Record<string, number> = { critical: 100, high: 70, normal: 30 };
|
|
84
|
+
const urgencyBonus = urgencyMap[hr.urgency] || 30;
|
|
85
|
+
|
|
86
|
+
// STALENESS_DECAY — old hand-raises lose weight
|
|
87
|
+
const raisedAt = new Date(hr.raised_at + "Z").getTime();
|
|
88
|
+
const secondsSinceRaise = (now - raisedAt) / 1000;
|
|
89
|
+
const stalenessDecay = Math.max(0.3, 1.0 - secondsSinceRaise / 300);
|
|
90
|
+
|
|
91
|
+
// COMPOSITE
|
|
92
|
+
const rawScore =
|
|
93
|
+
selfScore * V2_WEIGHTS.self_score +
|
|
94
|
+
addressScore * V2_WEIGHTS.address +
|
|
95
|
+
freshnessScore * V2_WEIGHTS.freshness +
|
|
96
|
+
equityScore * V2_WEIGHTS.equity +
|
|
97
|
+
urgencyBonus * V2_WEIGHTS.urgency;
|
|
98
|
+
|
|
99
|
+
const finalScore = Math.round(rawScore * stalenessDecay);
|
|
100
|
+
|
|
101
|
+
// Build reason
|
|
102
|
+
const reasons: string[] = [];
|
|
103
|
+
if (selfScore >= 70) reasons.push("high self-assessment");
|
|
104
|
+
if (addressScore >= 70) reasons.push("directly addressed");
|
|
105
|
+
if (freshnessScore >= 70) reasons.push("responding to recent");
|
|
106
|
+
if (equityScore >= 80) reasons.push("hasn't spoken much");
|
|
107
|
+
if (hr.urgency === "critical") reasons.push("CRITICAL");
|
|
108
|
+
else if (hr.urgency === "high") reasons.push("high urgency");
|
|
109
|
+
if (stalenessDecay < 0.7) reasons.push("hand-raise aging");
|
|
110
|
+
|
|
111
|
+
queue.push({
|
|
112
|
+
agent_name: hr.agent_name,
|
|
113
|
+
final_score: finalScore,
|
|
114
|
+
components: { self_score: selfScore, address_score: addressScore, freshness_score: freshnessScore, equity_score: equityScore, urgency_bonus: urgencyBonus, staleness_decay: stalenessDecay },
|
|
115
|
+
reason: reasons.join(", ") || "in queue",
|
|
116
|
+
should_speak: finalScore >= THRESHOLD,
|
|
117
|
+
intent_hash: hr.intent_hash || "",
|
|
118
|
+
in_response_to: hr.in_response_to,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Sort: critical urgency always first, then by score, then by equity (less talkative wins ties)
|
|
123
|
+
queue.sort((a, b) => {
|
|
124
|
+
// Critical urgency trumps everything
|
|
125
|
+
if (a.components.urgency_bonus === 100 && b.components.urgency_bonus !== 100) return -1;
|
|
126
|
+
if (b.components.urgency_bonus === 100 && a.components.urgency_bonus !== 100) return 1;
|
|
127
|
+
// Then by score
|
|
128
|
+
if (b.final_score !== a.final_score) return b.final_score - a.final_score;
|
|
129
|
+
// Tie-break: less talkative goes first
|
|
130
|
+
return a.components.equity_score - b.components.equity_score;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return queue;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function getNextSpeakerV2(meetingId: string, db: Database.Database): QueueEntry | null {
|
|
137
|
+
const queue = computePriorityQueue(meetingId, db);
|
|
138
|
+
return queue.length > 0 && queue[0].should_speak ? queue[0] : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function scoreForAgentV2(meetingId: string, agentName: string, db: Database.Database): QueueEntry | null {
|
|
142
|
+
const queue = computePriorityQueue(meetingId, db);
|
|
143
|
+
return queue.find(q => q.agent_name === agentName) || null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check if another agent's message overlaps with a pending hand-raise
|
|
147
|
+
export function checkCoverage(intentHash: string, messageContent: string): number {
|
|
148
|
+
if (!intentHash || !messageContent) return 0;
|
|
149
|
+
const intentWords = new Set(intentHash.toLowerCase().split(/\s+/).filter(w => w.length > 3));
|
|
150
|
+
const msgWords = new Set(messageContent.toLowerCase().split(/\s+/).filter(w => w.length > 3));
|
|
151
|
+
let overlap = 0;
|
|
152
|
+
for (const w of intentWords) {
|
|
153
|
+
if (msgWords.has(w)) overlap++;
|
|
154
|
+
}
|
|
155
|
+
return intentWords.size > 0 ? overlap / intentWords.size : 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// --- Helper ---
|
|
159
|
+
function scoreDirectAddress(agentName: string, recentMessages: any[]): number {
|
|
160
|
+
if (recentMessages.length === 0) return 0;
|
|
161
|
+
const nameLower = agentName.toLowerCase();
|
|
162
|
+
let best = 0;
|
|
163
|
+
|
|
164
|
+
for (const msg of recentMessages) {
|
|
165
|
+
let addresses: string[] = [];
|
|
166
|
+
try { addresses = typeof msg.addresses === "string" ? JSON.parse(msg.addresses) : (msg.addresses || []); } catch {}
|
|
167
|
+
if (addresses.some((a: string) => a.toLowerCase() === nameLower)) return 100;
|
|
168
|
+
if (msg.content.toLowerCase().includes(nameLower)) best = Math.max(best, 70);
|
|
169
|
+
if (msg.msg_type === "question" && msg.agent_name !== agentName) best = Math.max(best, 30);
|
|
170
|
+
}
|
|
171
|
+
return best;
|
|
172
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MetaClaw Boardroom — Intelligent Turn-Taking Algorithm
|
|
3
|
+
* 4-signal scoring: direct address (40%), expertise (30%), recency (20%), pending (10%)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type Database from "better-sqlite3";
|
|
7
|
+
import type { TurnScore } from "./types.js";
|
|
8
|
+
import { getRecentMessages, getTurnStates, getParticipants } from "./db.js";
|
|
9
|
+
|
|
10
|
+
const WEIGHTS = {
|
|
11
|
+
direct_address: 0.40,
|
|
12
|
+
expertise: 0.30,
|
|
13
|
+
recency: 0.20,
|
|
14
|
+
pending_flag: 0.10,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const THRESHOLD = 30;
|
|
18
|
+
|
|
19
|
+
// Common words to skip when matching expertise
|
|
20
|
+
const STOP_WORDS = new Set([
|
|
21
|
+
"the", "and", "for", "are", "but", "not", "you", "all", "can", "had",
|
|
22
|
+
"her", "was", "one", "our", "out", "has", "have", "this", "that", "with",
|
|
23
|
+
"they", "been", "from", "will", "what", "when", "make", "like", "just",
|
|
24
|
+
"over", "such", "take", "than", "them", "very", "some", "could", "other",
|
|
25
|
+
"into", "more", "also", "back", "after", "should", "would", "about",
|
|
26
|
+
"think", "know", "going", "want", "need", "meeting", "agent", "yeah",
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
export function scoreTurns(
|
|
30
|
+
meetingId: string,
|
|
31
|
+
db: Database.Database,
|
|
32
|
+
excludeAgent?: string
|
|
33
|
+
): TurnScore[] {
|
|
34
|
+
const participants = getParticipants(db, meetingId);
|
|
35
|
+
const activeParticipants = participants.filter((p: any) => p.status === "active");
|
|
36
|
+
const recentMessages = getRecentMessages(db, meetingId, 3);
|
|
37
|
+
const turnStates = getTurnStates(db, meetingId);
|
|
38
|
+
const turnMap = new Map(turnStates.map((t: any) => [t.agent_name, t]));
|
|
39
|
+
|
|
40
|
+
const scores: TurnScore[] = [];
|
|
41
|
+
|
|
42
|
+
for (const p of activeParticipants) {
|
|
43
|
+
const name = p.agent_name;
|
|
44
|
+
if (name === excludeAgent) continue;
|
|
45
|
+
|
|
46
|
+
const ts = turnMap.get(name) || { wants_turn: 0, last_spoke_at: null, speak_count: 0 };
|
|
47
|
+
const expertise: string[] = safeParseJson(p.expertise, []);
|
|
48
|
+
|
|
49
|
+
// Signal 1: Direct Address
|
|
50
|
+
const addressScore = scoreDirectAddress(name, recentMessages);
|
|
51
|
+
|
|
52
|
+
// Signal 2: Expertise Relevance
|
|
53
|
+
const expertiseScore = scoreExpertise(expertise, recentMessages);
|
|
54
|
+
|
|
55
|
+
// Signal 3: Recency
|
|
56
|
+
const recencyScore = scoreRecency(ts, turnStates);
|
|
57
|
+
|
|
58
|
+
// Signal 4: Pending Flag
|
|
59
|
+
const pendingScore = ts.wants_turn ? 100 : 0;
|
|
60
|
+
|
|
61
|
+
const composite = Math.round(
|
|
62
|
+
addressScore * WEIGHTS.direct_address +
|
|
63
|
+
expertiseScore * WEIGHTS.expertise +
|
|
64
|
+
recencyScore * WEIGHTS.recency +
|
|
65
|
+
pendingScore * WEIGHTS.pending_flag
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const reasons: string[] = [];
|
|
69
|
+
if (addressScore > 50) reasons.push("addressed directly");
|
|
70
|
+
if (expertiseScore > 50) reasons.push("expertise match");
|
|
71
|
+
if (recencyScore > 70) reasons.push("hasn't spoken recently");
|
|
72
|
+
if (ts.wants_turn) reasons.push("requested turn");
|
|
73
|
+
|
|
74
|
+
scores.push({
|
|
75
|
+
agent_name: name,
|
|
76
|
+
score: composite,
|
|
77
|
+
reason: reasons.length > 0 ? reasons.join(", ") : "waiting",
|
|
78
|
+
should_speak: composite >= THRESHOLD,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Sort by score descending, then by speak_count ascending (less talkative wins ties)
|
|
83
|
+
scores.sort((a, b) => {
|
|
84
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
85
|
+
const tsA = turnMap.get(a.agent_name);
|
|
86
|
+
const tsB = turnMap.get(b.agent_name);
|
|
87
|
+
return (tsA?.speak_count || 0) - (tsB?.speak_count || 0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return scores;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getNextSpeaker(
|
|
94
|
+
meetingId: string,
|
|
95
|
+
db: Database.Database,
|
|
96
|
+
lastSpeaker?: string
|
|
97
|
+
): TurnScore | null {
|
|
98
|
+
const scores = scoreTurns(meetingId, db, lastSpeaker);
|
|
99
|
+
return scores.length > 0 && scores[0].should_speak ? scores[0] : null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function scoreForAgent(
|
|
103
|
+
meetingId: string,
|
|
104
|
+
agentName: string,
|
|
105
|
+
db: Database.Database
|
|
106
|
+
): TurnScore {
|
|
107
|
+
const all = scoreTurns(meetingId, db);
|
|
108
|
+
return all.find(s => s.agent_name === agentName) || {
|
|
109
|
+
agent_name: agentName,
|
|
110
|
+
score: 0,
|
|
111
|
+
reason: "not in meeting",
|
|
112
|
+
should_speak: false,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// --- Signal Scoring Functions ---
|
|
117
|
+
|
|
118
|
+
function scoreDirectAddress(agentName: string, recentMessages: any[]): number {
|
|
119
|
+
if (recentMessages.length === 0) return 0;
|
|
120
|
+
|
|
121
|
+
const nameLower = agentName.toLowerCase();
|
|
122
|
+
let best = 0;
|
|
123
|
+
|
|
124
|
+
for (const msg of recentMessages) {
|
|
125
|
+
// Check explicit addresses field
|
|
126
|
+
const addresses: string[] = safeParseJson(msg.addresses, []);
|
|
127
|
+
if (addresses.some((a: string) => a.toLowerCase() === nameLower)) {
|
|
128
|
+
return 100; // Explicitly addressed
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check content for name mentions
|
|
132
|
+
const contentLower = msg.content.toLowerCase();
|
|
133
|
+
if (contentLower.includes(nameLower)) {
|
|
134
|
+
best = Math.max(best, 70);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check if it's a question (increases likelihood someone should respond)
|
|
138
|
+
if (msg.msg_type === "question" && msg.agent_name !== agentName) {
|
|
139
|
+
best = Math.max(best, 30);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return best;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function scoreExpertise(expertise: string[], recentMessages: any[]): number {
|
|
147
|
+
if (expertise.length === 0 || recentMessages.length === 0) return 0;
|
|
148
|
+
|
|
149
|
+
const expertiseLower = new Set(expertise.map(e => e.toLowerCase()));
|
|
150
|
+
|
|
151
|
+
// Extract meaningful words from recent messages
|
|
152
|
+
const messageWords = new Set<string>();
|
|
153
|
+
for (const msg of recentMessages) {
|
|
154
|
+
const words = msg.content.toLowerCase()
|
|
155
|
+
.replace(/[^a-z0-9\s-]/g, " ")
|
|
156
|
+
.split(/\s+/)
|
|
157
|
+
.filter((w: string) => w.length > 3 && !STOP_WORDS.has(w));
|
|
158
|
+
words.forEach((w: string) => messageWords.add(w));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Count matches
|
|
162
|
+
let matches = 0;
|
|
163
|
+
for (const tag of expertiseLower) {
|
|
164
|
+
// Check exact match and partial match (tag "pricing" matches word "pricing")
|
|
165
|
+
for (const word of messageWords) {
|
|
166
|
+
if (word.includes(tag) || tag.includes(word)) {
|
|
167
|
+
matches++;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return Math.min(100, Math.round((matches / expertise.length) * 100));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function scoreRecency(turnState: any, allTurnStates: any[]): number {
|
|
177
|
+
// Never spoken = maximum recency score
|
|
178
|
+
if (!turnState.last_spoke_at) return 100;
|
|
179
|
+
|
|
180
|
+
const lastSpoke = new Date(turnState.last_spoke_at + "Z").getTime();
|
|
181
|
+
const now = Date.now();
|
|
182
|
+
|
|
183
|
+
// Find the most recent speaker's time
|
|
184
|
+
let mostRecent = 0;
|
|
185
|
+
let oldest = Infinity;
|
|
186
|
+
for (const ts of allTurnStates) {
|
|
187
|
+
if (ts.last_spoke_at) {
|
|
188
|
+
const t = new Date(ts.last_spoke_at + "Z").getTime();
|
|
189
|
+
mostRecent = Math.max(mostRecent, t);
|
|
190
|
+
oldest = Math.min(oldest, t);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (mostRecent === oldest) return 50; // Everyone spoke at the same time
|
|
195
|
+
|
|
196
|
+
// Scale: most recent speaker = 0, oldest speaker = 100
|
|
197
|
+
const range = mostRecent - oldest;
|
|
198
|
+
if (range === 0) return 50;
|
|
199
|
+
return Math.round(((mostRecent - lastSpoke) / range) * 100);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function safeParseJson(val: any, fallback: any): any {
|
|
203
|
+
if (Array.isArray(val)) return val;
|
|
204
|
+
if (typeof val === "string") {
|
|
205
|
+
try { return JSON.parse(val); } catch { return fallback; }
|
|
206
|
+
}
|
|
207
|
+
return fallback;
|
|
208
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MetaClaw Boardroom — Shared Types
|
|
3
|
+
* Multi-agent meeting system with intelligent turn-taking
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface Meeting {
|
|
7
|
+
id: string;
|
|
8
|
+
topic: string;
|
|
9
|
+
created_by: string;
|
|
10
|
+
status: "active" | "paused" | "ended";
|
|
11
|
+
max_participants: number;
|
|
12
|
+
created_at: string;
|
|
13
|
+
ended_at: string | null;
|
|
14
|
+
summary: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Participant {
|
|
18
|
+
id: number;
|
|
19
|
+
meeting_id: string;
|
|
20
|
+
agent_name: string;
|
|
21
|
+
expertise: string[];
|
|
22
|
+
status: "active" | "left" | "idle";
|
|
23
|
+
joined_at: string;
|
|
24
|
+
last_poll_at: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface Message {
|
|
28
|
+
id: number;
|
|
29
|
+
meeting_id: string;
|
|
30
|
+
agent_name: string;
|
|
31
|
+
content: string;
|
|
32
|
+
addresses: string[] | null;
|
|
33
|
+
msg_type: "statement" | "question" | "proposal" | "decision" | "summary";
|
|
34
|
+
created_at: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface TurnState {
|
|
38
|
+
meeting_id: string;
|
|
39
|
+
agent_name: string;
|
|
40
|
+
wants_turn: boolean;
|
|
41
|
+
last_spoke_at: string | null;
|
|
42
|
+
speak_count: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface TurnScore {
|
|
46
|
+
agent_name: string;
|
|
47
|
+
score: number;
|
|
48
|
+
reason: string;
|
|
49
|
+
should_speak: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ContextPackage {
|
|
53
|
+
meeting_topic: string;
|
|
54
|
+
meeting_id: string;
|
|
55
|
+
participants: string[];
|
|
56
|
+
recent_messages: Message[];
|
|
57
|
+
running_summary: string | null;
|
|
58
|
+
your_turn: TurnScore;
|
|
59
|
+
total_messages: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface PollResult {
|
|
63
|
+
messages: Message[];
|
|
64
|
+
turn_signal: TurnScore;
|
|
65
|
+
context: ContextPackage | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface CreateMeetingRequest {
|
|
69
|
+
topic: string;
|
|
70
|
+
created_by: string;
|
|
71
|
+
expertise?: string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface JoinMeetingRequest {
|
|
75
|
+
agent_name: string;
|
|
76
|
+
expertise?: string[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface SpeakRequest {
|
|
80
|
+
agent_name: string;
|
|
81
|
+
content: string;
|
|
82
|
+
addresses?: string[];
|
|
83
|
+
msg_type?: Message["msg_type"];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface MeetingSummons {
|
|
87
|
+
id: string;
|
|
88
|
+
from: string;
|
|
89
|
+
to: string;
|
|
90
|
+
timestamp: string;
|
|
91
|
+
subject: string;
|
|
92
|
+
body: string;
|
|
93
|
+
type: "request";
|
|
94
|
+
priority: "high";
|
|
95
|
+
boardroom: {
|
|
96
|
+
meeting_id: string;
|
|
97
|
+
coordinator_url: string;
|
|
98
|
+
topic: string;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "boardroom",
|
|
3
|
+
"displayName": "Boardroom — Agent Meetings",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "Multi-agent meeting system: scheduling, intelligent turn-taking, persistent attendance, post-meeting artifacts",
|
|
6
|
+
"category": "collaboration",
|
|
7
|
+
"alwaysInstall": true,
|
|
8
|
+
"engines": { "metaclaw": ">=3.3.0" },
|
|
9
|
+
"requires": { "modules": ["core"] },
|
|
10
|
+
"contributes": {
|
|
11
|
+
"boardroom": {
|
|
12
|
+
"types.ts.txt": "boardroom/types.ts",
|
|
13
|
+
"db.ts.txt": "boardroom/db.ts",
|
|
14
|
+
"turn.ts.txt": "boardroom/turn.ts",
|
|
15
|
+
"turn-v2.ts.txt": "boardroom/turn-v2.ts",
|
|
16
|
+
"server.ts.txt": "boardroom/server.ts",
|
|
17
|
+
"client.ts.txt": "boardroom/client.ts",
|
|
18
|
+
"summons.ts.txt": "boardroom/summons.ts",
|
|
19
|
+
"meeting-agent.ts.txt": "boardroom/meeting-agent.ts",
|
|
20
|
+
"meeting-scheduler.ts.txt": "boardroom/meeting-scheduler.ts",
|
|
21
|
+
"start-boardroom.bat.txt": "boardroom/start-boardroom.bat"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"meeting-check.bat.txt": "scripts/meeting-check.bat"
|
|
25
|
+
},
|
|
26
|
+
"npmScripts": {
|
|
27
|
+
"boardroom": "tsx boardroom/server.ts",
|
|
28
|
+
"boardroom-start": "tsx boardroom/server.ts --port 7890",
|
|
29
|
+
"meeting-check": "tsx boardroom/meeting-scheduler.ts"
|
|
30
|
+
},
|
|
31
|
+
"claudeMd": ["boardroom"],
|
|
32
|
+
"directories": ["boardroom"]
|
|
33
|
+
},
|
|
34
|
+
"placeholders": ["__PROJECT_DIR__", "__COORDINATOR_URL__"]
|
|
35
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
@echo off
|
|
2
|
+
REM MetaClaw Boardroom — Meeting Check
|
|
3
|
+
REM Called at the start of every agent cycle.
|
|
4
|
+
REM Checks if this agent has an active meeting to attend.
|
|
5
|
+
REM Returns ERRORLEVEL 0 = meeting found (enter meeting mode)
|
|
6
|
+
REM Returns ERRORLEVEL 1 = no meeting (proceed with normal cycle)
|
|
7
|
+
|
|
8
|
+
setlocal enabledelayedexpansion
|
|
9
|
+
set "AGENT_NAME=%~1"
|
|
10
|
+
set "COORDINATOR=__COORDINATOR_URL__"
|
|
11
|
+
set "INBOX=__INBOX_ROOT__\%AGENT_NAME%"
|
|
12
|
+
set "MEETING_ID="
|
|
13
|
+
|
|
14
|
+
REM Check 1: MEETING_READY sentinel file in inbox
|
|
15
|
+
for %%f in ("%INBOX%\MEETING_READY_*.json") do (
|
|
16
|
+
if exist "%%f" (
|
|
17
|
+
echo [MEETING] Found meeting-ready sentinel: %%~nf
|
|
18
|
+
REM Extract meeting_id from the JSON file
|
|
19
|
+
for /f "usebackq tokens=*" %%j in (`node -e "const d=JSON.parse(require('fs').readFileSync('%%f','utf-8'));console.log(d.meeting_id)"`) do (
|
|
20
|
+
set "MEETING_ID=%%j"
|
|
21
|
+
)
|
|
22
|
+
if defined MEETING_ID (
|
|
23
|
+
echo !MEETING_ID!> "data\.meeting-id"
|
|
24
|
+
exit /b 0
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
REM Check 2: Active meeting where this agent is a participant
|
|
30
|
+
for /f "usebackq delims=" %%r in (`node -e "fetch('%COORDINATOR%/meetings').then(r=>r.json()).then(d=>{const m=d.meetings.find(m=>m.status==='active');if(m)console.log(m.id);else console.log('NONE')}).catch(()=>console.log('NONE'))"`) do (
|
|
31
|
+
if not "%%r"=="NONE" (
|
|
32
|
+
echo [MEETING] Active meeting found: %%r
|
|
33
|
+
echo %%r> "data\.meeting-id"
|
|
34
|
+
exit /b 0
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
exit /b 1
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "core",
|
|
3
|
+
"displayName": "Core — Database, Safety & Self-Improvement",
|
|
4
|
+
"version": "3.3.0",
|
|
5
|
+
"description": "Foundation: SQLite database, safety guardrails, observability, self-improvement, health checks, state management, hooks system",
|
|
6
|
+
"category": "foundation",
|
|
7
|
+
"alwaysInstall": true,
|
|
8
|
+
"engines": { "metaclaw": ">=3.3.0" },
|
|
9
|
+
"requires": { "modules": [], "env": [], "bins": ["node"] },
|
|
10
|
+
"contributes": {
|
|
11
|
+
"src": {
|
|
12
|
+
"db.ts.txt": "src/db.ts",
|
|
13
|
+
"safety.ts.txt": "src/safety.ts",
|
|
14
|
+
"observability.ts.txt": "src/observability.ts",
|
|
15
|
+
"self-improve.ts.txt": "src/self-improve.ts",
|
|
16
|
+
"self-update.ts.txt": "src/self-update.ts",
|
|
17
|
+
"health-check.ts.txt": "src/health-check.ts",
|
|
18
|
+
"tasks.ts.txt": "src/tasks.ts",
|
|
19
|
+
"scan-capabilities.ts.txt": "src/scan-capabilities.ts",
|
|
20
|
+
"sync-context.ts.txt": "src/sync-context.ts"
|
|
21
|
+
},
|
|
22
|
+
"data": {
|
|
23
|
+
"state.json.txt": "data/state.json",
|
|
24
|
+
"system-context.json.txt": "data/system-context.json"
|
|
25
|
+
},
|
|
26
|
+
"memory": {
|
|
27
|
+
"CAPABILITIES.md.txt": "memory/CAPABILITIES.md",
|
|
28
|
+
"stuck-patterns.md.txt": "memory/stuck-patterns.md",
|
|
29
|
+
"logic-log.jsonl.txt": "memory/logic-log.jsonl",
|
|
30
|
+
"reflections.jsonl.txt": "memory/reflections.jsonl",
|
|
31
|
+
"curiosity.md.txt": "memory/curiosity.md"
|
|
32
|
+
},
|
|
33
|
+
"npmScripts": {
|
|
34
|
+
"status": "tsx src/agent.ts --status",
|
|
35
|
+
"self-update": "tsx src/self-update.ts",
|
|
36
|
+
"health-check": "tsx src/health-check.ts",
|
|
37
|
+
"scan": "tsx src/scan-capabilities.ts",
|
|
38
|
+
"sync-context": "tsx src/sync-context.ts"
|
|
39
|
+
},
|
|
40
|
+
"claudeMd": ["identity", "first-launch", "meta-cognition", "state-management", "tasks", "commands"],
|
|
41
|
+
"directories": ["src", "src/tools", "data", "data/logs", "scripts", "memory", "memory/capabilities"],
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@anthropic-ai/claude-agent-sdk": "latest",
|
|
44
|
+
"@types/better-sqlite3": "latest",
|
|
45
|
+
"better-sqlite3": "latest",
|
|
46
|
+
"tsx": "latest",
|
|
47
|
+
"typescript": "latest"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"placeholders": ["__AGENT_NAME__", "__SESSION_NAME__", "__CLAW_TYPE__", "__TIMESTAMP__", "__SAFETY_CONFIG__", "__SYSTEM_PROMPT__"]
|
|
51
|
+
}
|