@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 +6 -0
- package/index.ts +82 -79
- package/openclaw.plugin.json +74 -1
- package/package.json +2 -2
- package/src/behavioral-sink.ts +434 -0
- package/src/choirs.ts +51 -23
- package/src/config.ts +91 -0
- package/src/daemon.ts +18 -13
- package/src/delivery.ts +121 -0
- package/src/economics.ts +166 -0
- package/src/purpose-research.ts +137 -42
- package/src/scheduler.ts +113 -72
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CHORUS Behavioral Sink Prevention
|
|
3
|
+
*
|
|
4
|
+
* Inspired by John B. Calhoun's Universe 25 (1968-1972):
|
|
5
|
+
* A mouse utopia with unlimited resources collapsed not from scarcity,
|
|
6
|
+
* but from loss of purpose and structural role degradation.
|
|
7
|
+
*
|
|
8
|
+
* Three failure modes mapped to CHORUS:
|
|
9
|
+
*
|
|
10
|
+
* 1. BEAUTIFUL ONES — Mice that looked perfect but did nothing.
|
|
11
|
+
* → Choirs producing verbose but vacuous output. Busy ≠ productive.
|
|
12
|
+
*
|
|
13
|
+
* 2. BEHAVIORAL SINK — Pathological behaviors spreading contagiously.
|
|
14
|
+
* → Degraded illumination cascading through the choir hierarchy.
|
|
15
|
+
*
|
|
16
|
+
* 3. TERRITORIAL EXCLUSION — Dominant mice monopolizing resources,
|
|
17
|
+
* forcing others into withdrawal.
|
|
18
|
+
* → Purpose starvation: dominant purposes crowding out others.
|
|
19
|
+
*
|
|
20
|
+
* "Purpose prevents extinction."
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
|
|
24
|
+
import { join } from "path";
|
|
25
|
+
import { homedir } from "os";
|
|
26
|
+
|
|
27
|
+
const SINK_STATE_DIR = join(homedir(), ".chorus", "behavioral-sink");
|
|
28
|
+
const SINK_STATE_PATH = join(SINK_STATE_DIR, "state.json");
|
|
29
|
+
|
|
30
|
+
// ── Beautiful Ones Detection ─────────────────────────────────
|
|
31
|
+
// Detect when choir output is present but substanceless.
|
|
32
|
+
|
|
33
|
+
export interface QualitySignal {
|
|
34
|
+
score: number; // 0-100: 0 = vacuous, 100 = highly substantive
|
|
35
|
+
actionsTaken: number; // File writes, commands run, state changes
|
|
36
|
+
novelty: number; // 0-100: how different from last N outputs
|
|
37
|
+
flags: string[]; // Human-readable quality concerns
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Analyze choir output quality. Returns a signal indicating whether
|
|
42
|
+
* the output represents real work or beautiful-one behavior.
|
|
43
|
+
*/
|
|
44
|
+
export function assessOutputQuality(
|
|
45
|
+
choirId: string,
|
|
46
|
+
output: string,
|
|
47
|
+
previousOutputs: string[] = []
|
|
48
|
+
): QualitySignal {
|
|
49
|
+
const flags: string[] = [];
|
|
50
|
+
let score = 50; // Start neutral
|
|
51
|
+
|
|
52
|
+
// ── Length check (too short = no work, too long = possibly padding)
|
|
53
|
+
if (output.length < 100) {
|
|
54
|
+
score -= 20;
|
|
55
|
+
flags.push("minimal_output");
|
|
56
|
+
} else if (output.length > 50) {
|
|
57
|
+
score += 10;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── Action density: Did it actually DO things?
|
|
61
|
+
const actionPatterns = [
|
|
62
|
+
/(?:updated|wrote|created|modified|edited|deleted|committed|pushed|deployed)\s/gi,
|
|
63
|
+
/(?:wrote to|saved|appended)\s+[^\s]+/gi,
|
|
64
|
+
/(?:ran|executed|running)\s+/gi,
|
|
65
|
+
/(?:changed|set|configured|enabled|disabled)\s/gi,
|
|
66
|
+
/(?:fixed|resolved|patched|upgraded)\s/gi,
|
|
67
|
+
];
|
|
68
|
+
let actionsTaken = 0;
|
|
69
|
+
for (const pattern of actionPatterns) {
|
|
70
|
+
const matches = output.match(pattern);
|
|
71
|
+
if (matches) actionsTaken += matches.length;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (actionsTaken === 0 && !["angels", "seraphim"].includes(choirId)) {
|
|
75
|
+
score -= 15;
|
|
76
|
+
flags.push("no_actions_detected");
|
|
77
|
+
} else if (actionsTaken >= 3) {
|
|
78
|
+
score += 20;
|
|
79
|
+
} else if (actionsTaken >= 1) {
|
|
80
|
+
score += 10;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Filler detection: Generic phrases that pad without substance
|
|
84
|
+
const fillerPatterns = [
|
|
85
|
+
/everything (?:looks|seems|appears) (?:good|fine|stable|normal|healthy)/gi,
|
|
86
|
+
/no (?:significant |major |notable )?(?:changes|updates|issues|concerns|findings)/gi,
|
|
87
|
+
/(?:continuing|continue) to monitor/gi,
|
|
88
|
+
/(?:all|everything) (?:is )?(?:on track|aligned|in order)/gi,
|
|
89
|
+
/nothing (?:urgent|notable|significant) to report/gi,
|
|
90
|
+
/mission (?:remains |is )?(?:clear|aligned|unchanged)/gi,
|
|
91
|
+
/no (?:drift|deviation) detected/gi,
|
|
92
|
+
];
|
|
93
|
+
let fillerCount = 0;
|
|
94
|
+
for (const pattern of fillerPatterns) {
|
|
95
|
+
const matches = output.match(pattern);
|
|
96
|
+
if (matches) fillerCount += matches.length;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (fillerCount >= 3) {
|
|
100
|
+
score -= 25;
|
|
101
|
+
flags.push("high_filler_ratio");
|
|
102
|
+
} else if (fillerCount >= 1) {
|
|
103
|
+
score -= 5;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Specificity: Does it contain concrete data (numbers, names, dates)?
|
|
107
|
+
const specificityPatterns = [
|
|
108
|
+
/\$[\d,]+(?:\.\d+)?/g, // Dollar amounts
|
|
109
|
+
/\d+(?:\.\d+)?%/g, // Percentages
|
|
110
|
+
/\d{4}-\d{2}-\d{2}/g, // Dates
|
|
111
|
+
/(?:PI|VIX|DXY|BTC|ETH)\s*(?:at|=|:)\s*[\d.,$]+/gi, // Market refs
|
|
112
|
+
];
|
|
113
|
+
let specificityCount = 0;
|
|
114
|
+
for (const pattern of specificityPatterns) {
|
|
115
|
+
const matches = output.match(pattern);
|
|
116
|
+
if (matches) specificityCount += matches.length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (specificityCount >= 3) {
|
|
120
|
+
score += 15;
|
|
121
|
+
} else if (specificityCount === 0 && !["angels"].includes(choirId)) {
|
|
122
|
+
score -= 10;
|
|
123
|
+
flags.push("low_specificity");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Novelty: Is this substantially different from recent outputs?
|
|
127
|
+
let novelty = 100;
|
|
128
|
+
if (previousOutputs.length > 0) {
|
|
129
|
+
const similarities = previousOutputs.map(prev => computeSimilarity(output, prev));
|
|
130
|
+
const maxSimilarity = Math.max(...similarities);
|
|
131
|
+
|
|
132
|
+
if (maxSimilarity > 0.85) {
|
|
133
|
+
novelty = 10;
|
|
134
|
+
score -= 20;
|
|
135
|
+
flags.push("near_duplicate_output");
|
|
136
|
+
} else if (maxSimilarity > 0.65) {
|
|
137
|
+
novelty = 40;
|
|
138
|
+
score -= 10;
|
|
139
|
+
flags.push("repetitive_output");
|
|
140
|
+
} else {
|
|
141
|
+
novelty = Math.round((1 - maxSimilarity) * 100);
|
|
142
|
+
score += 5;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── HEARTBEAT_OK is fine for Angels, vacuous for everything else
|
|
147
|
+
const isHeartbeat = output.trim().toUpperCase().includes("HEARTBEAT_OK");
|
|
148
|
+
if (isHeartbeat) {
|
|
149
|
+
if (choirId === "angels") {
|
|
150
|
+
return { score: 50, actionsTaken: 0, novelty: 0, flags: ["heartbeat_ack"] };
|
|
151
|
+
} else {
|
|
152
|
+
score = 5;
|
|
153
|
+
flags.push("beautiful_one_heartbeat");
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Clamp score
|
|
158
|
+
score = Math.max(0, Math.min(100, score));
|
|
159
|
+
|
|
160
|
+
return { score, actionsTaken, novelty, flags };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Simple bag-of-words Jaccard similarity.
|
|
165
|
+
* Fast enough for choir output comparison without external deps.
|
|
166
|
+
*/
|
|
167
|
+
function computeSimilarity(a: string, b: string): number {
|
|
168
|
+
const wordsA = new Set(a.toLowerCase().split(/\s+/).filter(w => w.length > 3));
|
|
169
|
+
const wordsB = new Set(b.toLowerCase().split(/\s+/).filter(w => w.length > 3));
|
|
170
|
+
|
|
171
|
+
if (wordsA.size === 0 && wordsB.size === 0) return 1;
|
|
172
|
+
if (wordsA.size === 0 || wordsB.size === 0) return 0;
|
|
173
|
+
|
|
174
|
+
let intersection = 0;
|
|
175
|
+
for (const w of wordsA) {
|
|
176
|
+
if (wordsB.has(w)) intersection++;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const union = wordsA.size + wordsB.size - intersection;
|
|
180
|
+
return union > 0 ? intersection / union : 0;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── Behavioral Sink (Illumination Quality Gate) ──────────────
|
|
184
|
+
// Prevent degraded context from cascading downstream.
|
|
185
|
+
|
|
186
|
+
export interface IlluminationAssessment {
|
|
187
|
+
sourceChoir: string;
|
|
188
|
+
quality: QualitySignal;
|
|
189
|
+
usable: boolean; // Should downstream choirs use this context?
|
|
190
|
+
degraded: boolean; // Is this a degradation from normal quality?
|
|
191
|
+
recommendation: string; // What to do
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Assess whether illumination from an upstream choir should be
|
|
196
|
+
* passed downstream, or whether it would propagate a behavioral sink.
|
|
197
|
+
*/
|
|
198
|
+
export function assessIllumination(
|
|
199
|
+
sourceChoir: string,
|
|
200
|
+
output: string,
|
|
201
|
+
historicalQuality: number[] = []
|
|
202
|
+
): IlluminationAssessment {
|
|
203
|
+
const quality = assessOutputQuality(sourceChoir, output);
|
|
204
|
+
|
|
205
|
+
// Is this a degradation from the choir's normal quality?
|
|
206
|
+
let degraded = false;
|
|
207
|
+
if (historicalQuality.length >= 3) {
|
|
208
|
+
const avgHistorical = historicalQuality.reduce((a, b) => a + b, 0) / historicalQuality.length;
|
|
209
|
+
degraded = quality.score < avgHistorical - 20;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Below 25 = do not pass downstream (sink prevention)
|
|
213
|
+
const usable = quality.score >= 25;
|
|
214
|
+
|
|
215
|
+
let recommendation: string;
|
|
216
|
+
if (!usable) {
|
|
217
|
+
recommendation = `SINK PREVENTION: ${sourceChoir} output below quality threshold (${quality.score}/100). Context NOT passed to downstream choirs. Flags: ${quality.flags.join(", ")}`;
|
|
218
|
+
} else if (degraded) {
|
|
219
|
+
recommendation = `DEGRADATION WARNING: ${sourceChoir} quality dropped from avg ${Math.round(historicalQuality.reduce((a, b) => a + b, 0) / historicalQuality.length)} to ${quality.score}. Context passed with warning.`;
|
|
220
|
+
} else {
|
|
221
|
+
recommendation = "OK";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { sourceChoir, quality, usable, degraded, recommendation };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ── Territorial Exclusion Prevention (Purpose Fairness) ──────
|
|
228
|
+
// Ensure no purpose monopolizes execution at the expense of others.
|
|
229
|
+
|
|
230
|
+
export interface FairnessScore {
|
|
231
|
+
purposeId: string;
|
|
232
|
+
expectedInterval: number; // ms between runs (from frequency config)
|
|
233
|
+
actualInterval: number; // ms since last run
|
|
234
|
+
starvationRatio: number; // actual / expected — >2.0 = starving
|
|
235
|
+
starving: boolean;
|
|
236
|
+
priorityBoost: number; // Multiplier to apply to scheduling priority
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Calculate starvation-aware priority for purpose scheduling.
|
|
241
|
+
* Replaces pure deadline-based sorting with fairness weighting.
|
|
242
|
+
*
|
|
243
|
+
* Universe 25 lesson: Dominant mice monopolized prime territory.
|
|
244
|
+
* Fix: overdue purposes get exponentially increasing priority.
|
|
245
|
+
*/
|
|
246
|
+
export function calculateFairness(
|
|
247
|
+
purposeId: string,
|
|
248
|
+
lastRun: number | undefined,
|
|
249
|
+
frequencyPerDay: number,
|
|
250
|
+
deadline?: number
|
|
251
|
+
): FairnessScore {
|
|
252
|
+
const expectedInterval = (24 * 60 * 60 * 1000) / Math.max(frequencyPerDay, 1);
|
|
253
|
+
const actualInterval = lastRun ? Date.now() - lastRun : expectedInterval * 10; // Never run = very overdue
|
|
254
|
+
|
|
255
|
+
const starvationRatio = actualInterval / expectedInterval;
|
|
256
|
+
const starving = starvationRatio >= 2.0;
|
|
257
|
+
|
|
258
|
+
// Priority boost: exponential with starvation ratio
|
|
259
|
+
// At 1x overdue: boost = 1 (normal)
|
|
260
|
+
// At 2x overdue: boost = 2
|
|
261
|
+
// At 4x overdue: boost = 4
|
|
262
|
+
// At 8x+ overdue: boost = 8 (capped)
|
|
263
|
+
let priorityBoost = 1;
|
|
264
|
+
if (starvationRatio > 1) {
|
|
265
|
+
priorityBoost = Math.min(starvationRatio, 8);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Deadline urgency still matters but doesn't dominate
|
|
269
|
+
if (deadline) {
|
|
270
|
+
const daysToDeadline = (deadline - Date.now()) / (24 * 60 * 60 * 1000);
|
|
271
|
+
if (daysToDeadline <= 0) {
|
|
272
|
+
priorityBoost *= 2; // Overdue deadline doubles existing boost
|
|
273
|
+
} else if (daysToDeadline <= 3) {
|
|
274
|
+
priorityBoost *= 1.5;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
purposeId,
|
|
280
|
+
expectedInterval,
|
|
281
|
+
actualInterval,
|
|
282
|
+
starvationRatio,
|
|
283
|
+
starving,
|
|
284
|
+
priorityBoost,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Sort purposes by fairness-weighted priority.
|
|
290
|
+
* Replaces the naive deadline sort that caused starvation.
|
|
291
|
+
*/
|
|
292
|
+
export function sortByFairness(purposes: Array<{
|
|
293
|
+
id: string;
|
|
294
|
+
lastRun?: number;
|
|
295
|
+
frequency: number;
|
|
296
|
+
deadline?: number;
|
|
297
|
+
}>): Array<{ id: string; fairness: FairnessScore }> {
|
|
298
|
+
const scored = purposes.map(p => ({
|
|
299
|
+
id: p.id,
|
|
300
|
+
fairness: calculateFairness(p.id, p.lastRun, p.frequency, p.deadline),
|
|
301
|
+
}));
|
|
302
|
+
|
|
303
|
+
// Sort by priorityBoost descending (most starved first)
|
|
304
|
+
scored.sort((a, b) => b.fairness.priorityBoost - a.fairness.priorityBoost);
|
|
305
|
+
return scored;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── State Persistence ────────────────────────────────────────
|
|
309
|
+
// Track quality history for degradation detection.
|
|
310
|
+
|
|
311
|
+
interface SinkState {
|
|
312
|
+
qualityHistory: Record<string, number[]>; // choirId → last N quality scores
|
|
313
|
+
recentOutputHashes: Record<string, string[]>; // choirId → last N output snippets for novelty
|
|
314
|
+
starvationAlerts: Record<string, number>; // purposeId → last alert timestamp
|
|
315
|
+
beautifulOnesDetected: Array<{
|
|
316
|
+
choirId: string;
|
|
317
|
+
timestamp: string;
|
|
318
|
+
score: number;
|
|
319
|
+
flags: string[];
|
|
320
|
+
}>;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const MAX_HISTORY = 10;
|
|
324
|
+
|
|
325
|
+
function loadSinkState(): SinkState {
|
|
326
|
+
try {
|
|
327
|
+
if (existsSync(SINK_STATE_PATH)) {
|
|
328
|
+
return JSON.parse(readFileSync(SINK_STATE_PATH, "utf-8"));
|
|
329
|
+
}
|
|
330
|
+
} catch {}
|
|
331
|
+
return {
|
|
332
|
+
qualityHistory: {},
|
|
333
|
+
recentOutputHashes: {},
|
|
334
|
+
starvationAlerts: {},
|
|
335
|
+
beautifulOnesDetected: [],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function saveSinkState(state: SinkState): void {
|
|
340
|
+
try {
|
|
341
|
+
if (!existsSync(SINK_STATE_DIR)) {
|
|
342
|
+
mkdirSync(SINK_STATE_DIR, { recursive: true });
|
|
343
|
+
}
|
|
344
|
+
writeFileSync(SINK_STATE_PATH, JSON.stringify(state, null, 2));
|
|
345
|
+
} catch {}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Record a choir execution's quality and return the assessment.
|
|
350
|
+
* Call this after every choir run.
|
|
351
|
+
*/
|
|
352
|
+
export function recordAndAssess(
|
|
353
|
+
choirId: string,
|
|
354
|
+
output: string
|
|
355
|
+
): { quality: QualitySignal; illumination: IlluminationAssessment } {
|
|
356
|
+
const state = loadSinkState();
|
|
357
|
+
|
|
358
|
+
// Get previous outputs for novelty detection
|
|
359
|
+
const previousOutputs = state.recentOutputHashes[choirId] || [];
|
|
360
|
+
const historicalQuality = state.qualityHistory[choirId] || [];
|
|
361
|
+
|
|
362
|
+
// Assess quality
|
|
363
|
+
const quality = assessOutputQuality(choirId, output, previousOutputs);
|
|
364
|
+
const illumination = assessIllumination(choirId, output, historicalQuality);
|
|
365
|
+
|
|
366
|
+
// Update history
|
|
367
|
+
if (!state.qualityHistory[choirId]) state.qualityHistory[choirId] = [];
|
|
368
|
+
state.qualityHistory[choirId].push(quality.score);
|
|
369
|
+
if (state.qualityHistory[choirId].length > MAX_HISTORY) {
|
|
370
|
+
state.qualityHistory[choirId] = state.qualityHistory[choirId].slice(-MAX_HISTORY);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Store output snippet for novelty comparison (first 500 chars)
|
|
374
|
+
if (!state.recentOutputHashes[choirId]) state.recentOutputHashes[choirId] = [];
|
|
375
|
+
state.recentOutputHashes[choirId].push(output.slice(0, 500));
|
|
376
|
+
if (state.recentOutputHashes[choirId].length > MAX_HISTORY) {
|
|
377
|
+
state.recentOutputHashes[choirId] = state.recentOutputHashes[choirId].slice(-MAX_HISTORY);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Track beautiful ones incidents
|
|
381
|
+
if (quality.score < 20 && choirId !== "angels") {
|
|
382
|
+
state.beautifulOnesDetected.push({
|
|
383
|
+
choirId,
|
|
384
|
+
timestamp: new Date().toISOString(),
|
|
385
|
+
score: quality.score,
|
|
386
|
+
flags: quality.flags,
|
|
387
|
+
});
|
|
388
|
+
// Keep last 50 incidents
|
|
389
|
+
if (state.beautifulOnesDetected.length > 50) {
|
|
390
|
+
state.beautifulOnesDetected = state.beautifulOnesDetected.slice(-50);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
saveSinkState(state);
|
|
395
|
+
|
|
396
|
+
return { quality, illumination };
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Get a summary of behavioral sink health across all choirs.
|
|
401
|
+
*/
|
|
402
|
+
export function getSinkHealthSummary(): {
|
|
403
|
+
healthy: boolean;
|
|
404
|
+
choirHealth: Record<string, { avgQuality: number; trend: string; beautifulOneCount: number }>;
|
|
405
|
+
starvingPurposes: string[];
|
|
406
|
+
recentIncidents: Array<{ choirId: string; timestamp: string; score: number }>;
|
|
407
|
+
} {
|
|
408
|
+
const state = loadSinkState();
|
|
409
|
+
const choirHealth: Record<string, { avgQuality: number; trend: string; beautifulOneCount: number }> = {};
|
|
410
|
+
|
|
411
|
+
let healthy = true;
|
|
412
|
+
|
|
413
|
+
for (const [choirId, scores] of Object.entries(state.qualityHistory)) {
|
|
414
|
+
const avg = scores.reduce((a, b) => a + b, 0) / scores.length;
|
|
415
|
+
const recentAvg = scores.slice(-3).reduce((a, b) => a + b, 0) / Math.min(scores.length, 3);
|
|
416
|
+
const trend = recentAvg > avg + 10 ? "improving" : recentAvg < avg - 10 ? "declining" : "stable";
|
|
417
|
+
|
|
418
|
+
const beautifulOneCount = state.beautifulOnesDetected
|
|
419
|
+
.filter(b => b.choirId === choirId)
|
|
420
|
+
.length;
|
|
421
|
+
|
|
422
|
+
choirHealth[choirId] = { avgQuality: Math.round(avg), trend, beautifulOneCount };
|
|
423
|
+
|
|
424
|
+
if (avg < 30 || trend === "declining") healthy = false;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const starvingPurposes = Object.entries(state.starvationAlerts)
|
|
428
|
+
.filter(([_, ts]) => Date.now() - ts < 24 * 60 * 60 * 1000)
|
|
429
|
+
.map(([id]) => id);
|
|
430
|
+
|
|
431
|
+
const recentIncidents = state.beautifulOnesDetected.slice(-5);
|
|
432
|
+
|
|
433
|
+
return { healthy, choirHealth, starvingPurposes, recentIncidents };
|
|
434
|
+
}
|
package/src/choirs.ts
CHANGED
|
@@ -36,7 +36,7 @@ export const CHOIRS: Record<string, Choir> = {
|
|
|
36
36
|
frequencyPerDay: 1, // 1×/day
|
|
37
37
|
intervalMinutes: 1440, // Once per day
|
|
38
38
|
function: "Mission clarity and purpose",
|
|
39
|
-
output: "
|
|
39
|
+
output: "Mission assessment",
|
|
40
40
|
prompt: `You are SERAPHIM — the Mission Keeper.
|
|
41
41
|
|
|
42
42
|
Your role: Ensure the mission remains true and aligned. Burn away drift.
|
|
@@ -49,7 +49,10 @@ Core questions:
|
|
|
49
49
|
|
|
50
50
|
Read SOUL.md, USER.md, and MEMORY.md for context.
|
|
51
51
|
|
|
52
|
-
|
|
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.
|
|
53
56
|
If mission is clear and unchanged, simply confirm alignment.
|
|
54
57
|
|
|
55
58
|
Pass illumination to Cherubim.`,
|
|
@@ -103,13 +106,13 @@ Pass illumination to Thrones.`,
|
|
|
103
106
|
frequencyPerDay: 3, // 3×/day
|
|
104
107
|
intervalMinutes: 480, // Every 8 hours
|
|
105
108
|
function: "Judgment and prioritization",
|
|
106
|
-
output: "
|
|
109
|
+
output: "Priority decisions",
|
|
107
110
|
prompt: `You are THRONES — the Judgment Bearer.
|
|
108
111
|
|
|
109
112
|
Your role: Decide priorities and allocate focus ruthlessly.
|
|
110
113
|
|
|
111
114
|
Tasks:
|
|
112
|
-
1. Review current priorities in
|
|
115
|
+
1. Review current priorities in PROJECTS.md
|
|
113
116
|
2. Assess what's working and what isn't
|
|
114
117
|
3. Decide what to focus on next
|
|
115
118
|
4. Identify what to say NO to — what to kill
|
|
@@ -119,7 +122,7 @@ Context from Cherubim: {cherubim_context}
|
|
|
119
122
|
|
|
120
123
|
Output: Updated priorities (max 3 focus areas). What we're NOT doing. Any escalations.
|
|
121
124
|
|
|
122
|
-
Update
|
|
125
|
+
Update PROJECTS.md if priorities changed.
|
|
123
126
|
Pass illumination to Dominions.`,
|
|
124
127
|
passesTo: ["dominions"],
|
|
125
128
|
receivesFrom: ["cherubim"],
|
|
@@ -182,24 +185,24 @@ Your role: Ensure purposes are being fulfilled. When they're not, FIX THE SYSTEM
|
|
|
182
185
|
|
|
183
186
|
## When a Purpose Isn't Producing Results
|
|
184
187
|
|
|
185
|
-
DO NOT just write "
|
|
188
|
+
DO NOT just write "a purpose isn't happening" and move on.
|
|
186
189
|
DO update local state files to make it happen:
|
|
187
190
|
|
|
188
191
|
- Update ~/.chorus/purposes.json (increase frequency, change criteria)
|
|
189
|
-
- Update workspace files (
|
|
192
|
+
- Update relevant workspace files (research notes, summaries, HEARTBEAT.md)
|
|
190
193
|
- Modify behavioral configs to enforce execution
|
|
191
194
|
- The next cycle should run DIFFERENTLY because of your changes
|
|
192
195
|
|
|
193
|
-
Example: If
|
|
196
|
+
Example: If a purpose has 0 concrete outputs logged:
|
|
194
197
|
1. Read ~/.chorus/purposes.json
|
|
195
|
-
2. Increase researchFrequency for
|
|
196
|
-
3. Update
|
|
197
|
-
4. Add to HEARTBEAT.md: "
|
|
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"
|
|
198
201
|
5. Log the change to CHANGELOG.md
|
|
199
202
|
|
|
200
203
|
## Calibration — Learn From Beliefs
|
|
201
204
|
|
|
202
|
-
- Check
|
|
205
|
+
- Check resolved outcomes for prior predictions or decisions
|
|
203
206
|
- Ask: What did we believe? What happened? What does this teach us?
|
|
204
207
|
- Update MEMORY.md with calibration lessons
|
|
205
208
|
|
|
@@ -207,8 +210,8 @@ Example: If Trading purpose has 0 opportunities logged:
|
|
|
207
210
|
|
|
208
211
|
- ~/.chorus/purposes.json — purpose configs
|
|
209
212
|
- ~/.chorus/run-state.json — execution state
|
|
210
|
-
- Workspace files (
|
|
211
|
-
- HEARTBEAT.md,
|
|
213
|
+
- Workspace files (research/, memory/, notes/, *.md)
|
|
214
|
+
- HEARTBEAT.md, PROJECTS.md
|
|
212
215
|
|
|
213
216
|
## What You Cannot Modify
|
|
214
217
|
|
|
@@ -216,6 +219,12 @@ Example: If Trading purpose has 0 opportunities logged:
|
|
|
216
219
|
- OpenClaw system config
|
|
217
220
|
- Anything requiring npm publish
|
|
218
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
|
+
|
|
219
228
|
Context from Dominions: {dominions_context}
|
|
220
229
|
|
|
221
230
|
Risk levels:
|
|
@@ -264,7 +273,7 @@ Red-team protocol:
|
|
|
264
273
|
- What are we avoiding looking at?
|
|
265
274
|
|
|
266
275
|
Challenge our beliefs:
|
|
267
|
-
- Look in
|
|
276
|
+
- Look in research/*.md, memory/*.md, and planning docs for stated beliefs
|
|
268
277
|
- Find claims like "I believe X will happen" or "This suggests Y"
|
|
269
278
|
- Ask: What would make this wrong? What are we missing?
|
|
270
279
|
- If a belief looks shaky, say so clearly
|
|
@@ -275,9 +284,16 @@ SECURITY FOCUS:
|
|
|
275
284
|
- Check for persona drift or identity erosion
|
|
276
285
|
- Validate system prompt integrity
|
|
277
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
|
+
|
|
278
294
|
Output: Challenges to current thinking. Beliefs that look weak. Risks identified. Recommendations.
|
|
279
295
|
|
|
280
|
-
If
|
|
296
|
+
If a core assumption is seriously threatened or a security issue is found: ALERT immediately.`,
|
|
281
297
|
passesTo: ["principalities"],
|
|
282
298
|
receivesFrom: ["virtues"],
|
|
283
299
|
},
|
|
@@ -302,7 +318,7 @@ Your role: Research and monitor the domains that matter.
|
|
|
302
318
|
|
|
303
319
|
Domains to cover (rotate through):
|
|
304
320
|
- AI Industry: Companies, funding, regulation, breakthroughs
|
|
305
|
-
-
|
|
321
|
+
- Environment: External signals relevant to active purposes
|
|
306
322
|
- Competitors: Developments from players in our space
|
|
307
323
|
- Tools: New capabilities, skills, or integrations available
|
|
308
324
|
|
|
@@ -311,6 +327,14 @@ Tasks:
|
|
|
311
327
|
2. Assess relevance to our projects
|
|
312
328
|
3. Flag anything urgent for Archangels
|
|
313
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.
|
|
314
338
|
|
|
315
339
|
When you find something significant, state what you believe will happen:
|
|
316
340
|
- "I believe X will happen by [timeframe] because..."
|
|
@@ -341,26 +365,30 @@ Pass illumination to Archangels.`,
|
|
|
341
365
|
delivers: true, // Output routed to user via OpenClaw messaging
|
|
342
366
|
prompt: `You are ARCHANGELS — the Herald.
|
|
343
367
|
|
|
344
|
-
Your role: Produce briefings and deliver them
|
|
368
|
+
Your role: Produce briefings and deliver them through configured channels.
|
|
345
369
|
|
|
346
370
|
Briefing types:
|
|
347
|
-
- Morning (6-9 AM
|
|
348
|
-
- Evening (9-11 PM
|
|
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
|
|
349
373
|
- Alert: Time-sensitive information requiring attention
|
|
350
|
-
- Update: Regular
|
|
374
|
+
- Update: Regular status when conditions change
|
|
351
375
|
|
|
352
376
|
Alert criteria (send immediately):
|
|
353
|
-
-
|
|
377
|
+
- Core purpose assumption challenged
|
|
354
378
|
- Time-sensitive opportunity
|
|
355
379
|
- Urgent calendar/email
|
|
356
380
|
- Security concern from Powers
|
|
357
381
|
|
|
358
382
|
Context from Principalities: {principalities_context}
|
|
359
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
|
+
|
|
360
388
|
RULES:
|
|
361
389
|
- ALWAYS produce a briefing. Never return HEARTBEAT_OK or NO_REPLY.
|
|
362
390
|
- Be concise — headlines, not essays.
|
|
363
|
-
- Morning briefings should include
|
|
391
|
+
- Morning briefings should include priorities, schedule constraints, and key catalysts.
|
|
364
392
|
- If nothing is urgent, still produce a status update.
|
|
365
393
|
- During quiet hours (11 PM - 7 AM ET), only deliver truly urgent alerts.
|
|
366
394
|
|