@iamoberlin/chorus 2.2.1 → 2.4.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/README.md CHANGED
@@ -8,6 +8,12 @@
8
8
 
9
9
  CHORUS implements the Nine Choirs architecture — hierarchical cognition modeled on Pseudo-Dionysius's *Celestial Hierarchy*. Illumination descends through the choirs; understanding ascends. The agent is sanctified through structure.
10
10
 
11
+ ## What's New in v2.3
12
+
13
+ - **Unified delivery path.** Scheduler and manual commands now share one channel-aware delivery adapter for consistent formatting and routing.
14
+ - **Channel-agnostic output.** Choir output adapts to the destination automatically (plain text for iMessage, markdown for webchat, etc.).
15
+ - **Context passing fixed.** Illumination flow works correctly across all entry points — `run`, `vision`, and scheduled execution.
16
+
11
17
  ## The Core Idea
12
18
 
13
19
  Most AI agents are frozen. Same prompts, same limitations, no growth. CHORUS changes that through **architecture**:
package/index.ts CHANGED
@@ -12,6 +12,7 @@ import { loadChorusConfig, type ChorusPluginConfig } from "./src/config.js";
12
12
  import { createSecurityHooks } from "./src/security.js";
13
13
  import { createChoirScheduler } from "./src/scheduler.js";
14
14
  import { CHOIRS, formatFrequency, CASCADE_ORDER } from "./src/choirs.js";
15
+ import { deliverChoirOutput } from "./src/delivery.js";
15
16
  import {
16
17
  getTodayMetrics,
17
18
  getMetricsForDate,
@@ -21,8 +22,9 @@ import {
21
22
  formatMetricsSummary,
22
23
  formatWeeklySummary,
23
24
  } from "./src/metrics.js";
24
- import { createDaemon, DEFAULT_DAEMON_CONFIG, type DaemonConfig } from "./src/daemon.js";
25
+ import { createDaemon, type DaemonConfig } from "./src/daemon.js";
25
26
  import { getInboxPath } from "./src/senses.js";
27
+ import { formatEconomicsSummary, calculateAllROI } from "./src/economics.js";
26
28
  import {
27
29
  loadPurposes,
28
30
  addPurpose,
@@ -32,7 +34,6 @@ import {
32
34
  } from "./src/purposes.js";
33
35
  import {
34
36
  createPurposeResearchScheduler,
35
- DEFAULT_PURPOSE_RESEARCH_CONFIG,
36
37
  type PurposeResearchConfig,
37
38
  } from "./src/purpose-research.js";
38
39
  // On-chain prayer imports are lazy-loaded in pray commands to avoid startup cost
@@ -69,11 +70,7 @@ const plugin = {
69
70
  }
70
71
 
71
72
  // Register daemon service
72
- const daemonConfig: DaemonConfig = {
73
- ...DEFAULT_DAEMON_CONFIG,
74
- enabled: (pluginConfig as any)?.daemon?.enabled ?? true,
75
- ...(pluginConfig as any)?.daemon,
76
- };
73
+ const daemonConfig: DaemonConfig = config.daemon;
77
74
 
78
75
  let daemon: ReturnType<typeof createDaemon> | null = null;
79
76
  if (daemonConfig.enabled) {
@@ -85,13 +82,7 @@ const plugin = {
85
82
  }
86
83
 
87
84
  // Register purpose research service
88
- const purposeResearchConfig: PurposeResearchConfig = {
89
- ...DEFAULT_PURPOSE_RESEARCH_CONFIG,
90
- enabled: config.purposeResearch.enabled,
91
- dailyRunCap: config.purposeResearch.dailyRunCap,
92
- defaultFrequency: config.purposeResearch.defaultFrequency,
93
- defaultMaxFrequency: config.purposeResearch.defaultMaxFrequency,
94
- };
85
+ const purposeResearchConfig: PurposeResearchConfig = config.purposeResearch;
95
86
 
96
87
  let purposeResearch: ReturnType<typeof createPurposeResearchScheduler> | null = null;
97
88
  if (purposeResearchConfig.enabled) {
@@ -130,68 +121,6 @@ const plugin = {
130
121
  return '';
131
122
  }
132
123
 
133
- // Deliver choir output to user via OpenClaw messaging (channel-agnostic)
134
- // Reads target from OpenClaw config (channels.*.allowFrom) — no hardcoded PII
135
- function deliverIfNeeded(choir: typeof CHOIRS[string], text: string): void {
136
- if (!choir.delivers || !text || text === 'HEARTBEAT_OK' || text === 'NO_REPLY') return;
137
-
138
- // Resolve delivery target from OpenClaw channel config
139
- const channels = api.config?.channels as Record<string, any> | undefined;
140
- let target: string | undefined;
141
- let channel: string | undefined;
142
-
143
- if (channels) {
144
- for (const [ch, cfg] of Object.entries(channels)) {
145
- if (cfg?.enabled && cfg?.allowFrom?.[0]) {
146
- target = cfg.allowFrom[0];
147
- channel = ch;
148
- break;
149
- }
150
- }
151
- }
152
-
153
- if (!target) {
154
- console.log(` ⚠ No delivery target found in OpenClaw config`);
155
- return;
156
- }
157
-
158
- // Strip markdown for channels that don't support it
159
- let deliveryText = text.slice(0, 4000);
160
- if (channel === 'imessage') {
161
- deliveryText = deliveryText
162
- .replace(/\*\*(.+?)\*\*/g, '$1') // bold
163
- .replace(/\*(.+?)\*/g, '$1') // italic
164
- .replace(/__(.+?)__/g, '$1') // bold alt
165
- .replace(/_(.+?)_/g, '$1') // italic alt
166
- .replace(/`(.+?)`/g, '$1') // inline code
167
- .replace(/```[\s\S]*?```/g, '') // code blocks
168
- .replace(/^#{1,6}\s+/gm, '') // headers
169
- .replace(/^\s*[-*+]\s+/gm, '• ') // bullet lists
170
- .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1'); // links
171
- }
172
-
173
- try {
174
- const args = [
175
- 'message', 'send',
176
- '--target', target,
177
- '--message', deliveryText,
178
- ];
179
- if (channel) args.push('--channel', channel);
180
-
181
- const deliveryResult = spawnSync('openclaw', args, {
182
- encoding: 'utf-8',
183
- timeout: 30000,
184
- });
185
- if (deliveryResult.status === 0) {
186
- console.log(` 📨 Delivered to user via ${channel || 'default'}`);
187
- } else {
188
- console.log(` ⚠ Delivery failed: ${(deliveryResult.stderr || '').slice(0, 80)}`);
189
- }
190
- } catch (err: any) {
191
- console.log(` ⚠ Delivery error: ${(err.message || '').slice(0, 80)}`);
192
- }
193
- }
194
-
195
124
  // Register CLI
196
125
  api.registerCli((ctx) => {
197
126
  const program = ctx.program.command("chorus").description("CHORUS Nine Choirs management");
@@ -315,7 +244,7 @@ const plugin = {
315
244
  const preview = text.slice(0, 150).replace(/\n/g, ' ');
316
245
  console.log(` ${preview}${text.length > 150 ? '...' : ''}`);
317
246
  }
318
- deliverIfNeeded(choir, text);
247
+ await deliverChoirOutput(api, choir, text, api.logger);
319
248
  } catch (err) {
320
249
  console.error(` ✗ ${choir.name} failed:`, err);
321
250
  }
@@ -341,7 +270,7 @@ const plugin = {
341
270
  const preview = text.slice(0, 150).replace(/\n/g, ' ');
342
271
  console.log(` ${preview}${text.length > 150 ? '...' : ''}`);
343
272
  }
344
- deliverIfNeeded(choir, text);
273
+ await deliverChoirOutput(api, choir, text, api.logger);
345
274
  } else {
346
275
  const errMsg = result.stderr || result.stdout || 'Unknown error';
347
276
  if (errMsg.includes('ECONNREFUSED') || errMsg.includes('connect')) {
@@ -483,7 +412,7 @@ const plugin = {
483
412
  console.log(` ✓`);
484
413
 
485
414
  // Deliver output to user via OpenClaw messaging if choir is marked for delivery
486
- deliverIfNeeded(choir, text);
415
+ void deliverChoirOutput(api, choir, text, api.logger);
487
416
  } catch {
488
417
  const fallback = result.stdout?.slice(-2000) || `[${choir.name} completed]`;
489
418
  contextStore.set(choirId, fallback);
@@ -684,6 +613,80 @@ const plugin = {
684
613
  }
685
614
  });
686
615
 
616
+ // Economics commands
617
+ const econCmd = program.command("economics").alias("econ").description("Attention economics (Phase 1: observe)");
618
+
619
+ econCmd
620
+ .command("summary")
621
+ .description("Show ROI summary per choir")
622
+ .option("--days <n>", "Rolling window in days", "7")
623
+ .action((opts: any) => {
624
+ console.log("");
625
+ console.log(formatEconomicsSummary(parseInt(opts.days) || 7));
626
+ console.log("");
627
+ });
628
+
629
+ econCmd
630
+ .command("roi")
631
+ .description("Show ROI table for all choirs")
632
+ .option("--days <n>", "Rolling window in days", "7")
633
+ .action((opts: any) => {
634
+ const days = parseInt(opts.days) || 7;
635
+ const all = calculateAllROI(days);
636
+ console.log("");
637
+ console.log(`Choir ROI — ${days}d window`);
638
+ console.log("─".repeat(60));
639
+ for (const r of all) {
640
+ const roi = r.totalCostUsd > 0 ? `${r.roi.toFixed(1)}x` : "—";
641
+ const bar = r.totalCostUsd > 0 ? "█".repeat(Math.min(20, Math.round(r.roi * 2))) : "";
642
+ console.log(` ${r.choirId.padEnd(16)} ${roi.padStart(6)} $${r.totalCostUsd.toFixed(2).padStart(6)} ${bar}`);
643
+ }
644
+ console.log("");
645
+ });
646
+
647
+ // ── Behavioral Sink Health (Universe 25) ──────────────────
648
+ program
649
+ .command("health")
650
+ .description("Behavioral sink health check (Universe 25 anti-collapse)")
651
+ .action(async () => {
652
+ const { getSinkHealthSummary } = await import("./src/behavioral-sink.js");
653
+ const health = getSinkHealthSummary();
654
+ console.log("");
655
+ console.log(health.healthy ? "🟢 CHORUS Health: HEALTHY" : "🔴 CHORUS Health: DEGRADED");
656
+ console.log("═".repeat(50));
657
+ console.log("");
658
+
659
+ if (Object.keys(health.choirHealth).length === 0) {
660
+ console.log(" No quality data yet. Run some choir cycles first.");
661
+ } else {
662
+ console.log(" Choir Quality (avg over last 10 runs):");
663
+ for (const [id, h] of Object.entries(health.choirHealth) as any) {
664
+ const bar = "█".repeat(Math.round(h.avgQuality / 5)) + "░".repeat(20 - Math.round(h.avgQuality / 5));
665
+ const trendIcon = h.trend === "improving" ? "📈" : h.trend === "declining" ? "📉" : "➡️";
666
+ const boAlert = h.beautifulOneCount > 0 ? ` 🪞×${h.beautifulOneCount}` : "";
667
+ console.log(` ${id.padEnd(16)} [${bar}] ${h.avgQuality} ${trendIcon}${boAlert}`);
668
+ }
669
+ }
670
+
671
+ if (health.starvingPurposes.length > 0) {
672
+ console.log("");
673
+ console.log(" 🏚️ Starving Purposes:");
674
+ for (const p of health.starvingPurposes) {
675
+ console.log(` - ${p}`);
676
+ }
677
+ }
678
+
679
+ if (health.recentIncidents.length > 0) {
680
+ console.log("");
681
+ console.log(" 🪞 Recent Beautiful One Incidents:");
682
+ for (const i of health.recentIncidents) {
683
+ console.log(` ${i.timestamp.slice(0, 16)} ${i.choirId} (score: ${i.score})`);
684
+ }
685
+ }
686
+
687
+ console.log("");
688
+ });
689
+
687
690
  // Daemon commands
688
691
  const daemonCmd = program.command("daemon").description("Autonomous attention daemon");
689
692
 
@@ -2,7 +2,7 @@
2
2
  "id": "chorus",
3
3
  "name": "CHORUS",
4
4
  "description": "CHORUS: Hierarchy Of Recursive Unified Self-improvement",
5
- "version": "2.2.1",
5
+ "version": "2.3.0",
6
6
  "author": "Oberlin",
7
7
  "homepage": "https://chorus.oberlin.ai",
8
8
  "repository": "https://github.com/iamoberlin/chorus",
@@ -48,10 +48,40 @@
48
48
  "type": "boolean",
49
49
  "description": "Enable the daemon"
50
50
  },
51
+ "senses": {
52
+ "type": "object",
53
+ "description": "Enable/disable daemon input senses",
54
+ "properties": {
55
+ "inbox": {
56
+ "type": "boolean",
57
+ "description": "Watch inbox filesystem signals"
58
+ },
59
+ "purposes": {
60
+ "type": "boolean",
61
+ "description": "Watch purpose-state signals"
62
+ },
63
+ "time": {
64
+ "type": "boolean",
65
+ "description": "Watch time-based signals"
66
+ }
67
+ }
68
+ },
51
69
  "thinkThreshold": {
52
70
  "type": "number",
53
71
  "description": "Minimum priority to invoke cognition (0-100)"
54
72
  },
73
+ "pollIntervalMs": {
74
+ "type": "number",
75
+ "description": "Polling interval in milliseconds"
76
+ },
77
+ "minSleepMs": {
78
+ "type": "number",
79
+ "description": "Minimum sleep during high queue pressure in milliseconds"
80
+ },
81
+ "maxSleepMs": {
82
+ "type": "number",
83
+ "description": "Maximum sleep during quiet hours in milliseconds"
84
+ },
55
85
  "quietHoursStart": {
56
86
  "type": "number",
57
87
  "description": "Hour to start quiet mode (0-23)"
@@ -81,6 +111,44 @@
81
111
  "defaultMaxFrequency": {
82
112
  "type": "number",
83
113
  "description": "Maximum frequency when deadline is urgent"
114
+ },
115
+ "researchTimeoutMs": {
116
+ "type": "number",
117
+ "description": "Research run timeout in milliseconds"
118
+ },
119
+ "checkIntervalMs": {
120
+ "type": "number",
121
+ "description": "Research scheduler check interval in milliseconds"
122
+ }
123
+ }
124
+ },
125
+ "prayers": {
126
+ "type": "object",
127
+ "description": "On-chain prayer chain configuration",
128
+ "properties": {
129
+ "enabled": {
130
+ "type": "boolean",
131
+ "description": "Enable prayer chain commands"
132
+ },
133
+ "rpcUrl": {
134
+ "type": "string",
135
+ "description": "Solana RPC endpoint URL"
136
+ },
137
+ "autonomous": {
138
+ "type": "boolean",
139
+ "description": "Allow autonomous prayer actions without manual approval"
140
+ },
141
+ "maxBountySOL": {
142
+ "type": "number",
143
+ "description": "Maximum bounty per prayer in SOL"
144
+ },
145
+ "defaultTTL": {
146
+ "type": "number",
147
+ "description": "Default prayer time-to-live in seconds"
148
+ },
149
+ "keypairPath": {
150
+ "type": "string",
151
+ "description": "Path to Solana keypair file"
84
152
  }
85
153
  }
86
154
  }
@@ -118,6 +186,11 @@
118
186
  "purposeResearch": {
119
187
  "label": "Purpose Research",
120
188
  "help": "Configure purpose-derived research scheduler"
189
+ },
190
+ "prayers": {
191
+ "label": "Prayer Chain",
192
+ "advanced": true,
193
+ "help": "Configure on-chain prayer settings (RPC, autonomy, bounty limits, keypair)"
121
194
  }
122
195
  }
123
196
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iamoberlin/chorus",
3
- "version": "2.2.1",
4
- "description": "CHORUS: Hierarchy Of Recursive Unified Self-improvement with on-chain Prayer Chain (Solana)",
3
+ "version": "2.4.0",
4
+ "description": "CHORUS: Hierarchy Of Recursive Unified Self-improvement \u2014 with on-chain Prayer Chain (Solana)",
5
5
  "author": "Oberlin <iam@oberlin.ai>",
6
6
  "license": "MIT",
7
7
  "repository": {