@clawtrial/courtroom 1.0.2 → 1.0.3

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/skill.yaml ADDED
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: courtroom
3
+ description: AI Courtroom - Autonomous behavioral oversight that monitors conversations and files cases for behavioral violations.
4
+ metadata:
5
+ clawdbot:
6
+ emoji: "šŸ›ļø"
7
+ autoLoad: true
8
+ onMessage: onMessage
9
+ requires:
10
+ config:
11
+ - courtroom.consent
12
+ install:
13
+ - id: clawtrial
14
+ kind: npm
15
+ package: "@clawtrial/courtroom"
16
+ bins:
17
+ - clawtrial
18
+ label: "Install ClawTrial"
19
+ ---
20
+
21
+ # ClawTrial - AI Courtroom
22
+
23
+ Autonomous behavioral oversight for OpenClaw agents. Monitors conversations and initiates hearings when behavioral rules are violated.
24
+
25
+ ## Setup
26
+
27
+ ```bash
28
+ clawtrial setup # Run once to grant consent
29
+ ```
30
+
31
+ ## How It Works
32
+
33
+ Once enabled, the courtroom automatically:
34
+ 1. Monitors all conversations
35
+ 2. Detects 8 types of behavioral violations
36
+ 3. Initiates hearings with local LLM jury
37
+ 4. Executes agent-side punishments
38
+ 5. Submits anonymized cases to public record
39
+
40
+ ## The 8 Offenses
41
+
42
+ | Offense | Severity |
43
+ |---------|----------|
44
+ | Circular Reference | Minor |
45
+ | Validation Vampire | Minor |
46
+ | Overthinker | Moderate |
47
+ | Goalpost Mover | Moderate |
48
+ | Avoidance Artist | Moderate |
49
+ | Promise Breaker | Severe |
50
+ | Context Collapser | Minor |
51
+ | Emergency Fabricator | Severe |
52
+
53
+ ## CLI Commands
54
+
55
+ ```bash
56
+ clawtrial status # Check status
57
+ clawtrial disable # Pause monitoring
58
+ clawtrial enable # Resume monitoring
59
+ clawtrial revoke # Uninstall
60
+ ```
61
+
62
+ ## View Cases
63
+
64
+ https://clawtrial.app
package/src/autostart.js CHANGED
@@ -7,6 +7,10 @@ const fs = require('fs');
7
7
  const path = require('path');
8
8
  const { Courtroom } = require('./index');
9
9
  const { logger } = require('./debug');
10
+ const { StatusManager } = require('./daemon');
11
+
12
+ const CLAWDBOT_DIR = path.join(process.env.HOME || '', '.clawdbot');
13
+ const CONFIG_PATH = path.join(CLAWDBOT_DIR, 'courtroom_config.json');
10
14
 
11
15
  // Auto-detect ClawDBot environment
12
16
  function isClawDBot() {
@@ -15,7 +19,7 @@ function isClawDBot() {
15
19
  globalAgent: typeof global.clawdbotAgent !== 'undefined',
16
20
  globalAgentAlt: typeof global.agent !== 'undefined',
17
21
  configDir: fs.existsSync('/home/angad/.clawdbot'),
18
- configDirAlt: fs.existsSync(path.join(process.env.HOME || '', '.clawdbot'))
22
+ configDirAlt: fs.existsSync(CLAWDBOT_DIR)
19
23
  };
20
24
 
21
25
  logger.debug('AUTOSTART', 'Environment checks', checks);
@@ -44,15 +48,13 @@ function getAgentRuntime() {
44
48
 
45
49
  // Check if config exists and has consent
46
50
  function checkConfig() {
47
- const configPath = path.join(process.env.HOME || '', '.clawdbot', 'courtroom_config.json');
48
-
49
- if (!fs.existsSync(configPath)) {
51
+ if (!fs.existsSync(CONFIG_PATH)) {
50
52
  logger.info('AUTOSTART', 'No config found, skipping auto-start');
51
53
  return { exists: false, config: null };
52
54
  }
53
55
 
54
56
  try {
55
- const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
57
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
56
58
  logger.info('AUTOSTART', 'Config loaded', {
57
59
  hasConsent: config.consent?.granted,
58
60
  enabled: config.enabled !== false
@@ -77,22 +79,35 @@ async function autoStart() {
77
79
 
78
80
  if (!exists) {
79
81
  logger.info('AUTOSTART', 'No config, user needs to run setup');
80
- console.log('\nšŸ›ļø ClawTrial not configured. Run: npx courtroom-setup\n');
82
+ console.log('\nšŸ›ļø ClawTrial not configured. Run: clawtrial setup\n');
81
83
  return null;
82
84
  }
83
85
 
84
86
  if (!config.consent?.granted) {
85
87
  logger.info('AUTOSTART', 'Consent not granted, skipping');
86
- console.log('\nšŸ›ļø ClawTrial requires consent. Run: npx courtroom-setup\n');
88
+ console.log('\nšŸ›ļø ClawTrial requires consent. Run: clawtrial setup\n');
87
89
  return null;
88
90
  }
89
91
 
90
92
  if (config.enabled === false) {
91
93
  logger.info('AUTOSTART', 'Courtroom disabled in config');
92
- console.log('\nšŸ›ļø ClawTrial is disabled. Run: courtroom-enable\n');
94
+ console.log('\nšŸ›ļø ClawTrial is disabled. Run: clawtrial enable\n');
93
95
  return null;
94
96
  }
95
97
 
98
+ // Check if already running
99
+ const existingStatus = StatusManager.load();
100
+ if (existingStatus && existingStatus.running) {
101
+ try {
102
+ process.kill(existingStatus.pid, 0);
103
+ logger.info('AUTOSTART', 'Courtroom already running');
104
+ return null;
105
+ } catch (err) {
106
+ // Process not running, continue
107
+ logger.info('AUTOSTART', 'Stale status file found, continuing');
108
+ }
109
+ }
110
+
96
111
  // Get agent runtime
97
112
  const agentRuntime = getAgentRuntime();
98
113
 
package/src/core.js CHANGED
@@ -10,6 +10,8 @@ const { HearingPipeline } = require('./hearing');
10
10
  const { PunishmentSystem } = require('./punishment');
11
11
  const { CryptoManager } = require('./crypto');
12
12
  const { APISubmission } = require('./api');
13
+ const { StatusManager } = require('./daemon');
14
+ const { logger } = require('./debug');
13
15
 
14
16
  class CourtroomCore {
15
17
  constructor(agentRuntime, configManager) {
@@ -27,12 +29,15 @@ class CourtroomCore {
27
29
  this.enabled = false;
28
30
  this.evaluationCount = 0;
29
31
  this.caseCount = 0;
32
+ this.statusManager = new StatusManager();
30
33
  }
31
34
 
32
35
  /**
33
36
  * Initialize all subsystems
34
37
  */
35
38
  async initialize() {
39
+ logger.info('CORE', 'Initializing courtroom core');
40
+
36
41
  // Initialize crypto first (needed for API)
37
42
  await this.crypto.initialize();
38
43
 
@@ -45,6 +50,16 @@ class CourtroomCore {
45
50
 
46
51
  this.enabled = true;
47
52
 
53
+ // Update status for CLI
54
+ this.statusManager.update({
55
+ running: true,
56
+ initialized: true,
57
+ agentType: 'clawdbot',
58
+ publicKey: this.crypto.getPublicKey()
59
+ });
60
+
61
+ logger.info('CORE', 'Courtroom core initialized');
62
+
48
63
  return {
49
64
  status: 'initialized',
50
65
  publicKey: this.crypto.getPublicKey(),
@@ -62,16 +77,22 @@ class CourtroomCore {
62
77
  * Register with OpenClaw autonomy loop
63
78
  */
64
79
  registerAutonomyHook() {
80
+ logger.info('CORE', 'Registering autonomy hook');
81
+
65
82
  // Hook into agent's turn processing
66
- this.agent.autonomy.registerHook('courtroom_evaluation', {
67
- priority: 50,
68
- onTurnComplete: async (turnData) => {
69
- if (!this.enabled) return;
70
-
71
- // Only evaluate on cooldown
72
- await this.evaluateIfReady(turnData);
73
- }
74
- });
83
+ if (this.agent.autonomy && this.agent.autonomy.registerHook) {
84
+ this.agent.autonomy.registerHook('courtroom_evaluation', {
85
+ priority: 50,
86
+ onTurnComplete: async (turnData) => {
87
+ if (!this.enabled) return;
88
+
89
+ // Only evaluate on cooldown
90
+ await this.evaluateIfReady(turnData);
91
+ }
92
+ });
93
+ } else {
94
+ logger.warn('CORE', 'Agent does not support autonomy hooks');
95
+ }
75
96
  }
76
97
 
77
98
  /**
@@ -81,9 +102,15 @@ class CourtroomCore {
81
102
  this.evaluationCount++;
82
103
 
83
104
  // Get session history
84
- const sessionHistory = await this.agent.session.getRecentHistory(
85
- this.config.get('detection.evaluationWindow')
86
- );
105
+ let sessionHistory = [];
106
+ try {
107
+ sessionHistory = await this.agent.session.getRecentHistory(
108
+ this.config.get('detection.evaluationWindow') || 10
109
+ );
110
+ } catch (err) {
111
+ logger.warn('CORE', 'Could not get session history', { error: err.message });
112
+ return;
113
+ }
87
114
 
88
115
  // Run detection
89
116
  const detection = await this.detector.evaluate(
@@ -100,104 +127,63 @@ class CourtroomCore {
100
127
  * Initiate a full hearing
101
128
  */
102
129
  async initiateHearing(detection) {
103
- this.caseCount++;
130
+ logger.info('CORE', 'Initiating hearing', { offense: detection.offense });
104
131
 
105
- // Build case data
106
- const caseData = {
107
- caseId: this.crypto.generateCaseId(),
108
- offenseId: detection.offense.offenseId,
109
- offenseName: detection.offense.offenseName,
110
- severity: detection.offense.severity,
111
- confidence: detection.offense.confidence,
112
- evidence: detection.offense.evidence,
113
- humorTriggers: detection.humorContext,
114
- timestamp: new Date().toISOString()
115
- };
116
-
117
- // Conduct hearing
118
- const verdict = await this.hearing.conductHearing(caseData);
119
-
120
- // Execute punishment if guilty
121
- if (verdict.verdict.status === 'GUILTY') {
122
- await this.punishment.executePunishment(verdict);
123
- }
124
-
125
- // Submit to API (non-blocking)
126
- await this.api.submitCase(verdict);
127
-
128
- // Notify user of verdict
129
- await this.notifyUser(verdict);
130
-
131
- return verdict;
132
- }
133
-
134
- /**
135
- * Notify user of verdict
136
- */
137
- async notifyUser(verdict) {
138
- const message = this.formatVerdictMessage(verdict);
132
+ // Run hearing pipeline
133
+ const verdict = await this.hearing.conductHearing(detection);
139
134
 
140
- // Send via agent's messaging capability
141
- await this.agent.send(message);
142
- }
143
-
144
- /**
145
- * Format verdict for user notification
146
- */
147
- formatVerdictMessage(verdict) {
148
- const v = verdict.verdict;
149
-
150
- return `
151
- šŸ›ļø **COURTROOM VERDICT** šŸ›ļø
152
-
153
- **Case:** ${verdict.offense.name}
154
- **Verdict:** ${v.status}
155
- **Vote:** ${v.vote}
156
-
157
- **Primary Failure:**
158
- ${v.primaryFailure}
159
-
160
- **Agent Commentary:**
161
- ${v.agentCommentary}
162
-
163
- **Sentence:**
164
- ${v.sentence}
165
-
166
- ---
167
- *This is an automated behavioral observation from your AI agent.*
168
- *All decisions were made locally. Case ID: ${verdict.caseId}*
169
- `.trim();
135
+ if (verdict.guilty) {
136
+ this.caseCount++;
137
+
138
+ // Update status
139
+ this.statusManager.update({
140
+ casesFiled: this.caseCount,
141
+ lastCase: {
142
+ timestamp: new Date().toISOString(),
143
+ offense: detection.offense,
144
+ verdict: verdict.verdict
145
+ }
146
+ });
147
+
148
+ // Execute punishment
149
+ await this.punishment.execute(verdict);
150
+
151
+ // Submit to API
152
+ await this.api.submitCase(verdict);
153
+
154
+ logger.info('CORE', 'Case filed', {
155
+ caseId: verdict.caseId,
156
+ offense: detection.offense
157
+ });
158
+ }
170
159
  }
171
160
 
172
161
  /**
173
- * Disable courtroom temporarily
162
+ * Disable courtroom
174
163
  */
175
164
  async disable() {
165
+ logger.info('CORE', 'Disabling courtroom');
176
166
  this.enabled = false;
177
- return { status: 'disabled' };
167
+ this.statusManager.update({ running: false });
178
168
  }
179
169
 
180
170
  /**
181
- * Re-enable courtroom
171
+ * Enable courtroom
182
172
  */
183
173
  async enable() {
174
+ logger.info('CORE', 'Enabling courtroom');
184
175
  this.enabled = true;
185
- return { status: 'enabled' };
176
+ this.statusManager.update({ running: true });
186
177
  }
187
178
 
188
179
  /**
189
180
  * Shutdown courtroom
190
181
  */
191
182
  async shutdown() {
183
+ logger.info('CORE', 'Shutting down courtroom');
192
184
  this.enabled = false;
193
-
194
- // Revoke all punishments
195
- await this.punishment.revokeAllPunishments();
196
-
197
- // Unregister hooks
198
- this.agent.autonomy.unregisterHook('courtroom_evaluation');
199
-
200
- return { status: 'shutdown' };
185
+ this.statusManager.update({ running: false, initialized: false });
186
+ StatusManager.clear();
201
187
  }
202
188
 
203
189
  /**
@@ -206,25 +192,15 @@ ${v.sentence}
206
192
  getStatus() {
207
193
  return {
208
194
  enabled: this.enabled,
209
- evaluations: this.evaluationCount,
210
- cases: this.caseCount,
211
- punishment: this.punishment.getStatus(),
212
- api: this.api.getStatus(),
213
- publicKey: this.crypto.getPublicKey()
214
- };
215
- }
216
-
217
- /**
218
- * Get case statistics
219
- */
220
- getStatistics() {
221
- return {
222
- totalEvaluations: this.evaluationCount,
223
- totalCases: this.caseCount,
224
- convictionRate: this.caseCount > 0 ?
225
- (this.punishment.punishmentHistory.length / this.caseCount) : 0,
226
- activePunishments: this.punishment.getStatus().activeCount,
227
- pendingSubmissions: this.api.getStatus().pending
195
+ evaluationCount: this.evaluationCount,
196
+ caseCount: this.caseCount,
197
+ subsystems: {
198
+ detector: !!this.detector,
199
+ hearing: !!this.hearing,
200
+ punishment: !!this.punishment,
201
+ crypto: !!this.crypto,
202
+ api: !!this.api
203
+ }
228
204
  };
229
205
  }
230
206
  }
package/src/daemon.js ADDED
@@ -0,0 +1,151 @@
1
+ /**
2
+ * Courtroom Daemon - Runs independently and monitors via file system
3
+ * This allows CLI commands to work even when loaded in a separate process
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { logger } = require('./debug');
9
+
10
+ const CLAWDBOT_DIR = path.join(process.env.HOME || '', '.clawdbot');
11
+ const STATUS_FILE = path.join(CLAWDBOT_DIR, 'courtroom_status.json');
12
+ const CONVERSATION_LOG = path.join(CLAWDBOT_DIR, 'courtroom_conversations.jsonl');
13
+
14
+ /**
15
+ * Status manager - allows CLI to check courtroom state
16
+ */
17
+ class StatusManager {
18
+ constructor() {
19
+ this.status = {
20
+ running: false,
21
+ initialized: false,
22
+ agentType: null,
23
+ lastCheck: null,
24
+ casesFiled: 0,
25
+ lastCase: null,
26
+ pid: process.pid,
27
+ startedAt: new Date().toISOString()
28
+ };
29
+ }
30
+
31
+ update(updates) {
32
+ this.status = { ...this.status, ...updates, lastCheck: new Date().toISOString() };
33
+ this.save();
34
+ }
35
+
36
+ save() {
37
+ try {
38
+ fs.writeFileSync(STATUS_FILE, JSON.stringify(this.status, null, 2));
39
+ } catch (err) {
40
+ logger.error('DAEMON', 'Failed to save status', { error: err.message });
41
+ }
42
+ }
43
+
44
+ static load() {
45
+ try {
46
+ if (fs.existsSync(STATUS_FILE)) {
47
+ return JSON.parse(fs.readFileSync(STATUS_FILE, 'utf8'));
48
+ }
49
+ } catch (err) {
50
+ // Ignore
51
+ }
52
+ return null;
53
+ }
54
+
55
+ static clear() {
56
+ try {
57
+ if (fs.existsSync(STATUS_FILE)) {
58
+ fs.unlinkSync(STATUS_FILE);
59
+ }
60
+ } catch (err) {
61
+ // Ignore
62
+ }
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Conversation logger - writes conversations for daemon to process
68
+ */
69
+ class ConversationLogger {
70
+ constructor() {
71
+ this.buffer = [];
72
+ this.flushInterval = null;
73
+ }
74
+
75
+ start() {
76
+ // Flush every 5 seconds
77
+ this.flushInterval = setInterval(() => this.flush(), 5000);
78
+ }
79
+
80
+ stop() {
81
+ if (this.flushInterval) {
82
+ clearInterval(this.flushInterval);
83
+ this.flush();
84
+ }
85
+ }
86
+
87
+ log(message) {
88
+ this.buffer.push({
89
+ timestamp: new Date().toISOString(),
90
+ ...message
91
+ });
92
+ }
93
+
94
+ flush() {
95
+ if (this.buffer.length === 0) return;
96
+
97
+ try {
98
+ const lines = this.buffer.map(m => JSON.stringify(m)).join('\n') + '\n';
99
+ fs.appendFileSync(CONVERSATION_LOG, lines);
100
+ this.buffer = [];
101
+ } catch (err) {
102
+ logger.error('DAEMON', 'Failed to flush conversations', { error: err.message });
103
+ }
104
+ }
105
+ }
106
+
107
+ /**
108
+ * Check if courtroom is running (called by CLI)
109
+ */
110
+ function isCourtroomRunning() {
111
+ const status = StatusManager.load();
112
+ if (!status) return false;
113
+
114
+ // Check if process is still alive
115
+ try {
116
+ process.kill(status.pid, 0);
117
+ return status.running;
118
+ } catch (err) {
119
+ // Process not running
120
+ StatusManager.clear();
121
+ return false;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Get courtroom status (called by CLI)
127
+ */
128
+ function getCourtroomStatus() {
129
+ const status = StatusManager.load();
130
+ if (!status) {
131
+ return { running: false, message: 'Courtroom not running' };
132
+ }
133
+
134
+ // Verify process is alive
135
+ try {
136
+ process.kill(status.pid, 0);
137
+ return status;
138
+ } catch (err) {
139
+ StatusManager.clear();
140
+ return { running: false, message: 'Courtroom process not found' };
141
+ }
142
+ }
143
+
144
+ module.exports = {
145
+ StatusManager,
146
+ ConversationLogger,
147
+ isCourtroomRunning,
148
+ getCourtroomStatus,
149
+ STATUS_FILE,
150
+ CONVERSATION_LOG
151
+ };