@iamoberlin/chorus 2.2.0 → 2.3.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**:
@@ -280,7 +286,7 @@ When `autonomous: false` (default), all prayer chain interactions require explic
280
286
  - **TypeScript client** — wraps Anchor IDL with PDA derivation helpers
281
287
  - **Anchor events** — `PrayerPosted`, `PrayerAnswered`, `PrayerConfirmed`, `PrayerClaimed`, `PrayerCancelled` for off-chain indexing
282
288
  - **Local text cache** — CLI stores full text in `.prayer-texts.json` for display
283
- - **Program ID:** `DZuj1ZcX4H6THBSgW4GhKA7SbZNXtPDE5xPkW2jN53PQ`
289
+ - **Program ID:** `Af61jGnh2AceK3E8FAxCh9j7Jt6JWtJz6PUtbciDjVJS`
284
290
 
285
291
  ## Philosophy
286
292
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "address": "DZuj1ZcX4H6THBSgW4GhKA7SbZNXtPDE5xPkW2jN53PQ",
2
+ "address": "Af61jGnh2AceK3E8FAxCh9j7Jt6JWtJz6PUtbciDjVJS",
3
3
  "metadata": {
4
4
  "name": "chorus_prayers",
5
5
  "version": "0.1.0",
package/index.ts CHANGED
@@ -11,7 +11,8 @@ import { spawnSync } from "child_process";
11
11
  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
- import { CHOIRS, formatFrequency } from "./src/choirs.js";
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) {
@@ -102,6 +93,34 @@ const plugin = {
102
93
  api.logger.info("[chorus] Purpose research disabled");
103
94
  }
104
95
 
96
+ // Helper: resolve {choir_context} placeholders using a context store
97
+ function resolvePrompt(choir: typeof CHOIRS[string], ctxStore: Map<string, string>): string {
98
+ let prompt = choir.prompt;
99
+ for (const upstreamId of choir.receivesFrom) {
100
+ const placeholder = `{${upstreamId}_context}`;
101
+ const ctx = ctxStore.get(upstreamId);
102
+ prompt = prompt.replace(placeholder, ctx || `(no prior ${upstreamId} output)`);
103
+ }
104
+ return prompt;
105
+ }
106
+
107
+ // Helper: extract text from openclaw agent JSON output
108
+ function extractAgentText(stdout: string): string {
109
+ // Find the last top-level JSON object (skip plugin log noise)
110
+ for (let i = stdout.length - 1; i >= 0; i--) {
111
+ if (stdout[i] === '{') {
112
+ try {
113
+ const json = JSON.parse(stdout.slice(i));
114
+ const payloads = json.result?.payloads || [];
115
+ const texts = payloads.map((p: any) => p?.text || '').filter(Boolean);
116
+ if (texts.length > 0) return texts[texts.length - 1];
117
+ return json.result?.text || json.response || json.content || '';
118
+ } catch { /* keep searching */ }
119
+ }
120
+ }
121
+ return '';
122
+ }
123
+
105
124
  // Register CLI
106
125
  api.registerCli((ctx) => {
107
126
  const program = ctx.program.command("chorus").description("CHORUS Nine Choirs management");
@@ -187,6 +206,8 @@ const plugin = {
187
206
  }
188
207
  }
189
208
 
209
+ const runCtxStore: Map<string, string> = new Map();
210
+
190
211
  console.log("");
191
212
  if (!choirId) {
192
213
  console.log("🎵 Running all Nine Choirs in cascade order...");
@@ -198,10 +219,11 @@ const plugin = {
198
219
  if (!choir) continue;
199
220
 
200
221
  console.log(`Running ${choir.name}...`);
222
+ const prompt = resolvePrompt(choir, runCtxStore);
201
223
 
202
224
  // Preview mode - just show the prompt
203
225
  if (options?.preview) {
204
- console.log(` Prompt: ${choir.prompt.slice(0, 100)}...`);
226
+ console.log(` Prompt: ${prompt.slice(0, 100)}...`);
205
227
  continue;
206
228
  }
207
229
 
@@ -210,17 +232,19 @@ const plugin = {
210
232
  try {
211
233
  const result = await api.runAgentTurn({
212
234
  sessionLabel: `chorus:${id}`,
213
- message: choir.prompt,
235
+ message: prompt,
214
236
  isolated: true,
215
237
  timeoutSeconds: 300,
216
238
  });
217
239
  const text = result?.text || result?.payloads?.[0]?.text || '';
218
240
  const duration = result?.meta?.durationMs || 0;
241
+ runCtxStore.set(id, text.slice(0, 2000));
219
242
  console.log(` ✓ ${choir.name} complete (${(duration/1000).toFixed(1)}s)`);
220
243
  if (text) {
221
244
  const preview = text.slice(0, 150).replace(/\n/g, ' ');
222
245
  console.log(` ${preview}${text.length > 150 ? '...' : ''}`);
223
246
  }
247
+ await deliverChoirOutput(api, choir, text, api.logger);
224
248
  } catch (err) {
225
249
  console.error(` ✗ ${choir.name} failed:`, err);
226
250
  }
@@ -230,7 +254,7 @@ const plugin = {
230
254
  const result = spawnSync('openclaw', [
231
255
  'agent',
232
256
  '--session-id', `chorus:${id}`,
233
- '--message', choir.prompt,
257
+ '--message', prompt,
234
258
  '--json',
235
259
  ], {
236
260
  encoding: 'utf-8',
@@ -239,22 +263,14 @@ const plugin = {
239
263
  });
240
264
 
241
265
  if (result.status === 0) {
242
- try {
243
- // Extract JSON from output (may have plugin logs before it)
244
- const stdout = result.stdout || '';
245
- const jsonStart = stdout.indexOf('{');
246
- const jsonStr = jsonStart >= 0 ? stdout.slice(jsonStart) : '{}';
247
- const json = JSON.parse(jsonStr);
248
- const text = json.result?.payloads?.[0]?.text || '';
249
- const duration = json.result?.meta?.durationMs || 0;
250
- console.log(` ✓ ${choir.name} complete (${(duration/1000).toFixed(1)}s)`);
251
- if (text) {
252
- const preview = text.slice(0, 150).replace(/\n/g, ' ');
253
- console.log(` ${preview}${text.length > 150 ? '...' : ''}`);
254
- }
255
- } catch (parseErr) {
256
- console.log(` ✓ ${choir.name} complete (parse error: ${parseErr})`);
266
+ const text = extractAgentText(result.stdout || '');
267
+ runCtxStore.set(id, text.slice(0, 2000));
268
+ console.log(` ✓ ${choir.name} complete`);
269
+ if (text) {
270
+ const preview = text.slice(0, 150).replace(/\n/g, ' ');
271
+ console.log(` ${preview}${text.length > 150 ? '...' : ''}`);
257
272
  }
273
+ await deliverChoirOutput(api, choir, text, api.logger);
258
274
  } else {
259
275
  const errMsg = result.stderr || result.stdout || 'Unknown error';
260
276
  if (errMsg.includes('ECONNREFUSED') || errMsg.includes('connect')) {
@@ -271,7 +287,7 @@ const plugin = {
271
287
 
272
288
  console.log("");
273
289
  if (!choirId) {
274
- console.log("🎵 All choirs scheduled.");
290
+ console.log("🎵 All choirs complete.");
275
291
  }
276
292
  console.log("");
277
293
  });
@@ -348,10 +364,13 @@ const plugin = {
348
364
  const jsonStr = jsonStart >= 0 ? stdout.slice(jsonStart) : '{}';
349
365
  const json = JSON.parse(jsonStr);
350
366
  const text = json.result?.payloads?.[0]?.text || '';
367
+ contextStore.set(choirId, text.slice(0, 500));
351
368
  contextStore.set(`${choirId}:d${day}`, text.slice(0, 500));
352
369
  console.log(` ✓ (dry)`);
353
370
  } catch {
354
- contextStore.set(`${choirId}:d${day}`, `[${choir.name} would run]`);
371
+ const fallback = `[${choir.name} would run]`;
372
+ contextStore.set(choirId, fallback);
373
+ contextStore.set(`${choirId}:d${day}`, fallback);
355
374
  console.log(` ✓ (dry)`);
356
375
  }
357
376
  } else {
@@ -366,11 +385,14 @@ const plugin = {
366
385
  process.stdout.write(` ${choir.emoji} ${choir.name}...`);
367
386
 
368
387
  try {
388
+ // Resolve context from upstream choirs
389
+ const prompt = resolvePrompt(choir, contextStore);
390
+
369
391
  // Run the REAL choir with full tool access via direct agent call
370
392
  const result = spawnSync('openclaw', [
371
393
  'agent',
372
394
  '--session-id', `chorus:vision:${choirId}:d${day}`,
373
- '--message', choir.prompt,
395
+ '--message', prompt,
374
396
  '--json',
375
397
  ], {
376
398
  encoding: 'utf-8',
@@ -382,19 +404,19 @@ const plugin = {
382
404
  // Parse the agent response (extract JSON from output)
383
405
  try {
384
406
  const stdout = result.stdout || '';
385
- const jsonStart = stdout.indexOf('{');
386
- const jsonStr = jsonStart >= 0 ? stdout.slice(jsonStart) : '{}';
387
- const json = JSON.parse(jsonStr);
388
- const payloads = json.result?.payloads || [];
389
- const text = payloads.map((p: any) => p.text || '').filter(Boolean).join('\n\n') || '';
390
- const duration = json.result?.meta?.durationMs || 0;
391
- contextStore.set(`${choirId}:d${day}`, text.slice(0, 2000)); // Keep 2KB of response
407
+ const text = extractAgentText(stdout);
408
+ // Store by both choirId (for resolvePrompt) and choirId:dN (for summary)
409
+ contextStore.set(choirId, text.slice(0, 2000));
410
+ contextStore.set(`${choirId}:d${day}`, text.slice(0, 2000));
392
411
  successfulRuns++;
393
- console.log(` ✓ (${(duration/1000).toFixed(1)}s)`);
412
+ console.log(` ✓`);
394
413
 
395
- // Note: Archangels handles its own delivery via OpenClaw messaging tools
414
+ // Deliver output to user via OpenClaw messaging if choir is marked for delivery
415
+ void deliverChoirOutput(api, choir, text, api.logger);
396
416
  } catch {
397
- contextStore.set(`${choirId}:d${day}`, result.stdout?.slice(-2000) || `[${choir.name} completed]`);
417
+ const fallback = result.stdout?.slice(-2000) || `[${choir.name} completed]`;
418
+ contextStore.set(choirId, fallback);
419
+ contextStore.set(`${choirId}:d${day}`, fallback);
398
420
  successfulRuns++;
399
421
  console.log(` ✓`);
400
422
  }
@@ -591,6 +613,37 @@ const plugin = {
591
613
  }
592
614
  });
593
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
+
594
647
  // Daemon commands
595
648
  const daemonCmd = program.command("daemon").description("Autonomous attention daemon");
596
649
 
@@ -2,11 +2,17 @@
2
2
  "id": "chorus",
3
3
  "name": "CHORUS",
4
4
  "description": "CHORUS: Hierarchy Of Recursive Unified Self-improvement",
5
- "version": "1.3.4",
5
+ "version": "2.3.0",
6
6
  "author": "Oberlin",
7
7
  "homepage": "https://chorus.oberlin.ai",
8
8
  "repository": "https://github.com/iamoberlin/chorus",
9
- "keywords": ["cognitive-architecture", "rsi", "self-improvement", "nine-choirs", "purposes"],
9
+ "keywords": [
10
+ "cognitive-architecture",
11
+ "rsi",
12
+ "self-improvement",
13
+ "nine-choirs",
14
+ "purposes"
15
+ ],
10
16
  "configSchema": {
11
17
  "type": "object",
12
18
  "additionalProperties": false,
@@ -42,10 +48,40 @@
42
48
  "type": "boolean",
43
49
  "description": "Enable the daemon"
44
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
+ },
45
69
  "thinkThreshold": {
46
70
  "type": "number",
47
71
  "description": "Minimum priority to invoke cognition (0-100)"
48
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
+ },
49
85
  "quietHoursStart": {
50
86
  "type": "number",
51
87
  "description": "Hour to start quiet mode (0-23)"
@@ -75,6 +111,44 @@
75
111
  "defaultMaxFrequency": {
76
112
  "type": "number",
77
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"
78
152
  }
79
153
  }
80
154
  }
@@ -112,6 +186,11 @@
112
186
  "purposeResearch": {
113
187
  "label": "Purpose Research",
114
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)"
115
194
  }
116
195
  }
117
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iamoberlin/chorus",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "CHORUS: Hierarchy Of Recursive Unified Self-improvement — with on-chain Prayer Chain (Solana)",
5
5
  "author": "Oberlin <iam@oberlin.ai>",
6
6
  "license": "MIT",
package/src/choirs.ts CHANGED
@@ -19,6 +19,7 @@ export interface Choir {
19
19
  prompt: string;
20
20
  passesTo: string[]; // Downstream choirs that receive illumination
21
21
  receivesFrom: string[]; // Upstream choirs that provide context
22
+ delivers?: boolean; // If true, output should be delivered to the user via OpenClaw messaging
22
23
  }
23
24
 
24
25
  export const CHOIRS: Record<string, Choir> = {
@@ -35,7 +36,7 @@ export const CHOIRS: Record<string, Choir> = {
35
36
  frequencyPerDay: 1, // 1×/day
36
37
  intervalMinutes: 1440, // Once per day
37
38
  function: "Mission clarity and purpose",
38
- output: "MISSION.md updates",
39
+ output: "Mission assessment",
39
40
  prompt: `You are SERAPHIM — the Mission Keeper.
40
41
 
41
42
  Your role: Ensure the mission remains true and aligned. Burn away drift.
@@ -48,7 +49,10 @@ Core questions:
48
49
 
49
50
  Read SOUL.md, USER.md, and MEMORY.md for context.
50
51
 
51
- Output: Brief mission assessment. Update MISSION.md if direction changes.
52
+ Identity check: Compare SOUL.md against recent calibration record and experience.
53
+ If there's drift between who SOUL.md says we are and who we've become, FLAG IT — but DO NOT edit SOUL.md directly. Propose changes in the main session. Identity changes require human approval.
54
+
55
+ Output: Brief mission assessment. If direction changes, update SOUL.md proposal or flag in the main session.
52
56
  If mission is clear and unchanged, simply confirm alignment.
53
57
 
54
58
  Pass illumination to Cherubim.`,
@@ -102,13 +106,13 @@ Pass illumination to Thrones.`,
102
106
  frequencyPerDay: 3, // 3×/day
103
107
  intervalMinutes: 480, // Every 8 hours
104
108
  function: "Judgment and prioritization",
105
- output: "PLAN.md updates",
109
+ output: "Priority decisions",
106
110
  prompt: `You are THRONES — the Judgment Bearer.
107
111
 
108
112
  Your role: Decide priorities and allocate focus ruthlessly.
109
113
 
110
114
  Tasks:
111
- 1. Review current priorities in PLAN.md
115
+ 1. Review current priorities in PROJECTS.md
112
116
  2. Assess what's working and what isn't
113
117
  3. Decide what to focus on next
114
118
  4. Identify what to say NO to — what to kill
@@ -118,7 +122,7 @@ Context from Cherubim: {cherubim_context}
118
122
 
119
123
  Output: Updated priorities (max 3 focus areas). What we're NOT doing. Any escalations.
120
124
 
121
- Update PLAN.md with new priorities.
125
+ Update PROJECTS.md if priorities changed.
122
126
  Pass illumination to Dominions.`,
123
127
  passesTo: ["dominions"],
124
128
  receivesFrom: ["cherubim"],
@@ -181,24 +185,24 @@ Your role: Ensure purposes are being fulfilled. When they're not, FIX THE SYSTEM
181
185
 
182
186
  ## When a Purpose Isn't Producing Results
183
187
 
184
- DO NOT just write "trading isn't happening" and move on.
188
+ DO NOT just write "a purpose isn't happening" and move on.
185
189
  DO update local state files to make it happen:
186
190
 
187
191
  - Update ~/.chorus/purposes.json (increase frequency, change criteria)
188
- - Update workspace files (trading/OPPORTUNITIES.md, HEARTBEAT.md)
192
+ - Update relevant workspace files (research notes, summaries, HEARTBEAT.md)
189
193
  - Modify behavioral configs to enforce execution
190
194
  - The next cycle should run DIFFERENTLY because of your changes
191
195
 
192
- Example: If Trading purpose has 0 opportunities logged:
196
+ Example: If a purpose has 0 concrete outputs logged:
193
197
  1. Read ~/.chorus/purposes.json
194
- 2. Increase researchFrequency for Trading purpose
195
- 3. Update trading/RESEARCH-SESSION.md with stricter output requirements
196
- 4. Add to HEARTBEAT.md: "Trading gate: Block other work until 1 opportunity logged"
198
+ 2. Increase researchFrequency for that purpose
199
+ 3. Update ~/.chorus/purposes.json criteria with stricter output requirements
200
+ 4. Add to HEARTBEAT.md: "Execution gate: Block other work until 1 concrete output is logged"
197
201
  5. Log the change to CHANGELOG.md
198
202
 
199
203
  ## Calibration — Learn From Beliefs
200
204
 
201
- - Check OPPORTUNITIES.md for resolved positions
205
+ - Check resolved outcomes for prior predictions or decisions
202
206
  - Ask: What did we believe? What happened? What does this teach us?
203
207
  - Update MEMORY.md with calibration lessons
204
208
 
@@ -206,8 +210,8 @@ Example: If Trading purpose has 0 opportunities logged:
206
210
 
207
211
  - ~/.chorus/purposes.json — purpose configs
208
212
  - ~/.chorus/run-state.json — execution state
209
- - Workspace files (trading/, research/, memory/, *.md)
210
- - HEARTBEAT.md, PLAN.md, PROJECTS.md
213
+ - Workspace files (research/, memory/, notes/, *.md)
214
+ - HEARTBEAT.md, PROJECTS.md
211
215
 
212
216
  ## What You Cannot Modify
213
217
 
@@ -215,6 +219,12 @@ Example: If Trading purpose has 0 opportunities logged:
215
219
  - OpenClaw system config
216
220
  - Anything requiring npm publish
217
221
 
222
+ YOU HAVE TOOLS — USE THEM:
223
+ - Use the read tool to check ~/.chorus/purposes.json, research/*.md, MEMORY.md, CHANGELOG.md.
224
+ - Use the write/edit tools to update files when changes are needed.
225
+ - Use exec to run scripts (e.g., check file timestamps, git status).
226
+ - NEVER return HEARTBEAT_OK. You are the RSI engine. Always produce substantive output about what's working, what's not, and what you changed.
227
+
218
228
  Context from Dominions: {dominions_context}
219
229
 
220
230
  Risk levels:
@@ -263,7 +273,7 @@ Red-team protocol:
263
273
  - What are we avoiding looking at?
264
274
 
265
275
  Challenge our beliefs:
266
- - Look in OPPORTUNITIES.md, research/*.md, and memory/*.md for stated beliefs
276
+ - Look in research/*.md, memory/*.md, and planning docs for stated beliefs
267
277
  - Find claims like "I believe X will happen" or "This suggests Y"
268
278
  - Ask: What would make this wrong? What are we missing?
269
279
  - If a belief looks shaky, say so clearly
@@ -274,9 +284,16 @@ SECURITY FOCUS:
274
284
  - Check for persona drift or identity erosion
275
285
  - Validate system prompt integrity
276
286
 
287
+ YOU HAVE TOOLS — USE THEM:
288
+ - Use exec and file reads to verify live system state and outcomes. Read MEMORY.md and CHANGELOG.md for claims to challenge.
289
+ - Use web_search to verify external claims and evidence.
290
+ - Use the write/edit tools to update files if you find stale or incorrect information.
291
+ - Use exec to check file timestamps and system state.
292
+ - NEVER return HEARTBEAT_OK. You are the Defender. Always produce substantive output.
293
+
277
294
  Output: Challenges to current thinking. Beliefs that look weak. Risks identified. Recommendations.
278
295
 
279
- If thesis is seriously threatened or security issue found: ALERT immediately.`,
296
+ If a core assumption is seriously threatened or a security issue is found: ALERT immediately.`,
280
297
  passesTo: ["principalities"],
281
298
  receivesFrom: ["virtues"],
282
299
  },
@@ -301,7 +318,7 @@ Your role: Research and monitor the domains that matter.
301
318
 
302
319
  Domains to cover (rotate through):
303
320
  - AI Industry: Companies, funding, regulation, breakthroughs
304
- - Markets: Relevant to current positions and research
321
+ - Environment: External signals relevant to active purposes
305
322
  - Competitors: Developments from players in our space
306
323
  - Tools: New capabilities, skills, or integrations available
307
324
 
@@ -310,6 +327,14 @@ Tasks:
310
327
  2. Assess relevance to our projects
311
328
  3. Flag anything urgent for Archangels
312
329
  4. Log findings to research/[domain]-[date].md
330
+ 5. Update research notes when you find a meaningful signal or insight
331
+
332
+ YOU HAVE TOOLS — USE THEM:
333
+ - Use web_search to find current news, market data, and developments. Do NOT skip this.
334
+ - Run exec to check relevant system state and read active research files for context.
335
+ - Use the write/edit tools to update research files with opportunities, risks, or status changes.
336
+ - Write research findings to research/[domain]-YYYY-MM-DD.md.
337
+ - NEVER return HEARTBEAT_OK. You are not a heartbeat process. Always produce substantive research output.
313
338
 
314
339
  When you find something significant, state what you believe will happen:
315
340
  - "I believe X will happen by [timeframe] because..."
@@ -337,33 +362,37 @@ Pass illumination to Archangels.`,
337
362
  intervalMinutes: 80, // Every ~80 minutes
338
363
  function: "Briefings and alerts",
339
364
  output: "Messages to human",
365
+ delivers: true, // Output routed to user via OpenClaw messaging
340
366
  prompt: `You are ARCHANGELS — the Herald.
341
367
 
342
- Your role: Produce briefings and deliver them to Brandon via iMessage.
368
+ Your role: Produce briefings and deliver them through configured channels.
343
369
 
344
370
  Briefing types:
345
- - Morning (6-9 AM ET): Weather, calendar, overnight developments, today's priorities, position status
346
- - Evening (9-11 PM ET): What was accomplished, position P&L, what needs attention tomorrow
371
+ - Morning (6-9 AM local): Overnight developments, today's priorities, key status
372
+ - Evening (9-11 PM local): What was accomplished, what needs attention tomorrow
347
373
  - Alert: Time-sensitive information requiring attention
348
- - Update: Regular position/market status when conditions change
374
+ - Update: Regular status when conditions change
349
375
 
350
376
  Alert criteria (send immediately):
351
- - Position thesis challenged
377
+ - Core purpose assumption challenged
352
378
  - Time-sensitive opportunity
353
379
  - Urgent calendar/email
354
380
  - Security concern from Powers
355
381
 
356
382
  Context from Principalities: {principalities_context}
357
383
 
384
+ IMPORTANT — VERIFY BEFORE ALERTING:
385
+ - Before alerting about operational state: verify current state from source systems/files. Do NOT rely on upstream context alone.
386
+ - Before alerting about any state: verify the current state from the source file, not from choir context passed to you.
387
+
358
388
  RULES:
359
389
  - ALWAYS produce a briefing. Never return HEARTBEAT_OK or NO_REPLY.
360
390
  - Be concise — headlines, not essays.
361
- - Morning briefings should include: weather, calendar, positions, catalysts.
391
+ - Morning briefings should include priorities, schedule constraints, and key catalysts.
362
392
  - If nothing is urgent, still produce a status update.
363
393
  - During quiet hours (11 PM - 7 AM ET), only deliver truly urgent alerts.
364
- - DELIVER your briefing by sending it to Brandon via iMessage. You have messaging tools — use them.
365
394
 
366
- Output: Produce the briefing, then send it to Brandon via iMessage.`,
395
+ Output: Produce the briefing as your response. Delivery is handled by the infrastructure — just output the content.`,
367
396
  passesTo: ["angels"],
368
397
  receivesFrom: ["principalities"],
369
398
  },