ofiere-openclaw-plugin 4.42.0 → 4.44.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.42.0",
3
+ "version": "4.44.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin for Ofiere PM - 16 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, execution plan builder, SOP management, agent brain, talent management, and corporate frameworks",
6
6
  "keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
@@ -15,6 +15,7 @@ import {
15
15
  invalidateAgentTier,
16
16
  } from "./agent-tier.js";
17
17
  import { issueAttachmentToken, verifyAttachmentToken } from "./attach-token.js";
18
+ import { loadSubagentRow, buildStaffPersonaBlock, readDispatchSubagentId, type SubagentRow } from "./staffPersona.js";
18
19
 
19
20
  interface ToolResult {
20
21
  content: Array<{ type: "text"; text: string }>;
@@ -360,8 +361,29 @@ async function buildAttachmentBlock(args: {
360
361
  supabase: SupabaseClient;
361
362
  userId: string;
362
363
  agentId: string;
364
+ subagentId?: string | null;
363
365
  }): Promise<string> {
364
- const { supabase, userId, agentId } = args;
366
+ const { supabase, userId, agentId, subagentId } = args;
367
+
368
+ // When a staff dispatch arrives, prefer attachments scoped to the subagent
369
+ // so a chief and its staff never share an attachment surface implicitly.
370
+ if (subagentId) {
371
+ const { data: staffConv } = await supabase
372
+ .from("conversations")
373
+ .select("id, attached_sop_ids, attached_framework_ids")
374
+ .eq("user_id", userId)
375
+ .eq("agent_id", agentId)
376
+ .eq("subagent_id", subagentId)
377
+ .order("updated_at", { ascending: false })
378
+ .limit(1)
379
+ .maybeSingle();
380
+ if (staffConv) {
381
+ const sopIds: string[] = (staffConv.attached_sop_ids as string[] | null) || [];
382
+ const fwIds: string[] = (staffConv.attached_framework_ids as string[] | null) || [];
383
+ return renderBlockForIds({ supabase, userId, sopIds, fwIds });
384
+ }
385
+ // No staff-scoped conversation yet — fall through to chief-level lookup.
386
+ }
365
387
 
366
388
  // Find the most recently active conversation for this agent. The dashboard
367
389
  // bumps `updated_at` whenever a message is sent or attachments change, so
@@ -402,6 +424,36 @@ export function registerAttachmentContextHook(args: {
402
424
  }
403
425
  if (!resolvedAgentId) return;
404
426
 
427
+ // Cycle 7b — staff persona injection. Only fires when subagent_id is
428
+ // present in dispatch metadata (task-dispatcher / scheduler / explicit
429
+ // dispatch params). Plain user chats with the chief never persona-swap.
430
+ const subagentId = readDispatchSubagentId(ctx);
431
+ let staffPrefix = "";
432
+ let staffRow: SubagentRow | null = null;
433
+ if (subagentId) {
434
+ staffRow = await loadSubagentRow(supabase, userId, subagentId);
435
+ if (staffRow && staffRow.chief_agent_id === resolvedAgentId) {
436
+ staffPrefix = buildStaffPersonaBlock(staffRow) + "\n\n---\n\n";
437
+ api.logger?.debug?.(
438
+ `[ofiere-staff] subagent ${subagentId} (${staffRow.name}) reporting to ${resolvedAgentId} — persona injected`,
439
+ );
440
+ } else if (staffRow) {
441
+ api.logger?.warn?.(
442
+ `[ofiere-staff] subagent ${subagentId} chief_mismatch (got ${staffRow.chief_agent_id}, expected ${resolvedAgentId}) — ignoring persona`,
443
+ );
444
+ staffRow = null;
445
+ } else {
446
+ api.logger?.warn?.(`[ofiere-staff] subagent ${subagentId} not found — ignoring persona`);
447
+ }
448
+ }
449
+
450
+ // wrap any return-block with the staff prefix (or pass through unchanged
451
+ // when no staff dispatch).
452
+ const wrap = (block: string | undefined | null): { appendSystemContext: string } | undefined => {
453
+ const finalBlock = staffPrefix + (block || "");
454
+ return finalBlock ? { appendSystemContext: finalBlock } : undefined;
455
+ };
456
+
405
457
  // Dispatch-params path: workflow executor + task-dispatcher edge function
406
458
  // can stash explicit `attached_sop_ids` / `attached_framework_ids` on the
407
459
  // chat.send frame's metadata. When present, prefer them over the
@@ -416,7 +468,16 @@ export function registerAttachmentContextHook(args: {
416
468
  sopIds: dispatchIds.sopIds,
417
469
  fwIds: dispatchIds.fwIds,
418
470
  });
419
- return block ? { appendSystemContext: block } : undefined;
471
+ return wrap(block);
472
+ }
473
+
474
+ // Staff-mode runs bypass the chief-level attachment cache so a chief
475
+ // and its staff never share a cached attachment block.
476
+ if (subagentId) {
477
+ const block = await buildAttachmentBlock({
478
+ supabase, userId, agentId: resolvedAgentId, subagentId,
479
+ });
480
+ return wrap(block);
420
481
  }
421
482
 
422
483
  // Multi-tenant: a single plugin process can serve multiple users — key
@@ -426,7 +487,7 @@ export function registerAttachmentContextHook(args: {
426
487
  if (cached) {
427
488
  const age = Date.now() - cached.at;
428
489
  if (age < ATTACH_CACHE_TTL_MS) {
429
- return cached.text ? { appendSystemContext: cached.text } : undefined;
490
+ return wrap(cached.text);
430
491
  }
431
492
  if (age < ATTACH_CACHE_STALE_MS) {
432
493
  // Refresh in background
@@ -436,13 +497,13 @@ export function registerAttachmentContextHook(args: {
436
497
  attachmentCache.set(cacheKey, { text: fresh, at: Date.now() });
437
498
  } catch { /* ignore */ }
438
499
  })();
439
- return cached.text ? { appendSystemContext: cached.text } : undefined;
500
+ return wrap(cached.text);
440
501
  }
441
502
  }
442
503
 
443
504
  const block = await buildAttachmentBlock({ supabase, userId, agentId: resolvedAgentId });
444
505
  attachmentCache.set(cacheKey, { text: block, at: Date.now() });
445
- return block ? { appendSystemContext: block } : undefined;
506
+ return wrap(block);
446
507
  } catch (e) {
447
508
  api.logger?.debug?.(`[ofiere-attach] before_prompt_build error: ${e instanceof Error ? e.message : e}`);
448
509
  }
@@ -0,0 +1,84 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+
3
+ export interface SubagentRow {
4
+ id: string;
5
+ chief_agent_id: string;
6
+ name: string;
7
+ role: string | null;
8
+ codename: string | null;
9
+ system_prompt: string | null;
10
+ mission: string | null;
11
+ responsibilities: string | null;
12
+ instructions: string | null;
13
+ primary_model: string | null;
14
+ coding_model: string | null;
15
+ tool_call_model: string | null;
16
+ function_call_model: string | null;
17
+ vision_model: string | null;
18
+ mcp_server_ids: string[] | null;
19
+ attached_sop_ids: string[] | null;
20
+ attached_framework_ids: string[] | null;
21
+ }
22
+
23
+ export async function loadSubagentRow(
24
+ supabase: SupabaseClient,
25
+ userId: string,
26
+ subagentId: string,
27
+ ): Promise<SubagentRow | null> {
28
+ const { data, error } = await supabase
29
+ .from("agent_subagents")
30
+ .select(
31
+ "id, chief_agent_id, name, role, codename, system_prompt, mission, responsibilities, instructions, primary_model, coding_model, tool_call_model, function_call_model, vision_model, mcp_server_ids, attached_sop_ids, attached_framework_ids",
32
+ )
33
+ .eq("user_id", userId)
34
+ .eq("id", subagentId)
35
+ .maybeSingle();
36
+ if (error || !data) return null;
37
+ return data as SubagentRow;
38
+ }
39
+
40
+ // Cycle 7b — pluck subagent_id from dispatch metadata only. Plain user chats
41
+ // must NOT trigger the staff persona swap or report emission, so we deliberately
42
+ // do not consult ctx.subagentId / ctx.subagent_id at the top level.
43
+ export function readDispatchSubagentId(ctx: any, event?: any): string | null {
44
+ const candidates: Array<unknown> = [
45
+ ctx?.metadata?.subagent_id,
46
+ ctx?.metadata?.dispatch?.subagent_id,
47
+ ctx?.params?.metadata?.subagent_id,
48
+ ctx?.payload?.metadata?.subagent_id,
49
+ ctx?.request?.metadata?.subagent_id,
50
+ ctx?.options?.metadata?.subagent_id,
51
+ ctx?.attachments?.subagent_id,
52
+ ctx?.dispatch?.subagent_id,
53
+ event?.metadata?.subagent_id,
54
+ event?.context?.metadata?.subagent_id,
55
+ ];
56
+ for (const c of candidates) {
57
+ if (typeof c === "string" && c.length) return c;
58
+ }
59
+ return null;
60
+ }
61
+
62
+ export function buildStaffPersonaBlock(staff: SubagentRow): string {
63
+ const lines: string[] = [];
64
+ lines.push(`# STAFF IDENTITY`);
65
+ lines.push(
66
+ `You are **${staff.name}**${staff.role ? ` (${staff.role})` : ""}, a staff member reporting to chief \`${staff.chief_agent_id}\`.`,
67
+ );
68
+ lines.push(
69
+ `You are NOT the chief. Execute the assigned task, then return a structured report. Do not adopt the chief's persona.`,
70
+ );
71
+ if (staff.system_prompt?.trim()) {
72
+ lines.push(``, `## Persona`, staff.system_prompt.trim());
73
+ }
74
+ if (staff.mission?.trim()) {
75
+ lines.push(``, `## Mission`, staff.mission.trim());
76
+ }
77
+ if (staff.responsibilities?.trim()) {
78
+ lines.push(``, `## Responsibilities`, staff.responsibilities.trim());
79
+ }
80
+ if (staff.instructions?.trim()) {
81
+ lines.push(``, `## Operating Instructions`, staff.instructions.trim());
82
+ }
83
+ return lines.join("\n");
84
+ }
package/src/tools.ts CHANGED
@@ -19,6 +19,7 @@ import {
19
19
  registerAttachmentContextHook,
20
20
  } from "./attachments.js";
21
21
  import { invalidateAgentTier } from "./agent-tier.js";
22
+ import { loadSubagentRow, readDispatchSubagentId } from "./staffPersona.js";
22
23
 
23
24
  // ─── Tool result shape (matches OpenClaw SDK) ────────────────────────────────
24
25
 
@@ -1174,7 +1175,9 @@ function registerAgentOps(
1174
1175
  `Actions:\n` +
1175
1176
  `- "list": List all top-level agents (chiefs / native + OpenClaw) with their IDs, names, roles, and status. Use this to find the correct agent_id for task assignment.\n` +
1176
1177
  `- "list_subagents": List staff subagents under a chief. Required: chief_agent_id.\n` +
1178
+ `- "get_subagent": Fetch a single subagent's full row (including persona + model overrides + mcp_server_ids). Required: subagent_id.\n` +
1177
1179
  `- "create_subagent": Create a staff subagent under a chief (max 5 per chief). Required: chief_agent_id, name. Optional: role (default "Staff"), codename, color_hex (default "#64748b").\n` +
1180
+ `- "update_subagent": Update a staff subagent's persona, model overrides, MCP allowlist, or identity fields. Required: subagent_id. Any of name, role, codename, color_hex, system_prompt, mission, responsibilities, instructions, primary_model, coding_model, tool_call_model, function_call_model, vision_model, mcp_server_ids may be set.\n` +
1178
1181
  `- "delete_subagent": Remove a staff subagent. Required: subagent_id.\n` +
1179
1182
  `- "invalidate_tier_cache": Flush the plugin's in-process tier resolver cache (5 min TTL). Optional: agent_id to flush a single entry; omit to flush all entries for the calling user. Use after any direct-DB mutation of agent_tier_overrides.`,
1180
1183
  parameters: {
@@ -1184,15 +1187,29 @@ function registerAgentOps(
1184
1187
  action: {
1185
1188
  type: "string",
1186
1189
  description: "The operation to perform",
1187
- enum: ["list", "list_subagents", "create_subagent", "delete_subagent", "invalidate_tier_cache"],
1190
+ enum: ["list", "list_subagents", "get_subagent", "create_subagent", "update_subagent", "delete_subagent", "invalidate_tier_cache"],
1188
1191
  },
1189
1192
  chief_agent_id: { type: "string", description: "Chief agent ID (required for list_subagents, create_subagent)" },
1190
- subagent_id: { type: "string", description: "Subagent ID (required for delete_subagent)" },
1193
+ subagent_id: { type: "string", description: "Subagent ID (required for get/update/delete_subagent)" },
1191
1194
  agent_id: { type: "string", description: "Agent ID (optional for invalidate_tier_cache — omit to flush all entries for the user)" },
1192
1195
  name: { type: "string", description: "Subagent display name (required for create_subagent)" },
1193
1196
  role: { type: "string", description: "Subagent role label, e.g. 'Staff', 'Analyst'. Defaults to 'Staff'." },
1194
1197
  codename: { type: "string", description: "Optional subagent codename" },
1195
1198
  color_hex: { type: "string", description: "Optional subagent UI color, default '#64748b'" },
1199
+ system_prompt: { type: "string", description: "Staff persona / identity (Markdown). Optional." },
1200
+ mission: { type: "string", description: "Short-form mission statement. Optional." },
1201
+ responsibilities: { type: "string", description: "Bulleted Markdown list of duties. Optional." },
1202
+ instructions: { type: "string", description: "Detailed how-they-work instructions (Markdown). Optional." },
1203
+ primary_model: { type: "string", description: "Override primary slot. NULL/omit = inherit chief." },
1204
+ coding_model: { type: "string", description: "Override coding slot." },
1205
+ tool_call_model: { type: "string", description: "Override tool-call slot." },
1206
+ function_call_model: { type: "string", description: "Override function-call slot." },
1207
+ vision_model: { type: "string", description: "Override vision slot." },
1208
+ mcp_server_ids: {
1209
+ type: "array",
1210
+ items: { type: "string" },
1211
+ description: "Allowlist of MCP server UUIDs. Reserved for cycle 8 enforcement.",
1212
+ },
1196
1213
  },
1197
1214
  },
1198
1215
  async execute(_id: string, params: Record<string, unknown>) {
@@ -1203,14 +1220,18 @@ function registerAgentOps(
1203
1220
  return handleListAgents(api, supabase, userId, fallbackAgentId);
1204
1221
  case "list_subagents":
1205
1222
  return handleListSubagents(supabase, userId, params);
1223
+ case "get_subagent":
1224
+ return handleGetSubagent(supabase, userId, params);
1206
1225
  case "create_subagent":
1207
1226
  return handleCreateSubagent(supabase, userId, params);
1227
+ case "update_subagent":
1228
+ return handleUpdateSubagent(supabase, userId, params);
1208
1229
  case "delete_subagent":
1209
1230
  return handleDeleteSubagent(supabase, userId, params);
1210
1231
  case "invalidate_tier_cache":
1211
1232
  return handleInvalidateTierCache(userId, params);
1212
1233
  default:
1213
- return err(`Unknown action "${action}". Valid actions: list, list_subagents, create_subagent, delete_subagent, invalidate_tier_cache`);
1234
+ return err(`Unknown action "${action}". Valid actions: list, list_subagents, get_subagent, create_subagent, update_subagent, delete_subagent, invalidate_tier_cache`);
1214
1235
  }
1215
1236
  },
1216
1237
  });
@@ -5398,6 +5419,66 @@ async function handleDeleteSubagent(supabase: SupabaseClient, userId: string, pa
5398
5419
  } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
5399
5420
  }
5400
5421
 
5422
+ async function handleGetSubagent(
5423
+ supabase: SupabaseClient,
5424
+ userId: string,
5425
+ params: Record<string, unknown>,
5426
+ ): Promise<ToolResult> {
5427
+ try {
5428
+ const subagentId = params.subagent_id as string | undefined;
5429
+ if (!subagentId) return err("Missing required field: subagent_id");
5430
+ const { data, error } = await supabase
5431
+ .from("agent_subagents")
5432
+ .select("*")
5433
+ .eq("user_id", userId)
5434
+ .eq("id", subagentId)
5435
+ .maybeSingle();
5436
+ if (error) return err(error.message);
5437
+ if (!data) return err("subagent_not_found_or_not_yours");
5438
+ return ok({ subagent: data });
5439
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
5440
+ }
5441
+
5442
+ async function handleUpdateSubagent(
5443
+ supabase: SupabaseClient,
5444
+ userId: string,
5445
+ params: Record<string, unknown>,
5446
+ ): Promise<ToolResult> {
5447
+ try {
5448
+ const subagentId = params.subagent_id as string | undefined;
5449
+ if (!subagentId) return err("Missing required field: subagent_id");
5450
+
5451
+ const updates: Record<string, unknown> = {};
5452
+ const stringFields = [
5453
+ "name", "role", "codename", "color_hex",
5454
+ "system_prompt", "mission", "responsibilities", "instructions",
5455
+ "primary_model", "coding_model", "tool_call_model", "function_call_model", "vision_model",
5456
+ ] as const;
5457
+ for (const key of stringFields) {
5458
+ if (key in params && params[key] !== undefined) updates[key] = params[key];
5459
+ }
5460
+ if ("mcp_server_ids" in params && Array.isArray(params.mcp_server_ids)) {
5461
+ updates.mcp_server_ids = (params.mcp_server_ids as unknown[]).filter(
5462
+ (x): x is string => typeof x === "string",
5463
+ );
5464
+ }
5465
+ if (Object.keys(updates).length === 0) {
5466
+ return err("no updatable fields provided");
5467
+ }
5468
+
5469
+ const { data, error } = await supabase
5470
+ .from("agent_subagents")
5471
+ .update(updates)
5472
+ .eq("id", subagentId)
5473
+ .eq("user_id", userId)
5474
+ .select()
5475
+ .maybeSingle();
5476
+ if (error) return err(error.message);
5477
+ if (!data) return err("subagent_not_found_or_not_yours");
5478
+ return ok({ subagent: data, updated_fields: Object.keys(updates) });
5479
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
5480
+ }
5481
+
5401
5482
  async function handleSOPApplyTemplate(
5402
5483
  supabase: SupabaseClient, userId: string,
5403
5484
  resolveAgent: (id?: string) => Promise<string | null>,
@@ -6450,6 +6531,77 @@ function registerBrainExtractionHook(
6450
6531
 
6451
6532
  api.logger.debug?.(`[ofiere-brain] agent_end identity: ctx.agentId=${ctx?.agentId || "(none)"} event.agentId=${event?.agentId || "(none)"} resolved=${resolvedAgentId}`);
6452
6533
 
6534
+ // ── Cycle 7b — Staff report emit ──
6535
+ // If this agent_end was triggered by a staff dispatch, write a chief-
6536
+ // scoped memory note ("Staff X reported on task Y: ...") and POST a
6537
+ // webhook so the dashboard can surface the report. The staff itself
6538
+ // gets no memory entry (req 3 — staff has no memory of its own).
6539
+ const staffDispatchSubagentId = readDispatchSubagentId(ctx, event);
6540
+ if (staffDispatchSubagentId) {
6541
+ (async () => {
6542
+ try {
6543
+ const staff = await loadSubagentRow(supabase, userId, staffDispatchSubagentId);
6544
+ if (!staff) {
6545
+ api.logger.warn?.(`[ofiere-staff-report] subagent ${staffDispatchSubagentId} not found — skipping report`);
6546
+ return;
6547
+ }
6548
+ if (staff.chief_agent_id !== resolvedAgentId) {
6549
+ api.logger.warn?.(`[ofiere-staff-report] chief_mismatch (subagent ${staffDispatchSubagentId} reports to ${staff.chief_agent_id}, run was on ${resolvedAgentId}) — skipping report`);
6550
+ return;
6551
+ }
6552
+ const taskId =
6553
+ ctx?.metadata?.task_id || ctx?.metadata?.dispatch?.task_id ||
6554
+ event?.metadata?.task_id || event?.context?.metadata?.task_id || null;
6555
+ const excerpt = lastAssistant.slice(0, 1500);
6556
+ const reportBody = taskId
6557
+ ? `Staff ${staff.name}${staff.role ? ` (${staff.role})` : ""} reported on task ${taskId}:\n\n${excerpt}`
6558
+ : `Staff ${staff.name}${staff.role ? ` (${staff.role})` : ""} reported:\n\n${excerpt}`;
6559
+ await supabase.from("agent_memories").insert({
6560
+ user_id: userId,
6561
+ agent_id: staff.chief_agent_id,
6562
+ tier: "L4_episodic",
6563
+ content: reportBody,
6564
+ source: "staff_report",
6565
+ importance: 5,
6566
+ context_key: taskId ? `staff_report:${staffDispatchSubagentId}:${taskId}` : `staff_report:${staffDispatchSubagentId}`,
6567
+ });
6568
+ api.logger.info?.(`[ofiere-staff-report] memory written for chief ${staff.chief_agent_id} from staff ${staff.name}`);
6569
+
6570
+ // Best-effort webhook to dashboard (existing OPENCLAW_WEBHOOK_SECRET).
6571
+ const webhookUrl = process.env.OFIERE_DASHBOARD_WEBHOOK_URL || "";
6572
+ const webhookSecret = process.env.OPENCLAW_WEBHOOK_SECRET || "";
6573
+ if (webhookUrl && webhookSecret) {
6574
+ try {
6575
+ await fetch(webhookUrl, {
6576
+ method: "POST",
6577
+ headers: {
6578
+ "content-type": "application/json",
6579
+ authorization: `Bearer ${webhookSecret}`,
6580
+ },
6581
+ body: JSON.stringify({
6582
+ type: "staff_report",
6583
+ payload: {
6584
+ user_id: userId,
6585
+ chief_agent_id: staff.chief_agent_id,
6586
+ subagent_id: staffDispatchSubagentId,
6587
+ task_id: taskId,
6588
+ response: excerpt,
6589
+ },
6590
+ }),
6591
+ });
6592
+ } catch (wErr) {
6593
+ api.logger.debug?.(`[ofiere-staff-report] webhook post failed: ${wErr instanceof Error ? wErr.message : String(wErr)}`);
6594
+ }
6595
+ }
6596
+ } catch (sErr) {
6597
+ api.logger.warn?.(`[ofiere-staff-report] failed: ${sErr instanceof Error ? sErr.message : String(sErr)}`);
6598
+ }
6599
+ })();
6600
+ // Skip the standard chief memory writes — this run was the staff's,
6601
+ // not the chief's. The chief gets a single L4 report entry above.
6602
+ return;
6603
+ }
6604
+
6453
6605
  // ── Fast Stream: Write L1_focus + L2_episode in parallel ──
6454
6606
  const rawContent = `User: ${lastUser}\nAssistant: ${lastAssistant}`;
6455
6607
  const immediateWrites: PromiseLike<any>[] = [];