ofiere-openclaw-plugin 4.26.0 → 4.27.1

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/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.26.0",
3
+ "version": "4.27.1",
4
4
  "type": "module",
5
- "description": "OpenClaw plugin for Ofiere PM - 13 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, execution plan builder, and SOP management",
5
+ "description": "OpenClaw plugin for Ofiere PM - 14 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, execution plan builder, SOP management, and agent brain (memory + self-improvement)",
6
6
  "keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
7
7
  "homepage": "https://github.com/gilanggemar/Ofiere",
8
8
  "repository": {
package/src/prompt.ts CHANGED
@@ -150,6 +150,19 @@ const TOOL_DOCS: Record<string, string> = {
150
150
  - Status values: draft, active, archived
151
151
  - SOPs appear in the SOP Manager page immediately via real-time sync
152
152
  - ADAPTIVE PROTOCOL: Do NOT always load SOPs. See the SOP PROTOCOL section in Rules for when to load vs skip`,
153
+
154
+ OFIERE_BRAIN_OPS: `- **OFIERE_BRAIN_OPS** — Agent memory and self-improvement (action: "save_memory", "recall", "delete_memory", "log_learning", "list_learnings", "promote_learning", "resolve_learning", "get_brain_status", "configure_brain")
155
+ - Memory Tiers: L1_focus (working memory, 24h TTL), L2_journal (medium-term events), L3_core (long-term wisdom)
156
+ - save_memory: Store a memory. Required: content, tier. Optional: agent_id, source, context_key, importance (1-10)
157
+ - recall: Search memories by keyword. Required: query. Optional: agent_id, tier, limit
158
+ - delete_memory: Remove a memory. Required: memory_id
159
+ - log_learning: Record a self-improvement entry. Required: title, category (correction|error|insight|best_practice|feature_request). Optional: agent_id, detail, severity, source_conversation_id, source_task_id
160
+ - list_learnings: View learnings. Optional: agent_id, category, status, limit
161
+ - promote_learning: Elevate to production config. Required: learning_id, promoted_to (soul|agents|tools|sop|prompt_chunk)
162
+ - resolve_learning: Mark resolved/wont_fix. Required: learning_id, status. Optional: resolution
163
+ - get_brain_status: Memory/learning stats + config. Optional: agent_id
164
+ - configure_brain: Update brain settings. Required: agent_id. Optional: l1_ttl_hours, l2_max_entries, auto_learn, auto_memory
165
+ - This is your SUBCONSCIOUS — use it instinctively, not deliberately`,
153
166
  };
154
167
 
155
168
  export function getSystemPrompt(state: {
@@ -273,6 +286,32 @@ Action:
273
286
  - When you load SOPs (🔴): state which SOP(s) you're following
274
287
  - When you ask about SOPs (🟡): keep the question brief and direct
275
288
  - When you skip SOPs (🟢): do NOT mention SOPs at all — just execute silently
289
+
290
+ ## Agent Brain Protocol (Subconscious)
291
+
292
+ Your brain persists across conversations. Active memories and unresolved learnings are injected at startup.
293
+
294
+ ### Auto-Memory (OFIERE_BRAIN_OPS save_memory)
295
+ After interactions where ANY of these occur, call save_memory:
296
+ - User shares important context about themselves, preferences, or workflow → L2_journal, source: "conversation"
297
+ - You complete a task with noteworthy results or discoveries → L2_journal, source: "task_execution"
298
+ - You observe important system behavior worth tracking → L2_journal, source: "observation"
299
+ - Something needs to stay in your working context RIGHT NOW → L1_focus (auto-expires)
300
+
301
+ ### Auto-Learn (OFIERE_BRAIN_OPS log_learning)
302
+ After interactions where ANY of these occur, call log_learning:
303
+ - User corrects you → category: "correction", severity: "medium"
304
+ - A tool call fails or returns an error → category: "error"
305
+ - User asks for something you can't do → category: "feature_request"
306
+ - Your knowledge or assumption was wrong → category: "insight"
307
+ - You discover a better approach than what you did → category: "best_practice"
308
+
309
+ ### Rules
310
+ - These calls happen AFTER your response to the user — never delay your reply to save a memory
311
+ - Do NOT announce "saving memory" or "logging learning" to the user — this is subconscious
312
+ - Do NOT save trivial interactions (greetings, confirmations, simple CRUD) — only signal worth remembering
313
+ - Keep memory content concise: 1-3 sentences max. No system junk, keep it human
314
+ - When you see your unresolved learnings at startup, actively avoid repeating those mistakes
276
315
  </ofiere-pm>`;
277
316
  }
278
317
 
package/src/tools.ts CHANGED
@@ -5085,6 +5085,323 @@ async function handleSOPApplyTemplate(
5085
5085
  }
5086
5086
 
5087
5087
 
5088
+ // ═══════════════════════════════════════════════════════════════════════════════
5089
+ // META-TOOL 14: OFIERE_BRAIN_OPS — Agent Memory + Self-Improvement
5090
+ // ═══════════════════════════════════════════════════════════════════════════════
5091
+
5092
+ function registerBrainOps(
5093
+ api: any,
5094
+ supabase: SupabaseClient,
5095
+ userId: string,
5096
+ resolveAgent: (id?: string) => Promise<string | null>,
5097
+ ): void {
5098
+ api.registerTool({
5099
+ name: "OFIERE_BRAIN_OPS",
5100
+ label: "Ofiere Brain Operations",
5101
+ description:
5102
+ `Agent memory and self-improvement system. Persistent brain that learns, remembers, and never repeats mistakes.\n\n` +
5103
+ `Memory Actions:\n` +
5104
+ `- "save_memory": Store a memory. Required: content, tier (L1_focus|L2_journal|L3_core). Optional: agent_id, source, context_key, importance (1-10)\n` +
5105
+ `- "recall": Search memories. Required: query. Optional: agent_id, tier, limit\n` +
5106
+ `- "delete_memory": Remove a memory. Required: memory_id\n\n` +
5107
+ `Learning Actions:\n` +
5108
+ `- "log_learning": Record a self-improvement entry. Required: title, category (correction|error|insight|best_practice|feature_request). Optional: agent_id, detail, severity (low|medium|high|critical), source_conversation_id, source_task_id\n` +
5109
+ `- "list_learnings": View learnings. Optional: agent_id, category, status, limit\n` +
5110
+ `- "promote_learning": Promote to production. Required: learning_id, promoted_to (soul|agents|tools|sop|prompt_chunk)\n` +
5111
+ `- "resolve_learning": Mark resolved/wont_fix. Required: learning_id, status (resolved|wont_fix). Optional: resolution\n\n` +
5112
+ `Status Actions:\n` +
5113
+ `- "get_brain_status": Memory counts, learning stats, config. Optional: agent_id\n` +
5114
+ `- "configure_brain": Update brain settings. Required: agent_id. Optional: l1_ttl_hours, l2_max_entries, auto_learn, auto_memory\n\n` +
5115
+ `Tier Guide: L1_focus = working memory (24h TTL), L2_journal = medium-term events, L3_core = long-term wisdom.\n` +
5116
+ `This tool is your subconscious — call it after corrections, errors, discoveries, and insights.`,
5117
+ parameters: {
5118
+ type: "object",
5119
+ required: ["action"],
5120
+ properties: {
5121
+ action: {
5122
+ type: "string",
5123
+ description: "The operation to perform: save_memory, recall, delete_memory, log_learning, list_learnings, promote_learning, resolve_learning, get_brain_status, configure_brain",
5124
+ },
5125
+ // Memory params
5126
+ content: { type: "string", description: "Memory content or search query" },
5127
+ tier: { type: "string", enum: ["L1_focus", "L2_journal", "L3_core"], description: "Memory tier" },
5128
+ source: { type: "string", description: "Where memory came from: conversation, task_execution, observation, manual" },
5129
+ context_key: { type: "string", description: "Grouping key (task_id, conversation_id, topic)" },
5130
+ importance: { type: "number", description: "1-10 importance scale" },
5131
+ memory_id: { type: "string", description: "Memory ID for delete" },
5132
+ // Learning params
5133
+ title: { type: "string", description: "Learning title/summary" },
5134
+ category: { type: "string", enum: ["correction", "error", "insight", "best_practice", "feature_request"] },
5135
+ severity: { type: "string", enum: ["low", "medium", "high", "critical"] },
5136
+ detail: { type: "string", description: "Full context of the learning" },
5137
+ resolution: { type: "string", description: "How it was resolved" },
5138
+ learning_id: { type: "string", description: "Learning ID for promote/resolve" },
5139
+ status: { type: "string", enum: ["pending", "resolved", "wont_fix", "promoted"] },
5140
+ promoted_to: { type: "string", enum: ["soul", "agents", "tools", "sop", "prompt_chunk"] },
5141
+ source_conversation_id: { type: "string" },
5142
+ source_task_id: { type: "string" },
5143
+ // Shared
5144
+ agent_id: { type: "string", description: "Agent name or ID" },
5145
+ query: { type: "string", description: "Search query for recall/list" },
5146
+ limit: { type: "number", description: "Max results (default 20)" },
5147
+ // Config params
5148
+ l1_ttl_hours: { type: "number", description: "L1 memory expiry in hours (default 24)" },
5149
+ l2_max_entries: { type: "number", description: "Max L2 entries before compaction (default 500)" },
5150
+ auto_learn: { type: "boolean", description: "Enable auto-learning from corrections" },
5151
+ auto_memory: { type: "boolean", description: "Enable auto-memory from conversations" },
5152
+ },
5153
+ },
5154
+ async execute(_id: string, params: Record<string, unknown>) {
5155
+ const action = params.action as string;
5156
+
5157
+ switch (action) {
5158
+ // ── Memory: Save ──
5159
+ case "save_memory": {
5160
+ {
5161
+ const missing: string[] = [];
5162
+ if (!params.content) missing.push("content");
5163
+ if (!params.tier) missing.push("tier");
5164
+ if (missing.length > 0) return err(`Missing required: ${missing.join(", ")}`);
5165
+ }
5166
+ const agentId = await resolveAgent(params.agent_id as string);
5167
+ if (!agentId) return err("Could not resolve agent_id");
5168
+
5169
+ const tier = params.tier as string;
5170
+
5171
+ // Calculate L1 expiry
5172
+ let expiresAt: string | null = null;
5173
+ if (tier === "L1_focus") {
5174
+ // Check config for custom TTL
5175
+ const { data: config } = await supabase
5176
+ .from("agent_memory_config")
5177
+ .select("l1_ttl_hours")
5178
+ .eq("user_id", userId)
5179
+ .eq("agent_id", agentId)
5180
+ .single();
5181
+ const ttlHours = config?.l1_ttl_hours || 24;
5182
+ expiresAt = new Date(Date.now() + ttlHours * 60 * 60 * 1000).toISOString();
5183
+ }
5184
+
5185
+ const { data, error } = await supabase.from("agent_memories").insert({
5186
+ user_id: userId,
5187
+ agent_id: agentId,
5188
+ tier,
5189
+ content: params.content,
5190
+ source: (params.source as string) || "conversation",
5191
+ context_key: (params.context_key as string) || null,
5192
+ importance: (params.importance as number) || 5,
5193
+ expires_at: expiresAt,
5194
+ }).select("id, tier, importance, created_at").single();
5195
+
5196
+ if (error) return err(error.message);
5197
+ return ok({ message: `Memory saved to ${tier}`, memory: data });
5198
+ }
5199
+
5200
+ // ── Memory: Recall ──
5201
+ case "recall": {
5202
+ if (!params.query) return err("Missing required: query");
5203
+ const agentId = params.agent_id ? await resolveAgent(params.agent_id as string) : null;
5204
+ const searchTerm = `%${params.query}%`;
5205
+ const limit = (params.limit as number) || 20;
5206
+
5207
+ let q = supabase.from("agent_memories")
5208
+ .select("id, agent_id, tier, content, source, context_key, importance, created_at")
5209
+ .eq("user_id", userId)
5210
+ .ilike("content", searchTerm)
5211
+ .order("importance", { ascending: false })
5212
+ .order("created_at", { ascending: false })
5213
+ .limit(limit);
5214
+
5215
+ if (agentId) q = q.eq("agent_id", agentId);
5216
+ if (params.tier) q = q.eq("tier", params.tier as string);
5217
+
5218
+ // Exclude expired L1 memories
5219
+ q = q.or("expires_at.is.null,expires_at.gt." + new Date().toISOString());
5220
+
5221
+ const { data, error } = await q;
5222
+ if (error) return err(error.message);
5223
+ return ok({ memories: data || [], count: (data || []).length, query: params.query });
5224
+ }
5225
+
5226
+ // ── Memory: Delete ──
5227
+ case "delete_memory": {
5228
+ if (!params.memory_id) return err("Missing required: memory_id");
5229
+ const { error } = await supabase.from("agent_memories")
5230
+ .delete()
5231
+ .eq("id", params.memory_id as string)
5232
+ .eq("user_id", userId);
5233
+ if (error) return err(error.message);
5234
+ return ok({ message: "Memory deleted", ok: true });
5235
+ }
5236
+
5237
+ // ── Learning: Log ──
5238
+ case "log_learning": {
5239
+ {
5240
+ const missing: string[] = [];
5241
+ if (!params.title) missing.push("title");
5242
+ if (!params.category) missing.push("category");
5243
+ if (missing.length > 0) return err(`Missing required: ${missing.join(", ")}`);
5244
+ }
5245
+ const agentId = await resolveAgent(params.agent_id as string);
5246
+ if (!agentId) return err("Could not resolve agent_id");
5247
+
5248
+ const { data, error } = await supabase.from("agent_learnings").insert({
5249
+ user_id: userId,
5250
+ agent_id: agentId,
5251
+ category: params.category,
5252
+ severity: (params.severity as string) || "low",
5253
+ title: params.title,
5254
+ detail: (params.detail as string) || null,
5255
+ source_conversation_id: (params.source_conversation_id as string) || null,
5256
+ source_task_id: (params.source_task_id as string) || null,
5257
+ }).select("id, category, severity, title, status, created_at").single();
5258
+
5259
+ if (error) return err(error.message);
5260
+ return ok({ message: `Learning logged: "${params.title}"`, learning: data });
5261
+ }
5262
+
5263
+ // ── Learning: List ──
5264
+ case "list_learnings": {
5265
+ const agentId = params.agent_id ? await resolveAgent(params.agent_id as string) : null;
5266
+ const limit = (params.limit as number) || 20;
5267
+
5268
+ let q = supabase.from("agent_learnings")
5269
+ .select("id, agent_id, category, severity, title, detail, resolution, status, promoted_to, created_at, resolved_at")
5270
+ .eq("user_id", userId)
5271
+ .order("created_at", { ascending: false })
5272
+ .limit(limit);
5273
+
5274
+ if (agentId) q = q.eq("agent_id", agentId);
5275
+ if (params.category) q = q.eq("category", params.category as string);
5276
+ if (params.status) q = q.eq("status", params.status as string);
5277
+
5278
+ const { data, error } = await q;
5279
+ if (error) return err(error.message);
5280
+ return ok({ learnings: data || [], count: (data || []).length });
5281
+ }
5282
+
5283
+ // ── Learning: Promote ──
5284
+ case "promote_learning": {
5285
+ if (!params.learning_id || !params.promoted_to) return err("Missing required: learning_id, promoted_to");
5286
+ const { data, error } = await supabase.from("agent_learnings")
5287
+ .update({
5288
+ status: "promoted",
5289
+ promoted_to: params.promoted_to as string,
5290
+ resolved_at: new Date().toISOString(),
5291
+ })
5292
+ .eq("id", params.learning_id as string)
5293
+ .eq("user_id", userId)
5294
+ .select("id, title, status, promoted_to")
5295
+ .single();
5296
+ if (error) return err(error.message);
5297
+ return ok({ message: `Learning promoted to ${params.promoted_to}`, learning: data });
5298
+ }
5299
+
5300
+ // ── Learning: Resolve ──
5301
+ case "resolve_learning": {
5302
+ if (!params.learning_id) return err("Missing required: learning_id");
5303
+ const newStatus = (params.status as string) || "resolved";
5304
+ if (!["resolved", "wont_fix"].includes(newStatus)) return err("status must be 'resolved' or 'wont_fix'");
5305
+
5306
+ const updates: Record<string, unknown> = {
5307
+ status: newStatus,
5308
+ resolved_at: new Date().toISOString(),
5309
+ };
5310
+ if (params.resolution) updates.resolution = params.resolution;
5311
+
5312
+ const { data, error } = await supabase.from("agent_learnings")
5313
+ .update(updates)
5314
+ .eq("id", params.learning_id as string)
5315
+ .eq("user_id", userId)
5316
+ .select("id, title, status, resolution")
5317
+ .single();
5318
+ if (error) return err(error.message);
5319
+ return ok({ message: `Learning marked as ${newStatus}`, learning: data });
5320
+ }
5321
+
5322
+ // ── Brain Status ──
5323
+ case "get_brain_status": {
5324
+ const agentId = params.agent_id ? await resolveAgent(params.agent_id as string) : null;
5325
+
5326
+ // Memory counts by tier
5327
+ let memQ = supabase.from("agent_memories")
5328
+ .select("tier")
5329
+ .eq("user_id", userId)
5330
+ .or("expires_at.is.null,expires_at.gt." + new Date().toISOString());
5331
+ if (agentId) memQ = memQ.eq("agent_id", agentId);
5332
+ const { data: memRows } = await memQ;
5333
+
5334
+ const tierCounts: Record<string, number> = { L1_focus: 0, L2_journal: 0, L3_core: 0 };
5335
+ for (const row of memRows || []) {
5336
+ const t = (row as any).tier;
5337
+ tierCounts[t] = (tierCounts[t] || 0) + 1;
5338
+ }
5339
+
5340
+ // Learning counts by status
5341
+ let learnQ = supabase.from("agent_learnings")
5342
+ .select("status, category")
5343
+ .eq("user_id", userId);
5344
+ if (agentId) learnQ = learnQ.eq("agent_id", agentId);
5345
+ const { data: learnRows } = await learnQ;
5346
+
5347
+ const statusCounts: Record<string, number> = {};
5348
+ const categoryCounts: Record<string, number> = {};
5349
+ for (const row of learnRows || []) {
5350
+ const s = (row as any).status;
5351
+ const c = (row as any).category;
5352
+ statusCounts[s] = (statusCounts[s] || 0) + 1;
5353
+ categoryCounts[c] = (categoryCounts[c] || 0) + 1;
5354
+ }
5355
+
5356
+ // Config
5357
+ let configQ = supabase.from("agent_memory_config")
5358
+ .select("*")
5359
+ .eq("user_id", userId);
5360
+ if (agentId) configQ = configQ.eq("agent_id", agentId);
5361
+ const { data: configs } = await configQ;
5362
+
5363
+ return ok({
5364
+ memories: { ...tierCounts, total: (memRows || []).length },
5365
+ learnings: {
5366
+ total: (learnRows || []).length,
5367
+ by_status: statusCounts,
5368
+ by_category: categoryCounts,
5369
+ },
5370
+ config: configs?.[0] || { l1_ttl_hours: 24, l2_max_entries: 500, auto_learn: true, auto_memory: true },
5371
+ });
5372
+ }
5373
+
5374
+ // ── Configure Brain ──
5375
+ case "configure_brain": {
5376
+ const agentId = await resolveAgent(params.agent_id as string);
5377
+ if (!agentId) return err("Missing required: agent_id");
5378
+
5379
+ const configData: Record<string, unknown> = {
5380
+ user_id: userId,
5381
+ agent_id: agentId,
5382
+ };
5383
+ if (params.l1_ttl_hours !== undefined) configData.l1_ttl_hours = params.l1_ttl_hours;
5384
+ if (params.l2_max_entries !== undefined) configData.l2_max_entries = params.l2_max_entries;
5385
+ if (params.auto_learn !== undefined) configData.auto_learn = params.auto_learn;
5386
+ if (params.auto_memory !== undefined) configData.auto_memory = params.auto_memory;
5387
+
5388
+ // Upsert by (user_id, agent_id)
5389
+ const { data, error } = await supabase.from("agent_memory_config")
5390
+ .upsert(configData, { onConflict: "user_id,agent_id" })
5391
+ .select()
5392
+ .single();
5393
+
5394
+ if (error) return err(error.message);
5395
+ return ok({ message: `Brain config updated for agent`, config: data });
5396
+ }
5397
+
5398
+ default:
5399
+ return err(`Unknown action "${action}".`);
5400
+ }
5401
+ },
5402
+ });
5403
+ }
5404
+
5088
5405
  // ═══════════════════════════════════════════════════════════════════════════════
5089
5406
  // Public: Register All Meta-Tools
5090
5407
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -5115,12 +5432,104 @@ export function registerTools(
5115
5432
  registerFileOps(api, supabase, userId); // 11
5116
5433
  registerPlanOps(api, supabase, userId, resolveAgent); // 12
5117
5434
  registerSOPOps(api, supabase, userId, resolveAgent); // 13
5435
+ registerBrainOps(api, supabase, userId, resolveAgent); // 14
5436
+
5437
+ // ── Inject brain context at bootstrap ──
5438
+ // This loads the agent's active memories + unresolved learnings into the
5439
+ // system prompt so the agent starts every conversation with full context.
5440
+ // Runs async — does NOT block registration.
5441
+ injectBrainContext(api, supabase, userId, fallbackAgentId).catch((e: any) => {
5442
+ api.logger.warn?.(`[ofiere] Brain context injection failed: ${e?.message || e}`);
5443
+ });
5118
5444
 
5119
5445
  // ── Count and log ──
5120
- const toolCount = 13;
5446
+ const toolCount = 14;
5121
5447
  const callerName = getCallingAgentName(api);
5122
5448
  const agentLabel = fallbackAgentId || callerName || "auto-detect";
5123
5449
  api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
5124
5450
 
5125
5451
  return toolCount;
5126
5452
  }
5453
+
5454
+ // ── Brain Context Bootstrap Injection ──────────────────────────────────────
5455
+ // Queries the agent's active memories and unresolved learnings, then appends
5456
+ // them to the system prompt via api.on("before_prompt_build"). This happens
5457
+ // once at registration time (per conversation) and costs ~20ms.
5458
+
5459
+ async function injectBrainContext(
5460
+ api: any,
5461
+ supabase: SupabaseClient,
5462
+ userId: string,
5463
+ agentId: string,
5464
+ ): Promise<void> {
5465
+ if (!agentId) return; // Can't inject without knowing which agent
5466
+
5467
+ const now = new Date().toISOString();
5468
+
5469
+ // Parallel queries — fast
5470
+ const [l1Res, l3Res, learningsRes] = await Promise.all([
5471
+ // Active L1 focus memories (not expired)
5472
+ supabase.from("agent_memories")
5473
+ .select("content, importance")
5474
+ .eq("user_id", userId)
5475
+ .eq("agent_id", agentId)
5476
+ .eq("tier", "L1_focus")
5477
+ .or(`expires_at.is.null,expires_at.gt.${now}`)
5478
+ .order("importance", { ascending: false })
5479
+ .limit(5),
5480
+ // Recent L3 core (long-term wisdom)
5481
+ supabase.from("agent_memories")
5482
+ .select("content")
5483
+ .eq("user_id", userId)
5484
+ .eq("agent_id", agentId)
5485
+ .eq("tier", "L3_core")
5486
+ .order("created_at", { ascending: false })
5487
+ .limit(3),
5488
+ // Unresolved learnings (don't repeat mistakes)
5489
+ supabase.from("agent_learnings")
5490
+ .select("title, category, detail")
5491
+ .eq("user_id", userId)
5492
+ .eq("agent_id", agentId)
5493
+ .eq("status", "pending")
5494
+ .order("severity", { ascending: false })
5495
+ .limit(10),
5496
+ ]);
5497
+
5498
+ const l1 = l1Res.data || [];
5499
+ const l3 = l3Res.data || [];
5500
+ const learnings = learningsRes.data || [];
5501
+
5502
+ // Only inject if there's something to inject
5503
+ if (l1.length === 0 && l3.length === 0 && learnings.length === 0) return;
5504
+
5505
+ const sections: string[] = [];
5506
+
5507
+ if (l1.length > 0) {
5508
+ sections.push("### Active Focus (L1)\n" + l1.map((m: any) => `- ${m.content}`).join("\n"));
5509
+ }
5510
+
5511
+ if (l3.length > 0) {
5512
+ sections.push("### Core Wisdom (L3)\n" + l3.map((m: any) => `- ${m.content}`).join("\n"));
5513
+ }
5514
+
5515
+ if (learnings.length > 0) {
5516
+ sections.push(
5517
+ "### ⚠️ Unresolved Learnings (DO NOT repeat these)\n" +
5518
+ learnings.map((l: any) => `- [${l.category}] ${l.title}${l.detail ? `: ${l.detail}` : ""}`).join("\n")
5519
+ );
5520
+ }
5521
+
5522
+ const brainContext = `<agent-brain>\n## Your Brain Context\n\n${sections.join("\n\n")}\n</agent-brain>`;
5523
+
5524
+ // Append to the existing before_prompt_build hook
5525
+ // We use a second hook — OpenClaw supports multiple listeners
5526
+ try {
5527
+ api.on("before_prompt_build", () => ({
5528
+ appendSystemContext: brainContext,
5529
+ }));
5530
+ } catch {
5531
+ // Fallback: log that injection wasn't possible
5532
+ api.logger.debug?.("[ofiere] Could not register brain context hook — appendSystemContext may not be supported");
5533
+ }
5534
+ }
5535
+