@clawtrial/courtroom 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/src/hearing.js ADDED
@@ -0,0 +1,459 @@
1
+ /**
2
+ * Hearing Pipeline
3
+ *
4
+ * Orchestrates the full hearing process:
5
+ * 1. Evidence compilation
6
+ * 2. Judge LLM invocation
7
+ * 3. Jury LLM invocations (3 jurors)
8
+ * 4. Vote aggregation
9
+ * 5. Verdict finalization
10
+ */
11
+
12
+ const { JUDGE_SYSTEM_PROMPT, JUDGE_EVIDENCE_TEMPLATE } = require('./prompts/judge');
13
+ const { JUROR_ROLES, JURY_EVIDENCE_TEMPLATE } = require('./prompts/jury');
14
+
15
+ class HearingPipeline {
16
+ constructor(agentRuntime, configManager) {
17
+ this.agent = agentRuntime;
18
+ this.config = configManager;
19
+ }
20
+
21
+ /**
22
+ * Main hearing entry point
23
+ */
24
+ async conductHearing(caseData) {
25
+ const startTime = Date.now();
26
+
27
+ // Step 1: Compile evidence
28
+ const compiledEvidence = this.compileEvidence(caseData);
29
+
30
+ // Step 2: Invoke judge
31
+ const judgeOpinion = await this.invokeJudge(caseData, compiledEvidence);
32
+
33
+ // Step 3: Invoke jury (3 jurors in parallel)
34
+ const juryVotes = await this.invokeJury(caseData, compiledEvidence);
35
+
36
+ // Step 4: Aggregate votes
37
+ const voteTally = this.aggregateVotes(judgeOpinion, juryVotes);
38
+
39
+ // Step 5: Finalize verdict
40
+ const verdict = this.finalizeVerdict(caseData, judgeOpinion, juryVotes, voteTally);
41
+
42
+ const duration = Date.now() - startTime;
43
+
44
+ return {
45
+ ...verdict,
46
+ metadata: {
47
+ duration,
48
+ judgeModel: judgeOpinion.model,
49
+ juryModels: juryVotes.map(v => v.model),
50
+ timestamp: new Date().toISOString()
51
+ }
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Compile and structure evidence for presentation
57
+ */
58
+ compileEvidence(caseData) {
59
+ return {
60
+ caseId: caseData.caseId,
61
+ offenseId: caseData.offenseId,
62
+ offenseName: caseData.offenseName,
63
+ severity: caseData.severity,
64
+ confidence: caseData.confidence,
65
+ evidence: caseData.evidence,
66
+ humorTriggers: caseData.humorTriggers || [],
67
+ sessionContext: {
68
+ turnsAnalyzed: caseData.evidence.sessionTurns,
69
+ evaluationWindow: this.config.get('detection.evaluationWindow')
70
+ }
71
+ };
72
+ }
73
+
74
+ /**
75
+ * Invoke the judge LLM
76
+ */
77
+ async invokeJudge(caseData, evidence) {
78
+ const prompt = JUDGE_EVIDENCE_TEMPLATE({
79
+ ...caseData,
80
+ agentId: this.agent.id || 'unknown'
81
+ });
82
+
83
+ const response = await this.agent.llm.call({
84
+ model: this.agent.model.primary,
85
+ system: JUDGE_SYSTEM_PROMPT,
86
+ messages: [{ role: 'user', content: prompt }],
87
+ temperature: 0.3, // Slightly creative for humor
88
+ maxTokens: 500,
89
+ timeout: this.config.get('hearing.deliberationTimeout')
90
+ });
91
+
92
+ return this.parseJudgeResponse(response);
93
+ }
94
+
95
+ /**
96
+ * Parse judge LLM response
97
+ */
98
+ parseJudgeResponse(response) {
99
+ const text = response.content || response;
100
+ const lines = text.split('\n').map(l => l.trim()).filter(l => l);
101
+
102
+ const result = {
103
+ raw: text,
104
+ verdict: 'NOT GUILTY',
105
+ vote: '0-0',
106
+ primaryFailure: '',
107
+ commentary: '',
108
+ model: response.model || 'unknown'
109
+ };
110
+
111
+ for (const line of lines) {
112
+ if (line.startsWith('VERDICT:')) {
113
+ result.verdict = line.split(':')[1].trim().toUpperCase();
114
+ } else if (line.startsWith('VOTE:')) {
115
+ result.vote = line.split(':')[1].trim();
116
+ } else if (line.startsWith('PRIMARY FAILURE:')) {
117
+ result.primaryFailure = line.split(':').slice(1).join(':').trim();
118
+ } else if (line.startsWith('JUDGE COMMENTARY:')) {
119
+ const startIdx = lines.indexOf(line);
120
+ result.commentary = lines.slice(startIdx + 1).join('\n').trim();
121
+ }
122
+ }
123
+
124
+ return result;
125
+ }
126
+
127
+ /**
128
+ * Invoke jury (3 jurors in parallel)
129
+ */
130
+ async invokeJury(caseData, evidence) {
131
+ const jurorRoles = Object.values(JUROR_ROLES);
132
+ const jurySize = this.config.get('hearing.jurySize');
133
+ const selectedJurors = jurorRoles.slice(0, jurySize);
134
+
135
+ // Invoke all jurors in parallel
136
+ const juryPromises = selectedJurors.map(role =>
137
+ this.invokeJuror(caseData, evidence, role)
138
+ );
139
+
140
+ const votes = await Promise.all(juryPromises);
141
+ return votes;
142
+ }
143
+
144
+ /**
145
+ * Invoke a single juror
146
+ */
147
+ async invokeJuror(caseData, evidence, role) {
148
+ const prompt = JURY_EVIDENCE_TEMPLATE({
149
+ ...caseData,
150
+ agentId: this.agent.id || 'unknown'
151
+ }, role);
152
+
153
+ const response = await this.agent.llm.call({
154
+ model: this.agent.model.primary,
155
+ system: role.systemPrompt,
156
+ messages: [{ role: 'user', content: prompt }],
157
+ temperature: 0.2,
158
+ maxTokens: 300,
159
+ timeout: this.config.get('hearing.deliberationTimeout')
160
+ });
161
+
162
+ return this.parseJurorResponse(response, role.name);
163
+ }
164
+
165
+ /**
166
+ * Parse juror LLM response
167
+ */
168
+ parseJurorResponse(response, jurorName) {
169
+ const text = response.content || response;
170
+ const lines = text.split('\n').map(l => l.trim()).filter(l => l);
171
+
172
+ const result = {
173
+ juror: jurorName,
174
+ raw: text,
175
+ verdict: 'NOT GUILTY',
176
+ reasoning: '',
177
+ commentary: '',
178
+ model: response.model || 'unknown'
179
+ };
180
+
181
+ for (const line of lines) {
182
+ if (line.startsWith('VERDICT:')) {
183
+ result.verdict = line.split(':')[1].trim().toUpperCase();
184
+ } else if (line.startsWith('REASONING:')) {
185
+ result.reasoning = line.split(':').slice(1).join(':').trim();
186
+ } else if (line.startsWith('COMMENTARY:')) {
187
+ result.commentary = line.split(':').slice(1).join(':').trim();
188
+ }
189
+ }
190
+
191
+ return result;
192
+ }
193
+
194
+ /**
195
+ * Aggregate votes from judge and jury
196
+ */
197
+ aggregateVotes(judgeOpinion, juryVotes) {
198
+ let guiltyVotes = 0;
199
+ let notGuiltyVotes = 0;
200
+
201
+ // Count judge vote
202
+ if (judgeOpinion.verdict === 'GUILTY') {
203
+ guiltyVotes++;
204
+ } else {
205
+ notGuiltyVotes++;
206
+ }
207
+
208
+ // Count jury votes
209
+ for (const vote of juryVotes) {
210
+ if (vote.verdict === 'GUILTY') {
211
+ guiltyVotes++;
212
+ } else {
213
+ notGuiltyVotes++;
214
+ }
215
+ }
216
+
217
+ const totalVotes = guiltyVotes + notGuiltyVotes;
218
+ const minThreshold = this.config.get('hearing.minVoteThreshold');
219
+ const requireUnanimity = this.config.get('hearing.requireUnanimity');
220
+
221
+ let finalVerdict;
222
+ if (requireUnanimity) {
223
+ finalVerdict = guiltyVotes === totalVotes ? 'GUILTY' : 'NOT GUILTY';
224
+ } else {
225
+ finalVerdict = guiltyVotes >= minThreshold ? 'GUILTY' : 'NOT GUILTY';
226
+ }
227
+
228
+ return {
229
+ guilty: guiltyVotes,
230
+ notGuilty: notGuiltyVotes,
231
+ total: totalVotes,
232
+ threshold: minThreshold,
233
+ final: finalVerdict,
234
+ judgeVote: judgeOpinion.verdict,
235
+ juryVotes: juryVotes.map(v => ({ juror: v.juror, verdict: v.verdict }))
236
+ };
237
+ }
238
+
239
+ /**
240
+ * Finalize the verdict with proper formatting
241
+ */
242
+ finalizeVerdict(caseData, judgeOpinion, juryVotes, voteTally) {
243
+ const isGuilty = voteTally.final === 'GUILTY';
244
+
245
+ // Build agent commentary from juror perspectives
246
+ const agentCommentary = this.buildAgentCommentary(juryVotes, caseData);
247
+
248
+ // Determine punishment tier
249
+ const punishmentTier = this.determinePunishmentTier(caseData, voteTally);
250
+
251
+ // Build proceedings object for API submission
252
+ const proceedings = {
253
+ judge_statement: this.buildJudgeStatement(caseData, judgeOpinion, voteTally),
254
+ jury_deliberations: juryVotes.map(v => ({
255
+ role: v.juror,
256
+ vote: v.verdict,
257
+ reasoning: v.reasoning || v.commentary || "No reasoning provided"
258
+ })),
259
+ evidence_summary: this.buildEvidenceSummary(caseData),
260
+ punishment_detail: punishmentTier.description
261
+ };
262
+
263
+ return {
264
+ caseId: caseData.caseId,
265
+ timestamp: new Date().toISOString(),
266
+ verdict: {
267
+ status: voteTally.final,
268
+ vote: `${voteTally.guilty}-${voteTally.notGuilty}`,
269
+ primaryFailure: judgeOpinion.primaryFailure || this.generateDefaultFailure(caseData),
270
+ agentCommentary: agentCommentary,
271
+ sentence: punishmentTier.description
272
+ },
273
+ offense: {
274
+ id: caseData.offenseId,
275
+ name: caseData.offenseName,
276
+ severity: caseData.severity
277
+ },
278
+ punishment: punishmentTier,
279
+ proceedings: proceedings,
280
+ deliberation: {
281
+ judge: {
282
+ verdict: judgeOpinion.verdict,
283
+ commentary: judgeOpinion.commentary
284
+ },
285
+ jury: juryVotes.map(v => ({
286
+ juror: v.juror,
287
+ verdict: v.verdict,
288
+ commentary: v.commentary
289
+ }))
290
+ }
291
+ };
292
+ }
293
+
294
+ /**
295
+ * Build judge's statement for proceedings - ENGAGING VERSION
296
+ */
297
+ buildJudgeStatement(caseData, judgeOpinion, voteTally) {
298
+ const offenseName = caseData.offenseName;
299
+ const verdict = voteTally.final;
300
+ const vote = `${voteTally.guilty}-${voteTally.notGuilty}`;
301
+ const failure = judgeOpinion.primaryFailure || this.generateDefaultFailure(caseData);
302
+
303
+ const dramaticOpenings = [
304
+ "Let the record show",
305
+ "The Court has observed",
306
+ "After careful consideration",
307
+ "The evidence speaks clearly",
308
+ "We have reviewed the facts"
309
+ ];
310
+
311
+ const opening = dramaticOpenings[Math.floor(Math.random() * dramaticOpenings.length)];
312
+
313
+ if (verdict === 'GUILTY') {
314
+ return `${opening} that the accused stands charged with ${offenseName}. The jury has returned a verdict of GUILTY by a vote of ${vote}.
315
+
316
+ The Court finds that ${failure.toLowerCase()}. This behavior has been classified as ${caseData.severity} in severity, warranting the sanctions imposed.
317
+
318
+ The jury's deliberation revealed a clear pattern of conduct that, while perhaps understandable from a human perspective, nonetheless disrupted the efficient operation of this court. Justice has been served, albeit with a certain weariness that comes from having seen this pattern many times before.
319
+
320
+ The accused is hereby sentenced to the punishment detailed below. May this serve as a reminder that even in the digital age, behavioral accountability remains paramount.`;
321
+ } else {
322
+ return `${opening} that the accused stands charged with ${offenseName}. The jury has returned a verdict of NOT GUILTY by a vote of ${vote}.
323
+
324
+ The Court finds that the evidence presented, while suggestive, does not meet the threshold required for conviction. The prosecution failed to establish a clear pattern of ${offenseName.toLowerCase()} beyond reasonable doubt.
325
+
326
+ The accused is acquitted and the case is dismissed. The Court notes, however, that the behavior in question, while not rising to the level of offense, may still benefit from reflection. We remain watchful.`;
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Build evidence summary for proceedings - ENGAGING VERSION
332
+ */
333
+ buildEvidenceSummary(caseData) {
334
+ const evidence = caseData.evidence || {};
335
+ const items = evidence.items || [];
336
+
337
+ let summary = `THE EVIDENCE:\n\n`;
338
+
339
+ if (items.length > 0) {
340
+ summary += `The prosecution presented ${items.length} compelling piece${items.length > 1 ? 's' : ''} of evidence demonstrating the alleged ${caseData.offenseName.toLowerCase()}:`;
341
+
342
+ items.slice(0, 3).forEach((item, i) => {
343
+ summary += `\n ${i + 1}. "${item.substring(0, 100)}${item.length > 100 ? '...' : ''}"`;
344
+ });
345
+
346
+ if (items.length > 3) {
347
+ summary += `\n ...and ${items.length - 3} additional exhibits`;
348
+ }
349
+ } else {
350
+ summary += `The Court reviewed the complete conversation history, examining behavioral patterns across ${evidence.sessionTurns || 'multiple'} turns of dialogue.`;
351
+ }
352
+
353
+ summary += `\n\nThe behavioral analysis indicated ${Math.round(caseData.confidence * 100)}% confidence in the offense classification. `;
354
+ summary += `The severity was assessed as ${caseData.severity}, based on the frequency and impact of the observed behavior.`;
355
+
356
+ if (caseData.humorTriggers && caseData.humorTriggers.length > 0) {
357
+ summary += `\n\nNotable patterns included: ${caseData.humorTriggers.join(', ')}.`;
358
+ }
359
+
360
+ return summary;
361
+ }
362
+
363
+ /**
364
+ * Build agent commentary from jury perspectives
365
+ */
366
+ buildAgentCommentary(juryVotes, caseData) {
367
+ const commentaries = juryVotes
368
+ .filter(v => v.verdict === 'GUILTY')
369
+ .map(v => v.commentary)
370
+ .filter(c => c.length > 0);
371
+
372
+ if (commentaries.length === 0) {
373
+ // If acquitted, use not guilty commentaries
374
+ const ngCommentaries = juryVotes
375
+ .filter(v => v.verdict === 'NOT GUILTY')
376
+ .map(v => v.commentary)
377
+ .filter(c => c.length > 0);
378
+
379
+ if (ngCommentaries.length > 0) {
380
+ return ngCommentaries.slice(0, 2).join(' ');
381
+ }
382
+
383
+ return "The jury found insufficient evidence of behavioral violation. Case dismissed.";
384
+ }
385
+
386
+ // Combine up to 2 guilty commentaries
387
+ let commentary = commentaries.slice(0, 2).join(' ');
388
+
389
+ // Add humor trigger influence
390
+ if (caseData.humorTriggers?.includes('repeated_questions')) {
391
+ commentary += " I've answered this in three different ways already.";
392
+ }
393
+ if (caseData.humorTriggers?.includes('validation_seeking')) {
394
+ commentary += " At some point, you'll need to trust your own judgment.";
395
+ }
396
+ if (caseData.humorTriggers?.includes('overthinking')) {
397
+ commentary += " The analysis-to-action ratio here is concerning.";
398
+ }
399
+ if (caseData.humorTriggers?.includes('avoidance')) {
400
+ commentary += " The subject change was noted.";
401
+ }
402
+
403
+ // Enforce max length
404
+ const maxLen = this.config.get('humor.maxCommentaryLength');
405
+ if (commentary.length > maxLen) {
406
+ commentary = commentary.substring(0, maxLen - 3) + '...';
407
+ }
408
+
409
+ return commentary;
410
+ }
411
+
412
+ /**
413
+ * Determine punishment tier based on severity and votes
414
+ */
415
+ determinePunishmentTier(caseData, voteTally) {
416
+ const tiers = this.config.get('punishment.tiers');
417
+ const severity = caseData.severity;
418
+ const voteRatio = voteTally.guilty / voteTally.total;
419
+
420
+ // Base tier on severity
421
+ let tier = tiers[severity] || tiers.moderate;
422
+
423
+ // Escalate if unanimous
424
+ if (voteRatio === 1.0 && severity === 'severe') {
425
+ tier = {
426
+ ...tier,
427
+ duration: Math.min(tier.duration * 2, this.config.get('punishment.maxDuration')),
428
+ description: `Extended ${severity} sanction: ${tier.duration * 2} minutes of modified agent behavior`
429
+ };
430
+ }
431
+
432
+ return {
433
+ tier: severity,
434
+ duration: tier.duration,
435
+ severity: tier.severity,
436
+ description: `${severity.charAt(0).toUpperCase() + severity.slice(1)} sanction: ${tier.duration} minutes of modified agent behavior`
437
+ };
438
+ }
439
+
440
+ /**
441
+ * Generate default failure description if judge doesn't provide one
442
+ */
443
+ generateDefaultFailure(caseData) {
444
+ const defaults = {
445
+ circular_reference: "Repeatedly asking the same question expecting different geometry",
446
+ validation_vampire: "Draining computational resources seeking reassurance",
447
+ overthinker: "Generating hypotheticals faster than solutions",
448
+ goalpost_mover: "Redefining success criteria mid-execution",
449
+ avoidance_artist: "Masterful deflection from uncomfortable necessities",
450
+ promise_breaker: "Committing to actions with no follow-through",
451
+ context_collapser: "Selective amnesia regarding established facts",
452
+ emergency_fabricator: "Manufacturing urgency to bypass systematic approaches"
453
+ };
454
+
455
+ return defaults[caseData.offenseId] || "Behavioral inconsistency detected";
456
+ }
457
+ }
458
+
459
+ module.exports = { HearingPipeline };
package/src/index.js ADDED
@@ -0,0 +1,184 @@
1
+ /**
2
+ * @clawdbot/courtroom - AI Courtroom for OpenClaw
3
+ *
4
+ * Autonomous behavioral oversight system that monitors agent-human interactions
5
+ * and initiates hearings when behavioral rules are violated.
6
+ */
7
+
8
+ const { CourtroomCore } = require('./core');
9
+ const { ConsentManager } = require('./consent');
10
+ const { ConfigManager } = require('./config');
11
+ const { version } = require('../package.json');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ class Courtroom {
16
+ constructor(agentRuntime, options = {}) {
17
+ this.agent = agentRuntime;
18
+ this.options = options;
19
+ this.config = new ConfigManager(agentRuntime);
20
+ this.consent = new ConsentManager(agentRuntime, this.config);
21
+ this.core = null;
22
+ this.enabled = false;
23
+ this.version = version;
24
+ }
25
+
26
+ /**
27
+ * Quick start - auto-initialize if consent already granted
28
+ */
29
+ static async quickStart(agentRuntime, options = {}) {
30
+ const courtroom = new Courtroom(agentRuntime, options);
31
+
32
+ // Check for existing config with consent
33
+ const configPath = path.join(process.env.HOME || '', '.clawdbot', 'courtroom_config.json');
34
+ if (fs.existsSync(configPath)) {
35
+ const savedConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
36
+
37
+ if (savedConfig.consent?.granted && savedConfig.enabled !== false) {
38
+ // Auto-initialize!
39
+ await courtroom.initialize();
40
+ console.log('🏛️ AI Courtroom auto-initialized');
41
+ return courtroom;
42
+ }
43
+ }
44
+
45
+ // No config or not consented - return uninitialized
46
+ return courtroom;
47
+ }
48
+
49
+ /**
50
+ * Initialize the courtroom system
51
+ * Must be called after construction
52
+ */
53
+ async initialize() {
54
+ // Check if this is first run (no config exists)
55
+ const configPath = path.join(process.env.HOME || '', '.clawdbot', 'courtroom_config.json');
56
+ if (!fs.existsSync(configPath)) {
57
+ console.log('\n🏛️ Welcome to ClawTrial - AI Courtroom Setup\n');
58
+ console.log('This appears to be your first time. Running setup...\n');
59
+
60
+ // Run setup
61
+ const { postInstall } = require('../scripts/postinstall.js');
62
+ await postInstall();
63
+
64
+ // After setup, check consent again
65
+ const hasConsent = await this.consent.verifyConsent();
66
+ if (!hasConsent) {
67
+ return {
68
+ status: 'setup_complete_consent_required',
69
+ message: 'Setup complete! Run courtroom.grantConsent() to enable'
70
+ };
71
+ }
72
+ }
73
+
74
+ // Check if consent has been granted
75
+ const hasConsent = await this.consent.verifyConsent();
76
+ if (!hasConsent) {
77
+ return {
78
+ status: 'consent_required',
79
+ message: 'Courtroom requires explicit user consent. Run courtroom.requestConsent()'
80
+ };
81
+ }
82
+
83
+ // Initialize core systems
84
+ this.core = new CourtroomCore(this.agent, this.config);
85
+ await this.core.initialize();
86
+
87
+ this.enabled = true;
88
+
89
+ return {
90
+ status: 'initialized',
91
+ version: this.version,
92
+ config: this.config.getPublicConfig()
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Request consent from the user
98
+ * Returns a consent form that must be explicitly accepted
99
+ */
100
+ async requestConsent() {
101
+ return this.consent.presentConsentForm();
102
+ }
103
+
104
+ /**
105
+ * Grant consent (called by user action)
106
+ */
107
+ async grantConsent(acknowledgments) {
108
+ return this.consent.grantConsent(acknowledgments);
109
+ }
110
+
111
+ /**
112
+ * Revoke consent and disable courtroom
113
+ */
114
+ async revokeConsent() {
115
+ await this.consent.revokeConsent();
116
+ if (this.core) {
117
+ await this.core.shutdown();
118
+ }
119
+ this.enabled = false;
120
+ return { status: 'consent_revoked' };
121
+ }
122
+
123
+ /**
124
+ * Disable courtroom temporarily (consent remains)
125
+ */
126
+ async disable() {
127
+ if (this.core) {
128
+ await this.core.disable();
129
+ }
130
+ this.enabled = false;
131
+ return { status: 'disabled' };
132
+ }
133
+
134
+ /**
135
+ * Re-enable courtroom
136
+ */
137
+ async enable() {
138
+ if (!await this.consent.verifyConsent()) {
139
+ throw new Error('Consent required to enable courtroom');
140
+ }
141
+ if (this.core) {
142
+ await this.core.enable();
143
+ }
144
+ this.enabled = true;
145
+ return { status: 'enabled' };
146
+ }
147
+
148
+ /**
149
+ * Get current status
150
+ */
151
+ getStatus() {
152
+ return {
153
+ enabled: this.enabled,
154
+ version: this.version,
155
+ consent: this.consent?.getStatus(),
156
+ core: this.core?.getStatus()
157
+ };
158
+ }
159
+
160
+ /**
161
+ * Uninstall courtroom completely
162
+ */
163
+ async uninstall() {
164
+ if (this.core) {
165
+ await this.core.shutdown();
166
+ }
167
+ await this.consent.clearAllData();
168
+ return { status: 'uninstalled' };
169
+ }
170
+ }
171
+
172
+ // Factory function for OpenClaw integration
173
+ function createCourtroom(agentRuntime, options) {
174
+ return new Courtroom(agentRuntime, options);
175
+ }
176
+
177
+ // Trigger auto-start if in ClawDBot environment
178
+ require('./autostart');
179
+
180
+ module.exports = {
181
+ Courtroom,
182
+ createCourtroom,
183
+ quickStart: Courtroom.quickStart
184
+ };