ofiere-openclaw-plugin 4.44.0 → 4.46.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 +43 -5
- package/src/staffPersona.ts +156 -1
- package/src/tools.ts +42 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.46.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,7 +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
|
+
import { loadSubagentRow, buildStaffPersonaBlock, readDispatchSubagentId, loadChiefStaffDefaults, loadDispatchContextBySession, resolveStaffModels, type SubagentRow, type DispatchContextRow } from "./staffPersona.js";
|
|
19
19
|
|
|
20
20
|
interface ToolResult {
|
|
21
21
|
content: Array<{ type: "text"; text: string }>;
|
|
@@ -427,15 +427,37 @@ export function registerAttachmentContextHook(args: {
|
|
|
427
427
|
// Cycle 7b — staff persona injection. Only fires when subagent_id is
|
|
428
428
|
// present in dispatch metadata (task-dispatcher / scheduler / explicit
|
|
429
429
|
// dispatch params). Plain user chats with the chief never persona-swap.
|
|
430
|
-
|
|
430
|
+
//
|
|
431
|
+
// Cycle 7b BUGSHOOT-1 (BUG 2) — OpenClaw gateway core rejects unknown
|
|
432
|
+
// root key `metadata` on chat.send, so the dispatcher can't ride
|
|
433
|
+
// metadata through the wire. After the metadata path comes up empty,
|
|
434
|
+
// try the dispatch_context table keyed by sessionKey. The metadata
|
|
435
|
+
// path is preferred so a future gateway upgrade keeps working.
|
|
436
|
+
const sessionKey: string | null =
|
|
437
|
+
ctx?.sessionKey || ctx?.session_key ||
|
|
438
|
+
(typeof _event === "object" ? (_event?.sessionKey || _event?.context?.sessionKey || null) : null) ||
|
|
439
|
+
null;
|
|
440
|
+
let dispatchCtxRow: DispatchContextRow | null = null;
|
|
441
|
+
let subagentId = readDispatchSubagentId(ctx);
|
|
442
|
+
if (!subagentId && sessionKey) {
|
|
443
|
+
dispatchCtxRow = await loadDispatchContextBySession(supabase, userId, sessionKey).catch(() => null);
|
|
444
|
+
if (dispatchCtxRow?.subagent_id) {
|
|
445
|
+
subagentId = dispatchCtxRow.subagent_id;
|
|
446
|
+
api.logger?.debug?.(
|
|
447
|
+
`[ofiere-staff] subagent_id resolved via dispatch_context (session=${sessionKey})`,
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
431
451
|
let staffPrefix = "";
|
|
432
452
|
let staffRow: SubagentRow | null = null;
|
|
433
453
|
if (subagentId) {
|
|
434
454
|
staffRow = await loadSubagentRow(supabase, userId, subagentId);
|
|
435
455
|
if (staffRow && staffRow.chief_agent_id === resolvedAgentId) {
|
|
436
|
-
|
|
456
|
+
const chiefDefaults = await loadChiefStaffDefaults(supabase, userId, staffRow.chief_agent_id).catch(() => null);
|
|
457
|
+
const resolved = resolveStaffModels(staffRow, chiefDefaults);
|
|
458
|
+
staffPrefix = buildStaffPersonaBlock(staffRow, resolved) + "\n\n---\n\n";
|
|
437
459
|
api.logger?.debug?.(
|
|
438
|
-
`[ofiere-staff] subagent ${subagentId} (${staffRow.name}) reporting to ${resolvedAgentId} — persona injected`,
|
|
460
|
+
`[ofiere-staff] subagent ${subagentId} (${staffRow.name}) reporting to ${resolvedAgentId} — persona injected; resolved models: ${JSON.stringify(resolved)}`,
|
|
439
461
|
);
|
|
440
462
|
} else if (staffRow) {
|
|
441
463
|
api.logger?.warn?.(
|
|
@@ -458,7 +480,23 @@ export function registerAttachmentContextHook(args: {
|
|
|
458
480
|
// can stash explicit `attached_sop_ids` / `attached_framework_ids` on the
|
|
459
481
|
// chat.send frame's metadata. When present, prefer them over the
|
|
460
482
|
// most-recent-conversation lookup and bypass cache (per-dispatch ids).
|
|
461
|
-
|
|
483
|
+
let dispatchIds = readDispatchAttachmentIds(ctx);
|
|
484
|
+
// Cycle 7b BUGSHOOT-1 (BUG 2) — DB-backed dispatch-context fallback for
|
|
485
|
+
// attachment ids. Same rationale as the subagent_id fallback above.
|
|
486
|
+
if (!dispatchIds.sopIds.length && !dispatchIds.fwIds.length && sessionKey) {
|
|
487
|
+
if (!dispatchCtxRow) {
|
|
488
|
+
dispatchCtxRow = await loadDispatchContextBySession(supabase, userId, sessionKey).catch(() => null);
|
|
489
|
+
}
|
|
490
|
+
if (dispatchCtxRow && (dispatchCtxRow.attached_sop_ids.length || dispatchCtxRow.attached_framework_ids.length)) {
|
|
491
|
+
dispatchIds = {
|
|
492
|
+
sopIds: dispatchCtxRow.attached_sop_ids,
|
|
493
|
+
fwIds: dispatchCtxRow.attached_framework_ids,
|
|
494
|
+
};
|
|
495
|
+
api.logger?.debug?.(
|
|
496
|
+
`[ofiere-attach] dispatch ids via dispatch_context (session=${sessionKey}) sops=${dispatchIds.sopIds.length} fws=${dispatchIds.fwIds.length}`,
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
462
500
|
if (dispatchIds.sopIds.length || dispatchIds.fwIds.length) {
|
|
463
501
|
api.logger?.debug?.(
|
|
464
502
|
`[ofiere-attach] dispatch ids agent=${resolvedAgentId} sops=${dispatchIds.sopIds.length} fws=${dispatchIds.fwIds.length}`,
|
package/src/staffPersona.ts
CHANGED
|
@@ -59,7 +59,149 @@ export function readDispatchSubagentId(ctx: any, event?: any): string | null {
|
|
|
59
59
|
return null;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
// Cycle 7b BUGSHOOT-1 (BUG 2) — DB-backed dispatch context fallback.
|
|
63
|
+
// OpenClaw gateway core rejects unknown root key `metadata` on chat.send, so
|
|
64
|
+
// the task-dispatcher edge function can't ride dispatch metadata through the
|
|
65
|
+
// wire. The dispatcher writes a row to public.dispatch_context keyed by
|
|
66
|
+
// (user_id, session_key) before chat.send fires; this loader reads it back
|
|
67
|
+
// during before_prompt_build when the metadata path comes up empty.
|
|
68
|
+
//
|
|
69
|
+
// Returns the dispatch context for the most recent matching row, or null if
|
|
70
|
+
// none exists. The metadata path is preferred so a future gateway upgrade
|
|
71
|
+
// that natively accepts metadata keeps working with no flag day.
|
|
72
|
+
export interface DispatchContextRow {
|
|
73
|
+
subagent_id: string | null;
|
|
74
|
+
attached_sop_ids: string[];
|
|
75
|
+
attached_framework_ids: string[];
|
|
76
|
+
task_id: string | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function loadDispatchContextBySession(
|
|
80
|
+
supabase: SupabaseClient,
|
|
81
|
+
userId: string,
|
|
82
|
+
sessionKey: string,
|
|
83
|
+
): Promise<DispatchContextRow | null> {
|
|
84
|
+
if (!sessionKey) return null;
|
|
85
|
+
const { data, error } = await supabase
|
|
86
|
+
.from("dispatch_context")
|
|
87
|
+
.select("subagent_id, attached_sop_ids, attached_framework_ids, task_id")
|
|
88
|
+
.eq("user_id", userId)
|
|
89
|
+
.eq("session_key", sessionKey)
|
|
90
|
+
.order("created_at", { ascending: false })
|
|
91
|
+
.limit(1)
|
|
92
|
+
.maybeSingle();
|
|
93
|
+
if (error || !data) return null;
|
|
94
|
+
return {
|
|
95
|
+
subagent_id: (data.subagent_id as string | null) ?? null,
|
|
96
|
+
attached_sop_ids: Array.isArray(data.attached_sop_ids)
|
|
97
|
+
? (data.attached_sop_ids as unknown[]).filter((x): x is string => typeof x === "string")
|
|
98
|
+
: [],
|
|
99
|
+
attached_framework_ids: Array.isArray(data.attached_framework_ids)
|
|
100
|
+
? (data.attached_framework_ids as unknown[]).filter((x): x is string => typeof x === "string")
|
|
101
|
+
: [],
|
|
102
|
+
task_id: (data.task_id as string | null) ?? null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type StaffModelSlot =
|
|
107
|
+
| "primary_model"
|
|
108
|
+
| "coding_model"
|
|
109
|
+
| "tool_call_model"
|
|
110
|
+
| "function_call_model"
|
|
111
|
+
| "vision_model";
|
|
112
|
+
|
|
113
|
+
export interface ChiefStaffDefaultsRow {
|
|
114
|
+
chief_agent_id: string;
|
|
115
|
+
primary_model: string | null;
|
|
116
|
+
coding_model: string | null;
|
|
117
|
+
tool_call_model: string | null;
|
|
118
|
+
function_call_model: string | null;
|
|
119
|
+
vision_model: string | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export interface ResolvedStaffModels {
|
|
123
|
+
primary_model: string | null;
|
|
124
|
+
coding_model: string | null;
|
|
125
|
+
tool_call_model: string | null;
|
|
126
|
+
function_call_model: string | null;
|
|
127
|
+
vision_model: string | null;
|
|
128
|
+
source: Record<StaffModelSlot, "staff" | "chief_default" | "chief_fallback" | null>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const SLOTS: StaffModelSlot[] = [
|
|
132
|
+
"primary_model",
|
|
133
|
+
"coding_model",
|
|
134
|
+
"tool_call_model",
|
|
135
|
+
"function_call_model",
|
|
136
|
+
"vision_model",
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
export async function loadChiefStaffDefaults(
|
|
140
|
+
supabase: SupabaseClient,
|
|
141
|
+
userId: string,
|
|
142
|
+
chiefAgentId: string,
|
|
143
|
+
): Promise<ChiefStaffDefaultsRow | null> {
|
|
144
|
+
const { data, error } = await supabase
|
|
145
|
+
.from("chief_staff_defaults")
|
|
146
|
+
.select(
|
|
147
|
+
"chief_agent_id, primary_model, coding_model, tool_call_model, function_call_model, vision_model",
|
|
148
|
+
)
|
|
149
|
+
.eq("user_id", userId)
|
|
150
|
+
.eq("chief_agent_id", chiefAgentId)
|
|
151
|
+
.maybeSingle();
|
|
152
|
+
if (error || !data) return null;
|
|
153
|
+
return data as ChiefStaffDefaultsRow;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Resolution chain at delegate_to_staff time:
|
|
158
|
+
* staff.<slot> (per-staff override on agent_subagents)
|
|
159
|
+
* ?? chief_staff_defaults.<slot> (chief-level "STAFF MODEL")
|
|
160
|
+
* ?? null (caller falls back to the chief's own model — that lookup
|
|
161
|
+
* happens in the OpenClaw runtime, not here).
|
|
162
|
+
*
|
|
163
|
+
* The chief's own model is intentionally not touched. Empty staff slot +
|
|
164
|
+
* empty chief STAFF MODEL = inherit chief's primary_model (gateway default).
|
|
165
|
+
*/
|
|
166
|
+
export function resolveStaffModels(
|
|
167
|
+
staff: SubagentRow,
|
|
168
|
+
chiefDefaults: ChiefStaffDefaultsRow | null,
|
|
169
|
+
): ResolvedStaffModels {
|
|
170
|
+
const out: ResolvedStaffModels = {
|
|
171
|
+
primary_model: null,
|
|
172
|
+
coding_model: null,
|
|
173
|
+
tool_call_model: null,
|
|
174
|
+
function_call_model: null,
|
|
175
|
+
vision_model: null,
|
|
176
|
+
source: {
|
|
177
|
+
primary_model: null,
|
|
178
|
+
coding_model: null,
|
|
179
|
+
tool_call_model: null,
|
|
180
|
+
function_call_model: null,
|
|
181
|
+
vision_model: null,
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
for (const slot of SLOTS) {
|
|
185
|
+
const staffVal = (staff as any)[slot] as string | null | undefined;
|
|
186
|
+
const chiefVal = chiefDefaults ? (chiefDefaults as any)[slot] as string | null | undefined : null;
|
|
187
|
+
if (staffVal && staffVal.trim()) {
|
|
188
|
+
out[slot] = staffVal;
|
|
189
|
+
out.source[slot] = "staff";
|
|
190
|
+
} else if (chiefVal && chiefVal.trim()) {
|
|
191
|
+
out[slot] = chiefVal;
|
|
192
|
+
out.source[slot] = "chief_default";
|
|
193
|
+
} else {
|
|
194
|
+
out[slot] = null;
|
|
195
|
+
out.source[slot] = "chief_fallback";
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function buildStaffPersonaBlock(
|
|
202
|
+
staff: SubagentRow,
|
|
203
|
+
resolved?: ResolvedStaffModels,
|
|
204
|
+
): string {
|
|
63
205
|
const lines: string[] = [];
|
|
64
206
|
lines.push(`# STAFF IDENTITY`);
|
|
65
207
|
lines.push(
|
|
@@ -80,5 +222,18 @@ export function buildStaffPersonaBlock(staff: SubagentRow): string {
|
|
|
80
222
|
if (staff.instructions?.trim()) {
|
|
81
223
|
lines.push(``, `## Operating Instructions`, staff.instructions.trim());
|
|
82
224
|
}
|
|
225
|
+
if (resolved && (resolved.primary_model || resolved.coding_model || resolved.tool_call_model || resolved.function_call_model || resolved.vision_model)) {
|
|
226
|
+
const sourceLabel = (s: "staff" | "chief_default" | "chief_fallback" | null) =>
|
|
227
|
+
s === "staff" ? "per-staff override" : s === "chief_default" ? "chief STAFF MODEL" : "inherits chief";
|
|
228
|
+
const rows: string[] = [];
|
|
229
|
+
if (resolved.primary_model) rows.push(`- primary: \`${resolved.primary_model}\` (${sourceLabel(resolved.source.primary_model)})`);
|
|
230
|
+
if (resolved.coding_model) rows.push(`- coding: \`${resolved.coding_model}\` (${sourceLabel(resolved.source.coding_model)})`);
|
|
231
|
+
if (resolved.tool_call_model) rows.push(`- tool_call: \`${resolved.tool_call_model}\` (${sourceLabel(resolved.source.tool_call_model)})`);
|
|
232
|
+
if (resolved.function_call_model) rows.push(`- function_call: \`${resolved.function_call_model}\` (${sourceLabel(resolved.source.function_call_model)})`);
|
|
233
|
+
if (resolved.vision_model) rows.push(`- vision: \`${resolved.vision_model}\` (${sourceLabel(resolved.source.vision_model)})`);
|
|
234
|
+
if (rows.length) {
|
|
235
|
+
lines.push(``, `## Configured Models`, ...rows);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
83
238
|
return lines.join("\n");
|
|
84
239
|
}
|
package/src/tools.ts
CHANGED
|
@@ -653,6 +653,12 @@ async function handleCreateTask(
|
|
|
653
653
|
if (!isNaN(parsedDate.getTime())) {
|
|
654
654
|
// Valid date — check if it includes a meaningful time component
|
|
655
655
|
const hasTimeInfo = /[T ]\d{2}:\d{2}/.test(startDate);
|
|
656
|
+
// BUG 4 fix (cycle 7b BUGSHOOT-1): if the input string already carries
|
|
657
|
+
// an explicit UTC marker (Z, +00, +0000, +00:00, UTC), the time
|
|
658
|
+
// component is NOT user-local — skip the local→UTC subtraction.
|
|
659
|
+
// Without this, a UTC-Z input like "2026-05-10T16:25:00Z" got mistaken
|
|
660
|
+
// for local 16:25 and stored as 09:25Z (off by TZ_OFFSET_HOURS).
|
|
661
|
+
const isUtcInput = /Z$|[+-]00:?00$|UTC$/i.test(startDate.trim());
|
|
656
662
|
|
|
657
663
|
if (explicitScheduledTime) {
|
|
658
664
|
// Agent explicitly passed a scheduled_time — treat as user’s local time
|
|
@@ -666,6 +672,15 @@ async function handleCreateTask(
|
|
|
666
672
|
scheduledDateFinal = dateStr;
|
|
667
673
|
// Normalize start_date to ISO UTC for consistent downstream parsing
|
|
668
674
|
insertData.start_date = dt.toISOString();
|
|
675
|
+
} else if (isUtcInput) {
|
|
676
|
+
// Input is already UTC — no local→UTC conversion. Use parsedDate
|
|
677
|
+
// directly. scheduled_time is documented as user-local, so add the
|
|
678
|
+
// offset to recover the local-clock value.
|
|
679
|
+
nextRunAtEpoch = Math.floor(parsedDate.getTime() / 1000);
|
|
680
|
+
const localHour = (parsedDate.getUTCHours() + TZ_OFFSET_HOURS + 24) % 24;
|
|
681
|
+
scheduledTimeFinal = `${String(localHour).padStart(2, "0")}:${String(parsedDate.getUTCMinutes()).padStart(2, "0")}`;
|
|
682
|
+
scheduledDateFinal = parsedDate.toISOString().split("T")[0];
|
|
683
|
+
insertData.start_date = parsedDate.toISOString();
|
|
669
684
|
} else if (hasTimeInfo) {
|
|
670
685
|
// start_date already contains time — assume it’s in the user’s local timezone
|
|
671
686
|
// Extract the local hour:minute from the string, NOT from UTC parsing
|
|
@@ -5387,14 +5402,36 @@ async function handleCreateSubagent(supabase: SupabaseClient, userId: string, pa
|
|
|
5387
5402
|
return err(`Maximum ${MAX_SUBAGENTS_PER_CHIEF} subagents per department chief`);
|
|
5388
5403
|
}
|
|
5389
5404
|
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5405
|
+
// BUG 1 fix (cycle 7b BUGSHOOT-1): persist persona/model/MCP fields on
|
|
5406
|
+
// create. Schema accepts them (see SUBAGENT_INPUT_SCHEMA above) but the
|
|
5407
|
+
// previous insert dropped everything except identity/UI fields, forcing a
|
|
5408
|
+
// follow-up update_subagent. Mirror the field whitelist used in
|
|
5409
|
+
// handleUpdateSubagent so create/update stay in lockstep.
|
|
5410
|
+
const insertRow: Record<string, unknown> = {
|
|
5411
|
+
user_id: userId,
|
|
5412
|
+
chief_agent_id: chiefAgentId,
|
|
5413
|
+
name,
|
|
5414
|
+
role: (params.role as string) || "Staff",
|
|
5415
|
+
codename: (params.codename as string) ?? null,
|
|
5416
|
+
color_hex: (params.color_hex as string) || "#64748b",
|
|
5417
|
+
};
|
|
5418
|
+
const personaFields = [
|
|
5419
|
+
"system_prompt", "mission", "responsibilities", "instructions",
|
|
5420
|
+
"primary_model", "coding_model", "tool_call_model", "function_call_model", "vision_model",
|
|
5421
|
+
] as const;
|
|
5422
|
+
for (const key of personaFields) {
|
|
5423
|
+
if (key in params && params[key] !== undefined) insertRow[key] = params[key];
|
|
5424
|
+
}
|
|
5425
|
+
if ("mcp_server_ids" in params && Array.isArray(params.mcp_server_ids)) {
|
|
5426
|
+
insertRow.mcp_server_ids = (params.mcp_server_ids as unknown[]).filter(
|
|
5427
|
+
(x): x is string => typeof x === "string",
|
|
5428
|
+
);
|
|
5429
|
+
}
|
|
5393
5430
|
|
|
5394
5431
|
const { data, error } = await supabase
|
|
5395
5432
|
.from("agent_subagents")
|
|
5396
|
-
.insert(
|
|
5397
|
-
.select(
|
|
5433
|
+
.insert(insertRow)
|
|
5434
|
+
.select()
|
|
5398
5435
|
.single();
|
|
5399
5436
|
if (error) return err(error.message);
|
|
5400
5437
|
return ok({ subagent: data, message: `Subagent "${name}" created under chief ${chiefAgentId}` });
|