ofiere-openclaw-plugin 4.41.1 → 4.43.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 +1 -1
- package/src/attachments.ts +66 -5
- package/src/staffPersona.ts +82 -0
- package/src/tools.ts +278 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.43.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"],
|
package/src/attachments.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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,82 @@
|
|
|
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
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function loadSubagentRow(
|
|
22
|
+
supabase: SupabaseClient,
|
|
23
|
+
userId: string,
|
|
24
|
+
subagentId: string,
|
|
25
|
+
): Promise<SubagentRow | null> {
|
|
26
|
+
const { data, error } = await supabase
|
|
27
|
+
.from("agent_subagents")
|
|
28
|
+
.select(
|
|
29
|
+
"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",
|
|
30
|
+
)
|
|
31
|
+
.eq("user_id", userId)
|
|
32
|
+
.eq("id", subagentId)
|
|
33
|
+
.maybeSingle();
|
|
34
|
+
if (error || !data) return null;
|
|
35
|
+
return data as SubagentRow;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Cycle 7b — pluck subagent_id from dispatch metadata only. Plain user chats
|
|
39
|
+
// must NOT trigger the staff persona swap or report emission, so we deliberately
|
|
40
|
+
// do not consult ctx.subagentId / ctx.subagent_id at the top level.
|
|
41
|
+
export function readDispatchSubagentId(ctx: any, event?: any): string | null {
|
|
42
|
+
const candidates: Array<unknown> = [
|
|
43
|
+
ctx?.metadata?.subagent_id,
|
|
44
|
+
ctx?.metadata?.dispatch?.subagent_id,
|
|
45
|
+
ctx?.params?.metadata?.subagent_id,
|
|
46
|
+
ctx?.payload?.metadata?.subagent_id,
|
|
47
|
+
ctx?.request?.metadata?.subagent_id,
|
|
48
|
+
ctx?.options?.metadata?.subagent_id,
|
|
49
|
+
ctx?.attachments?.subagent_id,
|
|
50
|
+
ctx?.dispatch?.subagent_id,
|
|
51
|
+
event?.metadata?.subagent_id,
|
|
52
|
+
event?.context?.metadata?.subagent_id,
|
|
53
|
+
];
|
|
54
|
+
for (const c of candidates) {
|
|
55
|
+
if (typeof c === "string" && c.length) return c;
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function buildStaffPersonaBlock(staff: SubagentRow): string {
|
|
61
|
+
const lines: string[] = [];
|
|
62
|
+
lines.push(`# STAFF IDENTITY`);
|
|
63
|
+
lines.push(
|
|
64
|
+
`You are **${staff.name}**${staff.role ? ` (${staff.role})` : ""}, a staff member reporting to chief \`${staff.chief_agent_id}\`.`,
|
|
65
|
+
);
|
|
66
|
+
lines.push(
|
|
67
|
+
`You are NOT the chief. Execute the assigned task, then return a structured report. Do not adopt the chief's persona.`,
|
|
68
|
+
);
|
|
69
|
+
if (staff.system_prompt?.trim()) {
|
|
70
|
+
lines.push(``, `## Persona`, staff.system_prompt.trim());
|
|
71
|
+
}
|
|
72
|
+
if (staff.mission?.trim()) {
|
|
73
|
+
lines.push(``, `## Mission`, staff.mission.trim());
|
|
74
|
+
}
|
|
75
|
+
if (staff.responsibilities?.trim()) {
|
|
76
|
+
lines.push(``, `## Responsibilities`, staff.responsibilities.trim());
|
|
77
|
+
}
|
|
78
|
+
if (staff.instructions?.trim()) {
|
|
79
|
+
lines.push(``, `## Operating Instructions`, staff.instructions.trim());
|
|
80
|
+
}
|
|
81
|
+
return lines.join("\n");
|
|
82
|
+
}
|
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
|
|
|
@@ -38,6 +39,42 @@ function err(message: string): ToolResult {
|
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
// ─── Subagent ↔ chief invariant ──────────────────────────────────────────────
|
|
43
|
+
// Mirrors dashboard/lib/subagentValidation.ts. Used by TASK_OPS + SCHEDULE_OPS
|
|
44
|
+
// when a chief delegates work to one of their staff via tool call.
|
|
45
|
+
//
|
|
46
|
+
// Returns:
|
|
47
|
+
// { ok: true, subagentId: <id> | null } — passed (or no subagent provided)
|
|
48
|
+
// { ok: false, reason: string } — invariant violated
|
|
49
|
+
async function validateSubagentForChief(
|
|
50
|
+
supabase: SupabaseClient,
|
|
51
|
+
userId: string,
|
|
52
|
+
subagentIdInput: unknown,
|
|
53
|
+
chiefAgentId: string | null | undefined,
|
|
54
|
+
): Promise<{ ok: true; subagentId: string | null } | { ok: false; reason: string }> {
|
|
55
|
+
const subagentId =
|
|
56
|
+
typeof subagentIdInput === "string" && subagentIdInput.trim()
|
|
57
|
+
? subagentIdInput.trim()
|
|
58
|
+
: null;
|
|
59
|
+
if (!subagentId) return { ok: true, subagentId: null };
|
|
60
|
+
|
|
61
|
+
const { data: sub } = await supabase
|
|
62
|
+
.from("agent_subagents")
|
|
63
|
+
.select("id, chief_agent_id")
|
|
64
|
+
.eq("user_id", userId)
|
|
65
|
+
.eq("id", subagentId)
|
|
66
|
+
.maybeSingle();
|
|
67
|
+
|
|
68
|
+
if (!sub) {
|
|
69
|
+
return { ok: false, reason: "subagent_not_found_or_not_yours" };
|
|
70
|
+
}
|
|
71
|
+
const chief = (chiefAgentId || "").trim();
|
|
72
|
+
if (!chief || sub.chief_agent_id !== chief) {
|
|
73
|
+
return { ok: false, reason: "subagent_chief_mismatch" };
|
|
74
|
+
}
|
|
75
|
+
return { ok: true, subagentId };
|
|
76
|
+
}
|
|
77
|
+
|
|
41
78
|
// ─── Helper: extract calling agent's accountId from OpenClaw context ─────────
|
|
42
79
|
|
|
43
80
|
// Module-level: set once at registration time from index.ts
|
|
@@ -205,8 +242,8 @@ function registerTaskOps(
|
|
|
205
242
|
`Actions:\n` +
|
|
206
243
|
`- "list": List/filter tasks. Optional: status, agent_id, space_id, folder_id, task_id, limit\n` +
|
|
207
244
|
`- "get": Get a single task by ID. Required: task_id\n` +
|
|
208
|
-
`- "create": Create a task. Required: title. Optional: agent_id, description, status, priority, space_id, folder_id, start_date, due_date, tags, instructions, execution_plan, goals, constraints, system_prompt, recurrence_type, recurrence_interval, scheduled_time. ⚠️ PLANNING GATE: If the task has 3+ execution steps, goals, or constraints, FIRST ask the user "Plan first or create directly?" If they choose to plan, use OFIERE_PLAN_OPS instead.\n` +
|
|
209
|
-
`- "update": Update a task. Required: task_id. Optional: all create fields + progress\n` +
|
|
245
|
+
`- "create": Create a task. Required: title. Optional: agent_id, subagent_id, description, status, priority, space_id, folder_id, start_date, due_date, tags, instructions, execution_plan, goals, constraints, system_prompt, recurrence_type, recurrence_interval, scheduled_time. ⚠️ PLANNING GATE: If the task has 3+ execution steps, goals, or constraints, FIRST ask the user "Plan first or create directly?" If they choose to plan, use OFIERE_PLAN_OPS instead.\n` +
|
|
246
|
+
`- "update": Update a task. Required: task_id. Optional: all create fields + progress + subagent_id\n` +
|
|
210
247
|
`- "delete": Delete task + subtasks. Required: task_id\n` +
|
|
211
248
|
`- "add_approval": Request approval on a task. Required: task_id, approver_name. Optional: approver_type (human|agent, auto-detected), due_date, comment\n` +
|
|
212
249
|
`- "list_approvals": List approvals. Optional: task_id, approval_status filter (pending|approved|rejected)\n` +
|
|
@@ -214,6 +251,7 @@ function registerTaskOps(
|
|
|
214
251
|
`For complex tasks, fill in execution_plan (step-by-step plan), goals, constraints, and system_prompt to help the executing agent.\n` +
|
|
215
252
|
`For simple tasks, just provide title and optionally description.\n` +
|
|
216
253
|
`agent_id: Pass your name to self-assign, another agent's name, or 'none'.\n` +
|
|
254
|
+
`subagent_id: When delegating to one of your own staff, pass the staff UUID here AND set agent_id to yourself (or the chief). The staff's chief_agent_id MUST match agent_id or the call rejects with subagent_chief_mismatch. Use OFIERE_AGENT_OPS list_subagents to discover available staff under a chief.\n` +
|
|
217
255
|
`For recurring tasks: set start_date + recurrence_type + recurrence_interval. Example: every 2 minutes = recurrence_type: "minutely", recurrence_interval: 2.\n` +
|
|
218
256
|
`Approvals: Use add_approval to request sign-off from humans or agents. Approvals are separate from workflow gate nodes.\n` +
|
|
219
257
|
`Status: PENDING, IN_PROGRESS, DONE, FAILED | Priority: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL`,
|
|
@@ -237,6 +275,10 @@ function registerTaskOps(
|
|
|
237
275
|
type: "string",
|
|
238
276
|
description: "Agent name or ID. Your name to self-assign, 'none' for unassigned.",
|
|
239
277
|
},
|
|
278
|
+
subagent_id: {
|
|
279
|
+
type: "string",
|
|
280
|
+
description: "Staff subagent UUID for delegation. Must belong to the chief in agent_id (verified server-side). Discover available staff via OFIERE_AGENT_OPS list_subagents.",
|
|
281
|
+
},
|
|
240
282
|
status: {
|
|
241
283
|
type: "string",
|
|
242
284
|
description: "Task status",
|
|
@@ -437,6 +479,13 @@ async function handleCreateTask(
|
|
|
437
479
|
|
|
438
480
|
const assignee = isUnassigned ? null : await resolveAgent(rawAgentId);
|
|
439
481
|
|
|
482
|
+
// ── Subagent delegation: validate staff↔chief invariant ──────────────
|
|
483
|
+
const subCheck = await validateSubagentForChief(supabase, userId, params.subagent_id, assignee);
|
|
484
|
+
if (!subCheck.ok) {
|
|
485
|
+
return err(subCheck.reason);
|
|
486
|
+
}
|
|
487
|
+
const subagentId = subCheck.subagentId;
|
|
488
|
+
|
|
440
489
|
// Build custom_fields from task-ops extended fields
|
|
441
490
|
const cf: Record<string, unknown> = {};
|
|
442
491
|
|
|
@@ -533,6 +582,7 @@ async function handleCreateTask(
|
|
|
533
582
|
title: params.title,
|
|
534
583
|
description: (params.description as string) || (params.instructions as string) || null,
|
|
535
584
|
agent_id: assignee,
|
|
585
|
+
subagent_id: subagentId,
|
|
536
586
|
assignee_type: "agent",
|
|
537
587
|
status: (params.status as string) || "PENDING",
|
|
538
588
|
priority: params.priority !== undefined ? params.priority : 1,
|
|
@@ -666,6 +716,7 @@ async function handleCreateTask(
|
|
|
666
716
|
user_id: userId,
|
|
667
717
|
task_id: id,
|
|
668
718
|
agent_id: effectiveAgentId,
|
|
719
|
+
subagent_id: subagentId,
|
|
669
720
|
title: params.title,
|
|
670
721
|
description: (params.description as string) || (params.instructions as string) || null,
|
|
671
722
|
scheduled_date: scheduledDateFinal,
|
|
@@ -743,6 +794,37 @@ async function handleUpdateTask(
|
|
|
743
794
|
}
|
|
744
795
|
if (params.status === "DONE") updates.completed_at = new Date().toISOString();
|
|
745
796
|
|
|
797
|
+
// ── Subagent + chief invariant ───────────────────────────────────────
|
|
798
|
+
// Mirrors dashboard PATCH: chief change without explicit subagent_id
|
|
799
|
+
// clears any existing subagent assignment; subagent_id alone validates
|
|
800
|
+
// against the row's current agent_id.
|
|
801
|
+
if (params.agent_id !== undefined || params.subagent_id !== undefined) {
|
|
802
|
+
if (params.agent_id !== undefined && params.subagent_id === undefined) {
|
|
803
|
+
// Chief switched, no explicit subagent — clear subagent_id.
|
|
804
|
+
updates.subagent_id = null;
|
|
805
|
+
} else {
|
|
806
|
+
let effectiveAgentId: string | null | undefined =
|
|
807
|
+
params.agent_id !== undefined ? (params.agent_id as string) : undefined;
|
|
808
|
+
if (effectiveAgentId === undefined) {
|
|
809
|
+
const { data: row } = await supabase
|
|
810
|
+
.from("tasks")
|
|
811
|
+
.select("agent_id")
|
|
812
|
+
.eq("id", params.task_id as string)
|
|
813
|
+
.eq("user_id", userId)
|
|
814
|
+
.maybeSingle();
|
|
815
|
+
effectiveAgentId = (row?.agent_id as string) ?? null;
|
|
816
|
+
}
|
|
817
|
+
const subCheck = await validateSubagentForChief(
|
|
818
|
+
supabase,
|
|
819
|
+
userId,
|
|
820
|
+
params.subagent_id,
|
|
821
|
+
effectiveAgentId,
|
|
822
|
+
);
|
|
823
|
+
if (!subCheck.ok) return err(subCheck.reason);
|
|
824
|
+
updates.subagent_id = subCheck.subagentId;
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
746
828
|
// If task is being marked DONE or FAILED, auto-complete any linked scheduler events
|
|
747
829
|
if (params.status === "DONE" || params.status === "FAILED") {
|
|
748
830
|
try {
|
|
@@ -1093,7 +1175,9 @@ function registerAgentOps(
|
|
|
1093
1175
|
`Actions:\n` +
|
|
1094
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` +
|
|
1095
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` +
|
|
1096
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` +
|
|
1097
1181
|
`- "delete_subagent": Remove a staff subagent. Required: subagent_id.\n` +
|
|
1098
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.`,
|
|
1099
1183
|
parameters: {
|
|
@@ -1103,15 +1187,29 @@ function registerAgentOps(
|
|
|
1103
1187
|
action: {
|
|
1104
1188
|
type: "string",
|
|
1105
1189
|
description: "The operation to perform",
|
|
1106
|
-
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"],
|
|
1107
1191
|
},
|
|
1108
1192
|
chief_agent_id: { type: "string", description: "Chief agent ID (required for list_subagents, create_subagent)" },
|
|
1109
|
-
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)" },
|
|
1110
1194
|
agent_id: { type: "string", description: "Agent ID (optional for invalidate_tier_cache — omit to flush all entries for the user)" },
|
|
1111
1195
|
name: { type: "string", description: "Subagent display name (required for create_subagent)" },
|
|
1112
1196
|
role: { type: "string", description: "Subagent role label, e.g. 'Staff', 'Analyst'. Defaults to 'Staff'." },
|
|
1113
1197
|
codename: { type: "string", description: "Optional subagent codename" },
|
|
1114
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
|
+
},
|
|
1115
1213
|
},
|
|
1116
1214
|
},
|
|
1117
1215
|
async execute(_id: string, params: Record<string, unknown>) {
|
|
@@ -1122,14 +1220,18 @@ function registerAgentOps(
|
|
|
1122
1220
|
return handleListAgents(api, supabase, userId, fallbackAgentId);
|
|
1123
1221
|
case "list_subagents":
|
|
1124
1222
|
return handleListSubagents(supabase, userId, params);
|
|
1223
|
+
case "get_subagent":
|
|
1224
|
+
return handleGetSubagent(supabase, userId, params);
|
|
1125
1225
|
case "create_subagent":
|
|
1126
1226
|
return handleCreateSubagent(supabase, userId, params);
|
|
1227
|
+
case "update_subagent":
|
|
1228
|
+
return handleUpdateSubagent(supabase, userId, params);
|
|
1127
1229
|
case "delete_subagent":
|
|
1128
1230
|
return handleDeleteSubagent(supabase, userId, params);
|
|
1129
1231
|
case "invalidate_tier_cache":
|
|
1130
1232
|
return handleInvalidateTierCache(userId, params);
|
|
1131
1233
|
default:
|
|
1132
|
-
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`);
|
|
1133
1235
|
}
|
|
1134
1236
|
},
|
|
1135
1237
|
});
|
|
@@ -1395,8 +1497,8 @@ function registerScheduleOps(
|
|
|
1395
1497
|
`Manage calendar events and schedule tasks on the timeline.\n\n` +
|
|
1396
1498
|
`Actions:\n` +
|
|
1397
1499
|
`- "list": List events. Optional: start_date, end_date, agent_id\n` +
|
|
1398
|
-
`- "create": Schedule an event. Required: title, scheduled_date. Optional: task_id, agent_id, scheduled_time, duration_minutes, recurrence_type, recurrence_interval, color, priority\n` +
|
|
1399
|
-
`- "update": Update event. Required: id. Optional: title, scheduled_date, scheduled_time, duration_minutes, status, recurrence_type\n` +
|
|
1500
|
+
`- "create": Schedule an event. Required: title, scheduled_date. Optional: task_id, agent_id, subagent_id, scheduled_time, duration_minutes, recurrence_type, recurrence_interval, color, priority\n` +
|
|
1501
|
+
`- "update": Update event. Required: id. Optional: title, scheduled_date, scheduled_time, duration_minutes, status, recurrence_type, agent_id, subagent_id\n` +
|
|
1400
1502
|
`- "delete": Remove event. Required: id\n` +
|
|
1401
1503
|
`recurrence_type: none, hourly, daily, weekly, monthly\n` +
|
|
1402
1504
|
`priority: 0=low, 1=medium, 2=high, 3=critical`,
|
|
@@ -1410,6 +1512,7 @@ function registerScheduleOps(
|
|
|
1410
1512
|
description: { type: "string" },
|
|
1411
1513
|
task_id: { type: "string", description: "Link to a task" },
|
|
1412
1514
|
agent_id: { type: "string", description: "Assigned agent" },
|
|
1515
|
+
subagent_id: { type: "string", description: "Staff subagent UUID for delegation. Must belong to the chief in agent_id (verified server-side)." },
|
|
1413
1516
|
scheduled_date: { type: "string", description: "Date (YYYY-MM-DD)" },
|
|
1414
1517
|
scheduled_time: { type: "string", description: "Time (HH:MM)" },
|
|
1415
1518
|
start_date: { type: "string", description: "List filter: start (YYYY-MM-DD)" },
|
|
@@ -1442,6 +1545,15 @@ function registerScheduleOps(
|
|
|
1442
1545
|
const pVal = typeof params.priority === "number" ? params.priority
|
|
1443
1546
|
: priorityMap[String(params.priority || "").toLowerCase()] ?? 0;
|
|
1444
1547
|
|
|
1548
|
+
// Validate subagent ↔ chief invariant before insert
|
|
1549
|
+
const evtSubCheck = await validateSubagentForChief(
|
|
1550
|
+
supabase,
|
|
1551
|
+
userId,
|
|
1552
|
+
params.subagent_id,
|
|
1553
|
+
(params.agent_id as string) || null,
|
|
1554
|
+
);
|
|
1555
|
+
if (!evtSubCheck.ok) return err(evtSubCheck.reason);
|
|
1556
|
+
|
|
1445
1557
|
// Compute next_run_at from scheduled_date + scheduled_time
|
|
1446
1558
|
let nextRunAt: number | null = null;
|
|
1447
1559
|
try {
|
|
@@ -1461,6 +1573,7 @@ function registerScheduleOps(
|
|
|
1461
1573
|
user_id: userId,
|
|
1462
1574
|
task_id: (params.task_id as string) || null,
|
|
1463
1575
|
agent_id: (params.agent_id as string) || null,
|
|
1576
|
+
subagent_id: evtSubCheck.subagentId,
|
|
1464
1577
|
title: params.title,
|
|
1465
1578
|
description: (params.description as string) || null,
|
|
1466
1579
|
scheduled_date: params.scheduled_date,
|
|
@@ -1498,6 +1611,33 @@ function registerScheduleOps(
|
|
|
1498
1611
|
"recurrence_type", "recurrence_interval", "status", "color", "priority", "agent_id"]) {
|
|
1499
1612
|
if ((params as any)[f] !== undefined) upd[f] = (params as any)[f];
|
|
1500
1613
|
}
|
|
1614
|
+
|
|
1615
|
+
// Subagent + chief invariant. Same rule as TASK_OPS update.
|
|
1616
|
+
if (params.agent_id !== undefined || params.subagent_id !== undefined) {
|
|
1617
|
+
if (params.agent_id !== undefined && params.subagent_id === undefined) {
|
|
1618
|
+
upd.subagent_id = null;
|
|
1619
|
+
} else {
|
|
1620
|
+
let effectiveAgent: string | null | undefined =
|
|
1621
|
+
params.agent_id !== undefined ? (params.agent_id as string) : undefined;
|
|
1622
|
+
if (effectiveAgent === undefined) {
|
|
1623
|
+
const { data: row } = await supabase
|
|
1624
|
+
.from("scheduler_events")
|
|
1625
|
+
.select("agent_id")
|
|
1626
|
+
.eq("id", params.id as string)
|
|
1627
|
+
.eq("user_id", userId)
|
|
1628
|
+
.maybeSingle();
|
|
1629
|
+
effectiveAgent = (row?.agent_id as string) ?? null;
|
|
1630
|
+
}
|
|
1631
|
+
const evtUpdSub = await validateSubagentForChief(
|
|
1632
|
+
supabase,
|
|
1633
|
+
userId,
|
|
1634
|
+
params.subagent_id,
|
|
1635
|
+
effectiveAgent,
|
|
1636
|
+
);
|
|
1637
|
+
if (!evtUpdSub.ok) return err(evtUpdSub.reason);
|
|
1638
|
+
upd.subagent_id = evtUpdSub.subagentId;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1501
1641
|
// Map string priority to number if provided
|
|
1502
1642
|
if (upd.priority !== undefined && typeof upd.priority === "string") {
|
|
1503
1643
|
const pMap: Record<string, number> = { low: 0, medium: 1, high: 2, critical: 3 };
|
|
@@ -5279,6 +5419,66 @@ async function handleDeleteSubagent(supabase: SupabaseClient, userId: string, pa
|
|
|
5279
5419
|
} catch (e) { return err(e instanceof Error ? e.message : String(e)); }
|
|
5280
5420
|
}
|
|
5281
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
|
+
|
|
5282
5482
|
async function handleSOPApplyTemplate(
|
|
5283
5483
|
supabase: SupabaseClient, userId: string,
|
|
5284
5484
|
resolveAgent: (id?: string) => Promise<string | null>,
|
|
@@ -6331,6 +6531,77 @@ function registerBrainExtractionHook(
|
|
|
6331
6531
|
|
|
6332
6532
|
api.logger.debug?.(`[ofiere-brain] agent_end identity: ctx.agentId=${ctx?.agentId || "(none)"} event.agentId=${event?.agentId || "(none)"} resolved=${resolvedAgentId}`);
|
|
6333
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
|
+
|
|
6334
6605
|
// ── Fast Stream: Write L1_focus + L2_episode in parallel ──
|
|
6335
6606
|
const rawContent = `User: ${lastUser}\nAssistant: ${lastAssistant}`;
|
|
6336
6607
|
const immediateWrites: PromiseLike<any>[] = [];
|