ofiere-openclaw-plugin 4.18.5 → 4.19.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.18.5",
3
+ "version": "4.19.1",
4
4
  "type": "module",
5
- "description": "OpenClaw plugin for Ofiere PM - 12 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, and execution plan builder",
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",
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
@@ -130,6 +130,19 @@ const TOOL_DOCS: Record<string, string> = {
130
130
  - Plans are visual DAG drafts — they don't become real tasks until you call "execute"
131
131
  - Execution maps ALL node fields into real tasks: execution_steps → custom_fields.execution_plan, goals → custom_fields.goals, constraints → custom_fields.constraints, system_prompt → custom_fields.system_prompt
132
132
  - The user can see and edit your plans in the Planning Tab of the dashboard in real-time`,
133
+
134
+ OFIERE_SOP_OPS: `- **OFIERE_SOP_OPS** — Standard Operating Procedures for department chiefs (action: "list_templates", "create", "list", "get", "update", "delete", "list_subagents", "apply_template")
135
+ - list_templates: See available SOP templates (built-in + user-created)
136
+ - create: Create a new SOP. Required: agent_id, title, sop_data. Optional: department, status
137
+ - list: List SOPs. Optional: agent_id to filter by department chief
138
+ - get: Full SOP details with structured content. Required: sop_id
139
+ - update: Modify SOP content/status. Required: sop_id. Optional: title, sop_data, status, department
140
+ - delete: Remove an SOP. Required: sop_id
141
+ - list_subagents: View staff under a chief. Required: chief_agent_id
142
+ - apply_template: Create SOP from a saved template. Required: agent_id, template_id
143
+ - sop_data is a JSON object: { title, objective, scope, prerequisites[{text,checked}], steps[{name,action,owner,output}], deliverables[], escalationRules[{trigger,escalateTo,priority}], successCriteria[{text,checked}], notes }
144
+ - Status values: draft, active, archived
145
+ - SOPs appear in the SOP Manager page immediately via real-time sync`,
133
146
  };
134
147
 
135
148
  export function getSystemPrompt(state: {
@@ -195,6 +208,12 @@ ${toolDocs}
195
208
  - When creating a plan with nodes, nest children inside each node's children[] array. Sequential children execute in order; set parallel: true on a parent node to fork its children into parallel branches.
196
209
  - Always call OFIERE_PLAN_OPS action:"get" before action:"add_nodes" or action:"update" to see the current tree structure and node IDs.
197
210
  - Execution ("execute") maps plan nodes 1:1 into real PM tasks with ALL enrichment fields preserved: execution_steps, goals, constraints, system_prompt. No data is lost in the handoff.
211
+ - SOP CREATION: When asked to create an SOP, use OFIERE_SOP_OPS action:"create" with a COMPLETE sop_data object. Fill ALL fields with actionable, department-specific content — do NOT leave fields empty.
212
+ - Each SOP step MUST have: a clear name, a specific action description, an assigned owner, and a concrete expected output.
213
+ - Include escalation rules with appropriate priority levels (P1=critical blockers, P2=scope changes, P3=advisory).
214
+ - When creating SOPs for department chiefs (Thalia=CMO, Ivy=COO, Daisy=CTO-Intel, Celia=CTO-Eng), tailor content to their domain expertise.
215
+ - Prerequisites should be actionable checklist items. Success criteria should be measurable outcomes.
216
+ - After creating an SOP, suggest the agent set it to "active" status when ready for execution.
198
217
  </ofiere-pm>`;
199
218
  }
200
219
 
package/src/tools.ts CHANGED
@@ -4514,6 +4514,338 @@ async function handleExecutePlan(
4514
4514
  } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
4515
4515
  }
4516
4516
 
4517
+ // ═══════════════════════════════════════════════════════════════════════════════
4518
+ // META-TOOL 13: OFIERE_SOP_OPS — Standard Operating Procedures
4519
+ // ═══════════════════════════════════════════════════════════════════════════════
4520
+
4521
+ function registerSOPOps(
4522
+ api: any,
4523
+ supabase: SupabaseClient,
4524
+ userId: string,
4525
+ resolveAgent: (id?: string) => Promise<string | null>,
4526
+ ): void {
4527
+ api.registerTool({
4528
+ name: "OFIERE_SOP_OPS",
4529
+ label: "Ofiere SOP Operations",
4530
+ description:
4531
+ `Create, manage, and deploy Standard Operating Procedures for department chiefs.\n\n` +
4532
+ `Actions:\n` +
4533
+ `- "list_templates": List available SOP templates\n` +
4534
+ `- "create": Create a new SOP for an agent. Required: agent_id, title, sop_data. Optional: department, status\n` +
4535
+ `- "list": List SOPs. Optional: agent_id to filter\n` +
4536
+ `- "get": Get full SOP details. Required: sop_id\n` +
4537
+ `- "update": Modify SOP. Required: sop_id. Optional: title, sop_data, status, department\n` +
4538
+ `- "delete": Remove SOP. Required: sop_id\n` +
4539
+ `- "list_subagents": List subagents for a chief. Required: chief_agent_id\n` +
4540
+ `- "apply_template": Create SOP from template. Required: agent_id, template_id. Optional: title, department\n\n` +
4541
+ `sop_data structure (JSON object):\n` +
4542
+ `{\n` +
4543
+ ` title: string,\n` +
4544
+ ` objective: string (purpose and expected outcome),\n` +
4545
+ ` scope: string (department or domain),\n` +
4546
+ ` prerequisites: [{ text: string, checked: boolean }],\n` +
4547
+ ` steps: [{ id: string, name: string, action: string, owner: string, output: string }],\n` +
4548
+ ` deliverables: [string],\n` +
4549
+ ` escalationRules: [{ trigger: string, escalateTo: string, priority: "P1"|"P2"|"P3" }],\n` +
4550
+ ` successCriteria: [{ text: string, checked: boolean }],\n` +
4551
+ ` notes: string\n` +
4552
+ `}\n\n` +
4553
+ `Status values: "draft", "active", "archived"`,
4554
+ parameters: {
4555
+ type: "object",
4556
+ required: ["action"],
4557
+ properties: {
4558
+ action: { type: "string", enum: ["list_templates", "create", "list", "get", "update", "delete", "list_subagents", "apply_template"], description: "Valid actions: list_templates, create, list, get, update, delete, list_subagents, apply_template" },
4559
+ sop_id: { type: "string", description: "SOP ID (required for get, update, delete)" },
4560
+ agent_id: { type: "string", description: "Agent ID (required for create, list filter, apply_template)" },
4561
+ chief_agent_id: { type: "string", description: "Chief agent ID (required for list_subagents)" },
4562
+ template_id: { type: "string", description: "Template ID (required for apply_template)" },
4563
+ title: { type: "string", description: "SOP title" },
4564
+ department: { type: "string", description: "Department name (e.g. Marketing & Revenue)" },
4565
+ status: { type: "string", enum: ["draft", "active", "archived"], description: "SOP status" },
4566
+ sop_data: {
4567
+ type: "object",
4568
+ description: "Structured SOP content — pass a complete SOPData JSON object",
4569
+ properties: {
4570
+ title: { type: "string" },
4571
+ objective: { type: "string" },
4572
+ scope: { type: "string" },
4573
+ prerequisites: { type: "array", items: { type: "object", properties: { text: { type: "string" }, checked: { type: "boolean" } }, required: ["text"] } },
4574
+ steps: { type: "array", items: { type: "object", properties: { id: { type: "string" }, name: { type: "string" }, action: { type: "string" }, owner: { type: "string" }, output: { type: "string" } }, required: ["name", "action"] } },
4575
+ deliverables: { type: "array", items: { type: "string" } },
4576
+ escalationRules: { type: "array", items: { type: "object", properties: { trigger: { type: "string" }, escalateTo: { type: "string" }, priority: { type: "string", enum: ["P1", "P2", "P3"] } }, required: ["trigger", "escalateTo"] } },
4577
+ successCriteria: { type: "array", items: { type: "object", properties: { text: { type: "string" }, checked: { type: "boolean" } }, required: ["text"] } },
4578
+ notes: { type: "string" },
4579
+ },
4580
+ },
4581
+ },
4582
+ },
4583
+ async execute(_id: string, params: Record<string, unknown>) {
4584
+ const action = params.action as string;
4585
+ switch (action) {
4586
+ case "list_templates": return handleSOPListTemplates(supabase, userId);
4587
+ case "create": return handleSOPCreate(supabase, userId, resolveAgent, params);
4588
+ case "list": return handleSOPList(supabase, userId, params);
4589
+ case "get": return handleSOPGet(supabase, userId, params);
4590
+ case "update": return handleSOPUpdate(supabase, userId, params);
4591
+ case "delete": return handleSOPDelete(supabase, userId, params);
4592
+ case "list_subagents": return handleSOPListSubagents(supabase, userId, params);
4593
+ case "apply_template": return handleSOPApplyTemplate(supabase, userId, resolveAgent, params);
4594
+ default: return err(`Unknown action "${action}". Valid: list_templates, create, list, get, update, delete, list_subagents, apply_template`);
4595
+ }
4596
+ },
4597
+ });
4598
+ }
4599
+
4600
+ // ── SOP action handlers ──────────────────────────────────────────────────────
4601
+
4602
+ async function handleSOPListTemplates(supabase: SupabaseClient, userId: string): Promise<ToolResult> {
4603
+ try {
4604
+ const { data, error } = await supabase
4605
+ .from("sop_templates")
4606
+ .select("id, name, description, is_default, created_at")
4607
+ .eq("user_id", userId)
4608
+ .order("created_at", { ascending: false });
4609
+ if (error) return err(error.message);
4610
+ return ok({ templates: data || [], count: (data || []).length });
4611
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
4612
+ }
4613
+
4614
+ async function handleSOPCreate(
4615
+ supabase: SupabaseClient, userId: string,
4616
+ resolveAgent: (id?: string) => Promise<string | null>,
4617
+ params: Record<string, unknown>,
4618
+ ): Promise<ToolResult> {
4619
+ try {
4620
+ const agentIdRaw = params.agent_id as string;
4621
+ if (!agentIdRaw) return err("Missing required field: agent_id");
4622
+ const title = params.title as string;
4623
+ if (!title) return err("Missing required field: title");
4624
+
4625
+ // Build the SOPData JSON
4626
+ const sopDataRaw = params.sop_data as Record<string, unknown> | undefined;
4627
+ let sopContent: string;
4628
+
4629
+ if (sopDataRaw) {
4630
+ // Ensure each step has an id
4631
+ const steps = Array.isArray(sopDataRaw.steps)
4632
+ ? (sopDataRaw.steps as any[]).map((s: any, i: number) => ({
4633
+ id: s.id || `step-${Date.now()}-${i}`,
4634
+ name: s.name || "",
4635
+ action: s.action || "",
4636
+ owner: s.owner || "",
4637
+ output: s.output || "",
4638
+ }))
4639
+ : [];
4640
+ const prerequisites = Array.isArray(sopDataRaw.prerequisites)
4641
+ ? (sopDataRaw.prerequisites as any[]).map((p: any) => ({ text: p.text || "", checked: !!p.checked }))
4642
+ : [];
4643
+ const successCriteria = Array.isArray(sopDataRaw.successCriteria)
4644
+ ? (sopDataRaw.successCriteria as any[]).map((c: any) => ({ text: c.text || "", checked: !!c.checked }))
4645
+ : [];
4646
+ const escalationRules = Array.isArray(sopDataRaw.escalationRules)
4647
+ ? (sopDataRaw.escalationRules as any[]).map((r: any) => ({
4648
+ trigger: r.trigger || "",
4649
+ escalateTo: r.escalateTo || r.escalate_to || "",
4650
+ priority: (r.priority || "P2") as "P1" | "P2" | "P3",
4651
+ }))
4652
+ : [];
4653
+ const deliverables = Array.isArray(sopDataRaw.deliverables)
4654
+ ? (sopDataRaw.deliverables as string[])
4655
+ : [];
4656
+
4657
+ const sopData = {
4658
+ title: (sopDataRaw.title as string) || title,
4659
+ objective: (sopDataRaw.objective as string) || "",
4660
+ scope: (sopDataRaw.scope as string) || "",
4661
+ prerequisites,
4662
+ steps,
4663
+ deliverables,
4664
+ escalationRules,
4665
+ successCriteria,
4666
+ notes: (sopDataRaw.notes as string) || "",
4667
+ };
4668
+ sopContent = JSON.stringify(sopData);
4669
+ } else {
4670
+ // Create empty SOP data
4671
+ sopContent = JSON.stringify({
4672
+ title, objective: "", scope: "", prerequisites: [],
4673
+ steps: [{ id: `step-${Date.now()}-0`, name: "", action: "", owner: "", output: "" }],
4674
+ deliverables: [], escalationRules: [], successCriteria: [], notes: "",
4675
+ });
4676
+ }
4677
+
4678
+ const row = {
4679
+ user_id: userId,
4680
+ agent_id: agentIdRaw,
4681
+ title,
4682
+ content: sopContent,
4683
+ status: (params.status as string) || "draft",
4684
+ department: (params.department as string) || null,
4685
+ };
4686
+
4687
+ const { data, error } = await supabase.from("agent_sops").insert(row).select().single();
4688
+ if (error) return err(error.message);
4689
+
4690
+ return ok({
4691
+ message: `SOP "${title}" created for agent ${agentIdRaw}`,
4692
+ sop: { id: data.id, title: data.title, status: data.status, agent_id: data.agent_id, department: data.department },
4693
+ });
4694
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
4695
+ }
4696
+
4697
+ async function handleSOPList(supabase: SupabaseClient, userId: string, params: Record<string, unknown>): Promise<ToolResult> {
4698
+ try {
4699
+ let query = supabase
4700
+ .from("agent_sops")
4701
+ .select("id, agent_id, title, status, department, created_at, updated_at")
4702
+ .eq("user_id", userId)
4703
+ .order("updated_at", { ascending: false });
4704
+ if (params.agent_id) query = query.eq("agent_id", params.agent_id as string);
4705
+ const { data, error } = await query;
4706
+ if (error) return err(error.message);
4707
+ return ok({ sops: data || [], count: (data || []).length });
4708
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
4709
+ }
4710
+
4711
+ async function handleSOPGet(supabase: SupabaseClient, userId: string, params: Record<string, unknown>): Promise<ToolResult> {
4712
+ try {
4713
+ if (!params.sop_id) return err("Missing required field: sop_id");
4714
+ const { data, error } = await supabase
4715
+ .from("agent_sops")
4716
+ .select("*")
4717
+ .eq("id", params.sop_id as string)
4718
+ .eq("user_id", userId)
4719
+ .maybeSingle();
4720
+ if (error) return err(error.message);
4721
+ if (!data) return err("SOP not found");
4722
+
4723
+ // Parse the content to return structured data
4724
+ let parsedContent: any = {};
4725
+ try { parsedContent = JSON.parse(data.content || "{}"); } catch { /* leave empty */ }
4726
+
4727
+ return ok({ sop: { ...data, content: parsedContent } });
4728
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
4729
+ }
4730
+
4731
+ async function handleSOPUpdate(supabase: SupabaseClient, userId: string, params: Record<string, unknown>): Promise<ToolResult> {
4732
+ try {
4733
+ if (!params.sop_id) return err("Missing required field: sop_id");
4734
+ const updates: Record<string, any> = { updated_at: new Date().toISOString() };
4735
+ if (params.title !== undefined) updates.title = params.title;
4736
+ if (params.status !== undefined) updates.status = params.status;
4737
+ if (params.department !== undefined) updates.department = params.department;
4738
+
4739
+ if (params.sop_data) {
4740
+ const sopDataRaw = params.sop_data as Record<string, unknown>;
4741
+ const steps = Array.isArray(sopDataRaw.steps)
4742
+ ? (sopDataRaw.steps as any[]).map((s: any, i: number) => ({
4743
+ id: s.id || `step-${Date.now()}-${i}`,
4744
+ name: s.name || "", action: s.action || "", owner: s.owner || "", output: s.output || "",
4745
+ }))
4746
+ : [];
4747
+ const prerequisites = Array.isArray(sopDataRaw.prerequisites)
4748
+ ? (sopDataRaw.prerequisites as any[]).map((p: any) => ({ text: p.text || "", checked: !!p.checked }))
4749
+ : [];
4750
+ const successCriteria = Array.isArray(sopDataRaw.successCriteria)
4751
+ ? (sopDataRaw.successCriteria as any[]).map((c: any) => ({ text: c.text || "", checked: !!c.checked }))
4752
+ : [];
4753
+ const escalationRules = Array.isArray(sopDataRaw.escalationRules)
4754
+ ? (sopDataRaw.escalationRules as any[]).map((r: any) => ({
4755
+ trigger: r.trigger || "", escalateTo: r.escalateTo || r.escalate_to || "",
4756
+ priority: (r.priority || "P2") as "P1" | "P2" | "P3",
4757
+ }))
4758
+ : [];
4759
+
4760
+ updates.content = JSON.stringify({
4761
+ title: (sopDataRaw.title as string) || (params.title as string) || "",
4762
+ objective: (sopDataRaw.objective as string) || "",
4763
+ scope: (sopDataRaw.scope as string) || "",
4764
+ prerequisites, steps,
4765
+ deliverables: Array.isArray(sopDataRaw.deliverables) ? sopDataRaw.deliverables : [],
4766
+ escalationRules, successCriteria,
4767
+ notes: (sopDataRaw.notes as string) || "",
4768
+ });
4769
+ }
4770
+
4771
+ const { data, error } = await supabase
4772
+ .from("agent_sops").update(updates)
4773
+ .eq("id", params.sop_id as string).eq("user_id", userId)
4774
+ .select("id, title, status").maybeSingle();
4775
+ if (error) return err(error.message);
4776
+ if (!data) return err("SOP not found");
4777
+ return ok({ message: `SOP "${data.title}" updated`, sop: data });
4778
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
4779
+ }
4780
+
4781
+ async function handleSOPDelete(supabase: SupabaseClient, userId: string, params: Record<string, unknown>): Promise<ToolResult> {
4782
+ try {
4783
+ if (!params.sop_id) return err("Missing required field: sop_id");
4784
+ const { data, error } = await supabase
4785
+ .from("agent_sops").delete()
4786
+ .eq("id", params.sop_id as string).eq("user_id", userId)
4787
+ .select("id, title").maybeSingle();
4788
+ if (error) return err(error.message);
4789
+ if (!data) return err("SOP not found — nothing deleted");
4790
+ return ok({ message: `SOP "${data.title}" deleted`, deleted: true });
4791
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
4792
+ }
4793
+
4794
+ async function handleSOPListSubagents(supabase: SupabaseClient, userId: string, params: Record<string, unknown>): Promise<ToolResult> {
4795
+ try {
4796
+ if (!params.chief_agent_id) return err("Missing required field: chief_agent_id");
4797
+ const { data, error } = await supabase
4798
+ .from("agent_subagents")
4799
+ .select("id, name, role, codename, color_hex, created_at")
4800
+ .eq("user_id", userId)
4801
+ .eq("chief_agent_id", params.chief_agent_id as string)
4802
+ .order("created_at", { ascending: true });
4803
+ if (error) return err(error.message);
4804
+ return ok({ subagents: data || [], count: (data || []).length, max_allowed: 5 });
4805
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
4806
+ }
4807
+
4808
+ async function handleSOPApplyTemplate(
4809
+ supabase: SupabaseClient, userId: string,
4810
+ resolveAgent: (id?: string) => Promise<string | null>,
4811
+ params: Record<string, unknown>,
4812
+ ): Promise<ToolResult> {
4813
+ try {
4814
+ if (!params.agent_id) return err("Missing required field: agent_id");
4815
+ if (!params.template_id) return err("Missing required field: template_id");
4816
+
4817
+ // Fetch template
4818
+ const { data: template, error: tErr } = await supabase
4819
+ .from("sop_templates")
4820
+ .select("name, content")
4821
+ .eq("id", params.template_id as string)
4822
+ .eq("user_id", userId)
4823
+ .single();
4824
+ if (tErr || !template) return err("Template not found");
4825
+
4826
+ const title = (params.title as string) || template.name || "SOP from Template";
4827
+
4828
+ const row = {
4829
+ user_id: userId,
4830
+ agent_id: params.agent_id as string,
4831
+ template_id: params.template_id as string,
4832
+ title,
4833
+ content: template.content, // Template content is already JSON
4834
+ status: "draft",
4835
+ department: (params.department as string) || null,
4836
+ };
4837
+
4838
+ const { data, error } = await supabase.from("agent_sops").insert(row).select().single();
4839
+ if (error) return err(error.message);
4840
+
4841
+ return ok({
4842
+ message: `SOP "${title}" created from template "${template.name}" for agent ${params.agent_id}`,
4843
+ sop: { id: data.id, title: data.title, status: data.status, agent_id: data.agent_id },
4844
+ });
4845
+ } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
4846
+ }
4847
+
4848
+
4517
4849
  // ═══════════════════════════════════════════════════════════════════════════════
4518
4850
  // Public: Register All Meta-Tools
4519
4851
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -4543,9 +4875,10 @@ export function registerTools(
4543
4875
  registerConstellationOps(api, supabase, userId); // 10
4544
4876
  registerFileOps(api, supabase, userId); // 11
4545
4877
  registerPlanOps(api, supabase, userId, resolveAgent); // 12
4878
+ registerSOPOps(api, supabase, userId, resolveAgent); // 13
4546
4879
 
4547
4880
  // ── Count and log ──
4548
- const toolCount = 12;
4881
+ const toolCount = 13;
4549
4882
  const callerName = getCallingAgentName(api);
4550
4883
  const agentLabel = fallbackAgentId || callerName || "auto-detect";
4551
4884
  api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);