ofiere-openclaw-plugin 4.11.0 → 4.12.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,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.11.0",
3
+ "version": "4.12.1",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin for Ofiere PM - 10 meta-tools with 13-action workflow mastery covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, and constellation agent architecture",
6
6
  "keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
package/src/prompt.ts CHANGED
@@ -9,14 +9,17 @@
9
9
  // registered meta-tool and will be included in the system prompt.
10
10
 
11
11
  const TOOL_DOCS: Record<string, string> = {
12
- OFIERE_TASK_OPS: `- **OFIERE_TASK_OPS** — Manage tasks (action: "list", "create", "update", "delete")
12
+ OFIERE_TASK_OPS: `- **OFIERE_TASK_OPS** — Manage tasks and approvals (action: "list", "create", "update", "delete", "add_approval", "list_approvals", "resolve_approval")
13
13
  - list: Filter by status, agent_id, space_id, folder_id, limit. Returns execution_plan, goals, constraints if present
14
14
  - create: Requires title. IMPORTANT: Always pass agent_id with your own name to self-assign (e.g. agent_id: "celia")
15
15
  - Optional: description, instructions, execution_plan, goals, constraints, system_prompt, priority, tags, dates
16
16
  - For COMPLEX tasks: include execution_plan (step-by-step), goals, constraints, and system_prompt
17
17
  - For SIMPLE tasks: just title and optionally description
18
18
  - update: Requires task_id. All create fields + progress
19
- - delete: Requires task_id. Removes task and subtasks`,
19
+ - delete: Requires task_id. Removes task and subtasks
20
+ - add_approval: Request sign-off on a task. Requires: task_id, approver_name. Optional: approver_type (human|agent, auto-detected), due_date, comment
21
+ - list_approvals: List approvals. Optional: task_id to filter, approval_status (pending|approved|rejected)
22
+ - resolve_approval: Approve or reject. Requires: approval_id, approval_status (approved|rejected). Optional: comment`,
20
23
 
21
24
  OFIERE_AGENT_OPS: `- **OFIERE_AGENT_OPS** — Query agents (action: "list")
22
25
  - list: See all agents with IDs, names, roles for task assignment`,
@@ -142,6 +145,9 @@ ${toolDocs}
142
145
  - To change a node's configuration (e.g. update agent instructions, change template text), use "update_node" with just the fields that changed.
143
146
  - When creating workflows with condition or loop nodes, specify sourceHandle on edges: "condition-true"/"condition-false" for conditions, "loop_body"/"done" for loops.
144
147
  - Fill node data fields with actual content — include real task instructions, templates, and variable references. Do NOT leave fields empty unless intentionally blank.
148
+ - Use add_approval when a task needs sign-off from a human or another agent before proceeding. Approvers can be agents (by name) or humans.
149
+ - Task approvals (OFIERE_TASK_OPS) are SEPARATE from workflow gate approvals (human_approval nodes). Do not confuse them.
150
+ - When an agent completes critical work, consider adding an approval request for human review before marking the task DONE.
145
151
  </ofiere-pm>`;
146
152
  }
147
153
 
package/src/tools.ts CHANGED
@@ -188,17 +188,21 @@ function registerTaskOps(
188
188
  name: "OFIERE_TASK_OPS",
189
189
  label: "Ofiere Task Operations",
190
190
  description:
191
- `Manage tasks in the Ofiere PM dashboard. All task operations go through this tool.\n\n` +
191
+ `Manage tasks and task approvals in the Ofiere PM dashboard. All task operations go through this tool.\n\n` +
192
192
  `Actions:\n` +
193
193
  `- "list": List/filter tasks. Optional: status, agent_id, space_id, folder_id, task_id, limit\n` +
194
194
  `- "get": Get a single task by ID. Required: task_id\n` +
195
195
  `- "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\n` +
196
196
  `- "update": Update a task. Required: task_id. Optional: all create fields + progress\n` +
197
- `- "delete": Delete task + subtasks. Required: task_id\n\n` +
197
+ `- "delete": Delete task + subtasks. Required: task_id\n` +
198
+ `- "add_approval": Request approval on a task. Required: task_id, approver_name. Optional: approver_type (human|agent, auto-detected), due_date, comment\n` +
199
+ `- "list_approvals": List approvals. Optional: task_id, approval_status filter (pending|approved|rejected)\n` +
200
+ `- "resolve_approval": Approve or reject. Required: approval_id, approval_status (approved|rejected). Optional: comment\n\n` +
198
201
  `For complex tasks, fill in execution_plan (step-by-step plan), goals, constraints, and system_prompt to help the executing agent.\n` +
199
202
  `For simple tasks, just provide title and optionally description.\n` +
200
203
  `agent_id: Pass your name to self-assign, another agent's name, or 'none'.\n` +
201
204
  `For recurring tasks: set start_date + recurrence_type + recurrence_interval. Example: every 2 minutes = recurrence_type: "minutely", recurrence_interval: 2.\n` +
205
+ `Approvals: Use add_approval to request sign-off from humans or agents. Approvals are separate from workflow gate nodes.\n` +
202
206
  `Status: PENDING, IN_PROGRESS, DONE, FAILED | Priority: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL`,
203
207
  parameters: {
204
208
  type: "object",
@@ -207,7 +211,7 @@ function registerTaskOps(
207
211
  action: {
208
212
  type: "string",
209
213
  description: "The operation to perform",
210
- enum: ["list", "get", "create", "update", "delete"],
214
+ enum: ["list", "get", "create", "update", "delete", "add_approval", "list_approvals", "resolve_approval"],
211
215
  },
212
216
  title: { type: "string", description: "Task title (required for create)" },
213
217
  description: { type: "string", description: "Task description" },
@@ -280,6 +284,19 @@ function registerTaskOps(
280
284
  },
281
285
  system_prompt: { type: "string", description: "Custom system prompt injection for the executing agent" },
282
286
  limit: { type: "number", description: "Max results for list (default 50)" },
287
+ approver_name: { type: "string", description: "Name of the approver (human name or agent name). Required for add_approval." },
288
+ approver_type: {
289
+ type: "string",
290
+ description: "Whether the approver is a human or an agent. Auto-detected from agents list if omitted.",
291
+ enum: ["human", "agent"],
292
+ },
293
+ approval_id: { type: "string", description: "Approval ID for resolve_approval action" },
294
+ approval_status: {
295
+ type: "string",
296
+ description: "Approval decision: approved or rejected. Used with resolve_approval.",
297
+ enum: ["approved", "rejected"],
298
+ },
299
+ comment: { type: "string", description: "Comment for approval (add_approval or resolve_approval)" },
283
300
  },
284
301
  },
285
302
  async execute(_id: string, params: Record<string, unknown>) {
@@ -297,9 +314,15 @@ function registerTaskOps(
297
314
  return handleUpdateTask(supabase, userId, params);
298
315
  case "delete":
299
316
  return handleDeleteTask(supabase, userId, params);
317
+ case "add_approval":
318
+ return handleAddApproval(supabase, userId, params);
319
+ case "list_approvals":
320
+ return handleListApprovals(supabase, userId, params);
321
+ case "resolve_approval":
322
+ return handleResolveApproval(supabase, userId, params);
300
323
  default:
301
324
  return err(
302
- `Unknown action "${action}". Valid actions: list, get, create, update, delete`,
325
+ `Unknown action "${action}". Valid actions: list, get, create, update, delete, add_approval, list_approvals, resolve_approval`,
303
326
  );
304
327
  }
305
328
  },
@@ -730,6 +753,189 @@ async function handleDeleteTask(
730
753
  }
731
754
  }
732
755
 
756
+ // ── Approval action handlers ─────────────────────────────────────────────────
757
+
758
+ async function handleAddApproval(
759
+ supabase: SupabaseClient,
760
+ userId: string,
761
+ params: Record<string, unknown>,
762
+ ): Promise<ToolResult> {
763
+ try {
764
+ if (!params.task_id) return err("Missing required field: task_id");
765
+ if (!params.approver_name) return err("Missing required field: approver_name");
766
+
767
+ const taskId = params.task_id as string;
768
+ const approverName = params.approver_name as string;
769
+
770
+ // Verify task ownership
771
+ const { data: taskData, error: taskErr } = await supabase
772
+ .from("tasks")
773
+ .select("id, title")
774
+ .eq("id", taskId)
775
+ .eq("user_id", userId)
776
+ .single();
777
+ if (taskErr || !taskData) return err(`Task not found or not owned by you: ${taskId}`);
778
+
779
+ // Auto-detect approver_type if not provided
780
+ let approverType = params.approver_type as string | undefined;
781
+ if (!approverType) {
782
+ const { data: agentMatch } = await supabase
783
+ .from("agents")
784
+ .select("id")
785
+ .eq("user_id", userId)
786
+ .ilike("name", approverName)
787
+ .limit(1);
788
+ approverType = (agentMatch && agentMatch.length > 0) ? "agent" : "human";
789
+ }
790
+
791
+ const insertData: Record<string, unknown> = {
792
+ task_id: taskId,
793
+ approver_type: approverType,
794
+ approver_name: approverName,
795
+ status: "pending",
796
+ };
797
+ if (params.due_date) insertData.due_date = params.due_date;
798
+ if (params.comment) insertData.comment = params.comment;
799
+
800
+ const { data, error } = await supabase
801
+ .from("pm_approvals")
802
+ .insert(insertData)
803
+ .select()
804
+ .single();
805
+
806
+ if (error) return err(error.message);
807
+
808
+ // Log activity
809
+ try {
810
+ await supabase.from("pm_activities").insert({
811
+ user_id: userId,
812
+ entity_type: "task",
813
+ entity_id: taskId,
814
+ action_type: "approval_requested",
815
+ source: "agent",
816
+ source_name: _registrationAgentName || "agent",
817
+ content: `Requested ${approverType} approval from ${approverName}`,
818
+ metadata: { approval_id: data.id },
819
+ });
820
+ } catch { /* non-fatal */ }
821
+
822
+ return ok({
823
+ message: `Approval requested from ${approverName} (${approverType}) for task "${taskData.title}"`,
824
+ approval: data,
825
+ });
826
+ } catch (e) {
827
+ return err(e instanceof Error ? e.message : String(e));
828
+ }
829
+ }
830
+
831
+ async function handleListApprovals(
832
+ supabase: SupabaseClient,
833
+ userId: string,
834
+ params: Record<string, unknown>,
835
+ ): Promise<ToolResult> {
836
+ try {
837
+ // First get all task IDs owned by this user (for ownership guard)
838
+ const { data: userTasks, error: taskErr } = await supabase
839
+ .from("tasks")
840
+ .select("id")
841
+ .eq("user_id", userId);
842
+ if (taskErr) return err(taskErr.message);
843
+
844
+ const taskIds = (userTasks || []).map((t: { id: string }) => t.id);
845
+ if (taskIds.length === 0) return ok({ approvals: [], count: 0 });
846
+
847
+ let query = supabase
848
+ .from("pm_approvals")
849
+ .select("*")
850
+ .in("task_id", taskIds)
851
+ .order("created_at", { ascending: false });
852
+
853
+ if (params.task_id) query = query.eq("task_id", params.task_id as string);
854
+ if (params.approval_status) query = query.eq("status", params.approval_status as string);
855
+
856
+ const { data, error } = await query;
857
+ if (error) return err(error.message);
858
+
859
+ return ok({ approvals: data || [], count: (data || []).length });
860
+ } catch (e) {
861
+ return err(e instanceof Error ? e.message : String(e));
862
+ }
863
+ }
864
+
865
+ async function handleResolveApproval(
866
+ supabase: SupabaseClient,
867
+ userId: string,
868
+ params: Record<string, unknown>,
869
+ ): Promise<ToolResult> {
870
+ try {
871
+ if (!params.approval_id) return err("Missing required field: approval_id");
872
+ if (!params.approval_status) return err("Missing required field: approval_status");
873
+
874
+ const approvalId = params.approval_id as string;
875
+ const newStatus = params.approval_status as string;
876
+
877
+ if (!['approved', 'rejected'].includes(newStatus)) {
878
+ return err(`Invalid approval_status "${newStatus}". Must be "approved" or "rejected".`);
879
+ }
880
+
881
+ // Verify the approval belongs to a task owned by this user
882
+ const { data: approval, error: fetchErr } = await supabase
883
+ .from("pm_approvals")
884
+ .select("id, task_id, approver_name, status")
885
+ .eq("id", approvalId)
886
+ .single();
887
+ if (fetchErr || !approval) return err(`Approval not found: ${approvalId}`);
888
+
889
+ // Verify task ownership
890
+ const { data: taskData, error: taskErr } = await supabase
891
+ .from("tasks")
892
+ .select("id, title")
893
+ .eq("id", approval.task_id)
894
+ .eq("user_id", userId)
895
+ .single();
896
+ if (taskErr || !taskData) return err(`Approval's task not owned by you`);
897
+
898
+ if (approval.status !== 'pending') {
899
+ return err(`Approval already resolved as "${approval.status}". Cannot change.`);
900
+ }
901
+
902
+ const updates: Record<string, unknown> = {
903
+ status: newStatus,
904
+ resolved_at: new Date().toISOString(),
905
+ };
906
+ if (params.comment !== undefined) updates.comment = params.comment;
907
+
908
+ const { error } = await supabase
909
+ .from("pm_approvals")
910
+ .update(updates)
911
+ .eq("id", approvalId);
912
+
913
+ if (error) return err(error.message);
914
+
915
+ // Log activity
916
+ try {
917
+ await supabase.from("pm_activities").insert({
918
+ user_id: userId,
919
+ entity_type: "task",
920
+ entity_id: approval.task_id,
921
+ action_type: `approval_${newStatus}`,
922
+ source: "agent",
923
+ source_name: _registrationAgentName || "agent",
924
+ content: `${newStatus === 'approved' ? 'Approved' : 'Rejected'} approval from ${approval.approver_name}${params.comment ? `: ${params.comment}` : ''}`,
925
+ metadata: { approval_id: approvalId },
926
+ });
927
+ } catch { /* non-fatal */ }
928
+
929
+ return ok({
930
+ message: `Approval ${approvalId} ${newStatus} for task "${taskData.title}"`,
931
+ approval_id: approvalId,
932
+ status: newStatus,
933
+ });
934
+ } catch (e) {
935
+ return err(e instanceof Error ? e.message : String(e));
936
+ }
937
+ }
938
+
733
939
  // ═══════════════════════════════════════════════════════════════════════════════
734
940
  // META-TOOL 2: OFIERE_AGENT_OPS — Agent Management
735
941
  // ═══════════════════════════════════════════════════════════════════════════════