ofiere-openclaw-plugin 4.18.4 → 4.19.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.
Files changed (3) hide show
  1. package/package.json +2 -2
  2. package/src/prompt.ts +19 -0
  3. package/src/tools.ts +348 -10
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.18.4",
3
+ "version": "4.19.0",
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
@@ -4442,21 +4442,29 @@ async function handleExecutePlan(
4442
4442
  return idMap.get(n.id);
4443
4443
  }
4444
4444
 
4445
+ // Helper: insert a dependency row into pm_dependencies
4446
+ async function insertDep(predId: string, succId: string): Promise<boolean> {
4447
+ const { error } = await supabase.from("pm_dependencies").insert({
4448
+ id: `dep-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
4449
+ user_id: userId, predecessor_id: predId, successor_id: succId,
4450
+ dependency_type: "finish_to_start", lag_days: 0,
4451
+ });
4452
+ return !error;
4453
+ }
4454
+
4445
4455
  while (depsQueue.length > 0) {
4446
4456
  const node = depsQueue.shift()!;
4447
4457
  const nodeRealId = resolveTaskId(node);
4448
4458
 
4449
4459
  // Chain siblings: sequential children of this node get chained left-to-right
4450
4460
  if (Array.isArray(node.children) && node.children.length > 1 && !node.parallel) {
4451
- // Collect task-type children in order for chaining
4452
4461
  const taskChildren: string[] = [];
4453
4462
  for (const child of node.children) {
4454
4463
  const cid = resolveTaskId(child);
4455
4464
  if (cid) taskChildren.push(cid);
4456
4465
  }
4457
4466
  for (let i = 1; i < taskChildren.length; i++) {
4458
- await supabase.from("task_dependencies").insert({ user_id: userId, predecessor_id: taskChildren[i - 1], successor_id: taskChildren[i], dependency_type: "finish_to_start", lag_days: 0 });
4459
- depsCreated++;
4467
+ if (await insertDep(taskChildren[i - 1], taskChildren[i])) depsCreated++;
4460
4468
  }
4461
4469
  }
4462
4470
 
@@ -4464,8 +4472,7 @@ async function handleExecutePlan(
4464
4472
  if (nodeRealId && !node.parallel && node.children?.length > 0) {
4465
4473
  const firstChildId = resolveTaskId(node.children[0]);
4466
4474
  if (firstChildId) {
4467
- await supabase.from("task_dependencies").insert({ user_id: userId, predecessor_id: nodeRealId, successor_id: firstChildId, dependency_type: "finish_to_start", lag_days: 0 });
4468
- depsCreated++;
4475
+ if (await insertDep(nodeRealId, firstChildId)) depsCreated++;
4469
4476
  }
4470
4477
  }
4471
4478
 
@@ -4474,8 +4481,7 @@ async function handleExecutePlan(
4474
4481
  for (const child of node.children) {
4475
4482
  const childId = resolveTaskId(child);
4476
4483
  if (childId) {
4477
- await supabase.from("task_dependencies").insert({ user_id: userId, predecessor_id: nodeRealId, successor_id: childId, dependency_type: "finish_to_start", lag_days: 0 });
4478
- depsCreated++;
4484
+ if (await insertDep(nodeRealId, childId)) depsCreated++;
4479
4485
  }
4480
4486
  }
4481
4487
  }
@@ -4490,8 +4496,7 @@ async function handleExecutePlan(
4490
4496
  if (rid) rootTaskIds.push(rid);
4491
4497
  }
4492
4498
  for (let i = 1; i < rootTaskIds.length; i++) {
4493
- await supabase.from("task_dependencies").insert({ user_id: userId, predecessor_id: rootTaskIds[i - 1], successor_id: rootTaskIds[i], dependency_type: "finish_to_start", lag_days: 0 });
4494
- depsCreated++;
4499
+ if (await insertDep(rootTaskIds[i - 1], rootTaskIds[i])) depsCreated++;
4495
4500
  }
4496
4501
 
4497
4502
  // Mark plan as deployed
@@ -4509,6 +4514,338 @@ async function handleExecutePlan(
4509
4514
  } catch (e) { return err(e instanceof Error ? e.message : String(e)); }
4510
4515
  }
4511
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"] },
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
+ .single();
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
+
4512
4849
  // ═══════════════════════════════════════════════════════════════════════════════
4513
4850
  // Public: Register All Meta-Tools
4514
4851
  // ═══════════════════════════════════════════════════════════════════════════════
@@ -4538,9 +4875,10 @@ export function registerTools(
4538
4875
  registerConstellationOps(api, supabase, userId); // 10
4539
4876
  registerFileOps(api, supabase, userId); // 11
4540
4877
  registerPlanOps(api, supabase, userId, resolveAgent); // 12
4878
+ registerSOPOps(api, supabase, userId, resolveAgent); // 13
4541
4879
 
4542
4880
  // ── Count and log ──
4543
- const toolCount = 12;
4881
+ const toolCount = 13;
4544
4882
  const callerName = getCallingAgentName(api);
4545
4883
  const agentLabel = fallbackAgentId || callerName || "auto-detect";
4546
4884
  api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);