ofiere-openclaw-plugin 4.56.0 → 4.56.2

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/dist/src/tools.js CHANGED
@@ -17,7 +17,6 @@ import { handleProposeAttach, handleCommitAttach, registerAttachmentContextHook,
17
17
  import { invalidateAgentTier } from "./agent-tier.js";
18
18
  import { loadSubagentRow, readDispatchSubagentId, loadDispatchContextBySession } from "./staffPersona.js";
19
19
  import { expandPlanToDependencyEdges } from "./planExecute.js";
20
- import { registerGateOps } from "./gateOps.js";
21
20
  function ok(data) {
22
21
  return {
23
22
  content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
@@ -6914,6 +6913,168 @@ supabase, config) {
6914
6913
  api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
6915
6914
  return toolCount;
6916
6915
  }
6916
+ // ═══════════════════════════════════════════════════════════════════════════════
6917
+ // META-TOOL 18: OFIERE_GATE_OPS — Manual approval gates (PM Planning)
6918
+ // ═══════════════════════════════════════════════════════════════════════════════
6919
+ // v4.56.2: inlined from gateOps.ts. OpenClaw host 2026.5.7 tool-discovery only
6920
+ // indexes api.registerTool calls present in the same module its plugin entry
6921
+ // imports; registrations inside transitive modules (e.g. ./gateOps.ts) are
6922
+ // reachable at runtime but invisible to the discovery scan, so the tool router
6923
+ // throws `plugin tool runtime missing` even though boot logs show registration.
6924
+ async function _gateOpsCollectTransitiveSuccessors(supabase, userId, rootTaskId) {
6925
+ const visited = new Set([rootTaskId]);
6926
+ const out = [];
6927
+ let frontier = [rootTaskId];
6928
+ for (let depth = 0; depth < 20 && frontier.length > 0; depth++) {
6929
+ const { data: deps } = await supabase
6930
+ .from("pm_dependencies")
6931
+ .select("successor_id")
6932
+ .in("predecessor_id", frontier)
6933
+ .eq("user_id", userId);
6934
+ const next = [];
6935
+ for (const d of (deps || [])) {
6936
+ const sid = d.successor_id;
6937
+ if (!sid || visited.has(sid))
6938
+ continue;
6939
+ visited.add(sid);
6940
+ next.push(sid);
6941
+ }
6942
+ if (next.length === 0)
6943
+ break;
6944
+ const { data: tasks } = await supabase
6945
+ .from("tasks")
6946
+ .select("id, title")
6947
+ .in("id", next)
6948
+ .eq("user_id", userId);
6949
+ for (const t of (tasks || []))
6950
+ out.push({ id: t.id, title: t.title });
6951
+ frontier = next;
6952
+ }
6953
+ return out;
6954
+ }
6955
+ function registerGateOps(api, supabase, userId, fallbackAgentId) {
6956
+ api.registerTool({
6957
+ name: "OFIERE_GATE_OPS",
6958
+ label: "Ofiere Gate Operations",
6959
+ description: `Approve or reject manual gates blocking task dispatch in the Planning Tab.\n\n` +
6960
+ `Actions:\n` +
6961
+ `- "list_pending": List manual gates pending approval. Optional: agent_id (filter to gates whose blocked task is assigned to this agent).\n` +
6962
+ `- "get": Inspect a specific gate + see the cascade impact of rejecting. Required: gate_id. ALWAYS call before "reject" so you understand what will fail.\n` +
6963
+ `- "approve": Approve a gate so the blocked task can dispatch. Required: gate_id, agent_id (the calling chief/staff). Optional: comment.\n` +
6964
+ `- "reject": Reject a gate — FAILS the blocked task and BLOCKS all transitive successors (cascade). Required: gate_id, agent_id, reason (min 5 chars).\n\n` +
6965
+ `Why agent_id is required: plugin runtime has no implicit agent identity. The agent invoking this tool MUST pass its own id so approval audit trail records 'agent:<id>' instead of 'user:<owner_uuid>'. Falls back to gateway OFIERE_AGENT_ID env when absent.`,
6966
+ parameters: {
6967
+ type: "object",
6968
+ required: ["action"],
6969
+ properties: {
6970
+ action: { type: "string", enum: ["list_pending", "get", "approve", "reject"], description: "list_pending | get | approve | reject" },
6971
+ gate_id: { type: "string", description: "Gate UUID (required for get, approve, reject)" },
6972
+ agent_id: { type: "string", description: "Calling agent id (required for approve, reject — recorded as 'agent:<id>' in approved_by / rejected_by). Also optional filter for list_pending." },
6973
+ comment: { type: "string", description: "Optional approval comment" },
6974
+ reason: { type: "string", description: "Rejection reason (required for reject, min 5 chars)" },
6975
+ },
6976
+ },
6977
+ async execute(_id, params) {
6978
+ try {
6979
+ const action = params.action;
6980
+ const resolveActor = () => {
6981
+ const id = params.agent_id || fallbackAgentId;
6982
+ return id ? `agent:${id}` : `user:${userId}`;
6983
+ };
6984
+ switch (action) {
6985
+ case "list_pending": {
6986
+ let q = supabase
6987
+ .from("pm_gates")
6988
+ .select("id, plan_id, plan_node_id, gate_label, task_id_blocked, created_at, tasks!inner(title, agent_id)")
6989
+ .eq("user_id", userId)
6990
+ .is("approved_at", null)
6991
+ .is("rejected_at", null)
6992
+ .order("created_at", { ascending: true });
6993
+ if (params.agent_id)
6994
+ q = q.eq("tasks.agent_id", params.agent_id);
6995
+ const { data, error } = await q;
6996
+ if (error)
6997
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
6998
+ return { content: [{ type: "text", text: JSON.stringify({ pending: data || [], count: (data || []).length }, null, 2) }] };
6999
+ }
7000
+ case "get": {
7001
+ if (!params.gate_id)
7002
+ return { content: [{ type: "text", text: `Error: Missing required: gate_id` }] };
7003
+ const { data: gate, error } = await supabase
7004
+ .from("pm_gates")
7005
+ .select("*, tasks!inner(id, title, agent_id, status)")
7006
+ .eq("id", params.gate_id)
7007
+ .eq("user_id", userId)
7008
+ .maybeSingle();
7009
+ if (error)
7010
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
7011
+ if (!gate)
7012
+ return { content: [{ type: "text", text: `Error: Gate not found` }] };
7013
+ const blockedTaskId = gate.task_id_blocked;
7014
+ const downstream = await _gateOpsCollectTransitiveSuccessors(supabase, userId, blockedTaskId);
7015
+ const blockedTitle = gate.tasks?.title || "blocked task";
7016
+ return { content: [{ type: "text", text: JSON.stringify({
7017
+ gate,
7018
+ impact_on_reject: {
7019
+ failed_task: { id: blockedTaskId, title: blockedTitle },
7020
+ blocked_downstream: downstream,
7021
+ summary: `Rejecting will FAIL '${blockedTitle}' and BLOCK ${downstream.length} downstream task${downstream.length === 1 ? "" : "s"}.`,
7022
+ },
7023
+ }, null, 2) }] };
7024
+ }
7025
+ case "approve": {
7026
+ if (!params.gate_id)
7027
+ return { content: [{ type: "text", text: `Error: Missing required: gate_id` }] };
7028
+ if (!params.agent_id && !fallbackAgentId)
7029
+ return { content: [{ type: "text", text: `Error: Missing required: agent_id (calling agent identity — plugin has no implicit agent context)` }] };
7030
+ const actor = resolveActor();
7031
+ const { data, error } = await supabase
7032
+ .from("pm_gates")
7033
+ .update({
7034
+ approved_at: new Date().toISOString(),
7035
+ approved_by: actor,
7036
+ approval_comment: params.comment ?? null,
7037
+ })
7038
+ .eq("id", params.gate_id)
7039
+ .eq("user_id", userId)
7040
+ .is("approved_at", null)
7041
+ .is("rejected_at", null)
7042
+ .select("id");
7043
+ if (error)
7044
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
7045
+ if (!data || data.length === 0)
7046
+ return { content: [{ type: "text", text: `Error: Gate not found or already resolved` }] };
7047
+ return { content: [{ type: "text", text: JSON.stringify({ approved: true, gate_id: params.gate_id, approved_by: actor }, null, 2) }] };
7048
+ }
7049
+ case "reject": {
7050
+ if (!params.gate_id)
7051
+ return { content: [{ type: "text", text: `Error: Missing required: gate_id` }] };
7052
+ if (!params.agent_id && !fallbackAgentId)
7053
+ return { content: [{ type: "text", text: `Error: Missing required: agent_id (calling agent identity — plugin has no implicit agent context)` }] };
7054
+ const reason = (params.reason || "").trim();
7055
+ if (reason.length < 5)
7056
+ return { content: [{ type: "text", text: `Error: Missing required: reason (min 5 chars) — rejection requires explanation` }] };
7057
+ const actor = resolveActor();
7058
+ const { data, error } = await supabase.rpc("cascade_reject_gate", {
7059
+ p_gate_id: params.gate_id,
7060
+ p_user_id: userId,
7061
+ p_reason: reason,
7062
+ p_actor: actor,
7063
+ });
7064
+ if (error)
7065
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
7066
+ return { content: [{ type: "text", text: JSON.stringify({ rejected: true, gate_id: params.gate_id, rejected_by: actor, result: data }, null, 2) }] };
7067
+ }
7068
+ default:
7069
+ return { content: [{ type: "text", text: `Error: Unknown gate action: ${action}` }] };
7070
+ }
7071
+ }
7072
+ catch (e) {
7073
+ return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
7074
+ }
7075
+ },
7076
+ });
7077
+ }
6917
7078
  // ── Server-Side Brain Extraction (agent_end hook) ─────────────────────────
6918
7079
  // Fires after EVERY agent turn completes, regardless of channel.
6919
7080
  // Extracts L1 raw fragments and lightweight L2 summaries.
@@ -49,7 +49,8 @@
49
49
  "OFIERE_FRAMEWORK_OPS",
50
50
  "OFIERE_BRAIN_OPS",
51
51
  "OFIERE_TALENT_OPS",
52
- "OFIERE_OFFICE_OPS"
52
+ "OFIERE_OFFICE_OPS",
53
+ "OFIERE_GATE_OPS"
53
54
  ]
54
55
  },
55
56
  "activation": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.56.0",
3
+ "version": "4.56.2",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin for Ofiere PM - 18 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, corporate frameworks, agent office canvas, and PM gate approvals",
6
6
  "keywords": [
package/src/tools.ts CHANGED
@@ -24,7 +24,6 @@ import {
24
24
  import { invalidateAgentTier } from "./agent-tier.js";
25
25
  import { loadSubagentRow, readDispatchSubagentId, loadDispatchContextBySession, type DispatchContextRow } from "./staffPersona.js";
26
26
  import { expandPlanToDependencyEdges, type PlanNode } from "./planExecute.js";
27
- import { registerGateOps } from "./gateOps.js";
28
27
 
29
28
  // ─── Tool result shape (matches OpenClaw SDK) ────────────────────────────────
30
29
 
@@ -7081,6 +7080,169 @@ export function registerTools(
7081
7080
  return toolCount;
7082
7081
  }
7083
7082
 
7083
+ // ═══════════════════════════════════════════════════════════════════════════════
7084
+ // META-TOOL 18: OFIERE_GATE_OPS — Manual approval gates (PM Planning)
7085
+ // ═══════════════════════════════════════════════════════════════════════════════
7086
+ // v4.56.2: inlined from gateOps.ts. OpenClaw host 2026.5.7 tool-discovery only
7087
+ // indexes api.registerTool calls present in the same module its plugin entry
7088
+ // imports; registrations inside transitive modules (e.g. ./gateOps.ts) are
7089
+ // reachable at runtime but invisible to the discovery scan, so the tool router
7090
+ // throws `plugin tool runtime missing` even though boot logs show registration.
7091
+
7092
+ async function _gateOpsCollectTransitiveSuccessors(
7093
+ supabase: SupabaseClient,
7094
+ userId: string,
7095
+ rootTaskId: string,
7096
+ ): Promise<{ id: string; title: string }[]> {
7097
+ const visited = new Set<string>([rootTaskId]);
7098
+ const out: { id: string; title: string }[] = [];
7099
+ let frontier: string[] = [rootTaskId];
7100
+ for (let depth = 0; depth < 20 && frontier.length > 0; depth++) {
7101
+ const { data: deps } = await supabase
7102
+ .from("pm_dependencies")
7103
+ .select("successor_id")
7104
+ .in("predecessor_id", frontier)
7105
+ .eq("user_id", userId);
7106
+ const next: string[] = [];
7107
+ for (const d of (deps || [])) {
7108
+ const sid = (d as any).successor_id as string;
7109
+ if (!sid || visited.has(sid)) continue;
7110
+ visited.add(sid);
7111
+ next.push(sid);
7112
+ }
7113
+ if (next.length === 0) break;
7114
+ const { data: tasks } = await supabase
7115
+ .from("tasks")
7116
+ .select("id, title")
7117
+ .in("id", next)
7118
+ .eq("user_id", userId);
7119
+ for (const t of (tasks || [])) out.push({ id: (t as any).id, title: (t as any).title });
7120
+ frontier = next;
7121
+ }
7122
+ return out;
7123
+ }
7124
+
7125
+ function registerGateOps(
7126
+ api: any,
7127
+ supabase: SupabaseClient,
7128
+ userId: string,
7129
+ fallbackAgentId: string | undefined,
7130
+ ): void {
7131
+ api.registerTool({
7132
+ name: "OFIERE_GATE_OPS",
7133
+ label: "Ofiere Gate Operations",
7134
+ description:
7135
+ `Approve or reject manual gates blocking task dispatch in the Planning Tab.\n\n` +
7136
+ `Actions:\n` +
7137
+ `- "list_pending": List manual gates pending approval. Optional: agent_id (filter to gates whose blocked task is assigned to this agent).\n` +
7138
+ `- "get": Inspect a specific gate + see the cascade impact of rejecting. Required: gate_id. ALWAYS call before "reject" so you understand what will fail.\n` +
7139
+ `- "approve": Approve a gate so the blocked task can dispatch. Required: gate_id, agent_id (the calling chief/staff). Optional: comment.\n` +
7140
+ `- "reject": Reject a gate — FAILS the blocked task and BLOCKS all transitive successors (cascade). Required: gate_id, agent_id, reason (min 5 chars).\n\n` +
7141
+ `Why agent_id is required: plugin runtime has no implicit agent identity. The agent invoking this tool MUST pass its own id so approval audit trail records 'agent:<id>' instead of 'user:<owner_uuid>'. Falls back to gateway OFIERE_AGENT_ID env when absent.`,
7142
+ parameters: {
7143
+ type: "object",
7144
+ required: ["action"],
7145
+ properties: {
7146
+ action: { type: "string", enum: ["list_pending", "get", "approve", "reject"], description: "list_pending | get | approve | reject" },
7147
+ gate_id: { type: "string", description: "Gate UUID (required for get, approve, reject)" },
7148
+ agent_id: { type: "string", description: "Calling agent id (required for approve, reject — recorded as 'agent:<id>' in approved_by / rejected_by). Also optional filter for list_pending." },
7149
+ comment: { type: "string", description: "Optional approval comment" },
7150
+ reason: { type: "string", description: "Rejection reason (required for reject, min 5 chars)" },
7151
+ },
7152
+ },
7153
+ async execute(_id: string, params: Record<string, unknown>) {
7154
+ try {
7155
+ const action = params.action as string;
7156
+ const resolveActor = (): string => {
7157
+ const id = (params.agent_id as string) || fallbackAgentId;
7158
+ return id ? `agent:${id}` : `user:${userId}`;
7159
+ };
7160
+
7161
+ switch (action) {
7162
+ case "list_pending": {
7163
+ let q = supabase
7164
+ .from("pm_gates")
7165
+ .select("id, plan_id, plan_node_id, gate_label, task_id_blocked, created_at, tasks!inner(title, agent_id)")
7166
+ .eq("user_id", userId)
7167
+ .is("approved_at", null)
7168
+ .is("rejected_at", null)
7169
+ .order("created_at", { ascending: true });
7170
+ if (params.agent_id) q = q.eq("tasks.agent_id", params.agent_id as string);
7171
+ const { data, error } = await q;
7172
+ if (error) return { content: [{ type: "text" as const, text: `Error: ${error.message}` }] };
7173
+ return { content: [{ type: "text" as const, text: JSON.stringify({ pending: data || [], count: (data || []).length }, null, 2) }] };
7174
+ }
7175
+
7176
+ case "get": {
7177
+ if (!params.gate_id) return { content: [{ type: "text" as const, text: `Error: Missing required: gate_id` }] };
7178
+ const { data: gate, error } = await supabase
7179
+ .from("pm_gates")
7180
+ .select("*, tasks!inner(id, title, agent_id, status)")
7181
+ .eq("id", params.gate_id as string)
7182
+ .eq("user_id", userId)
7183
+ .maybeSingle();
7184
+ if (error) return { content: [{ type: "text" as const, text: `Error: ${error.message}` }] };
7185
+ if (!gate) return { content: [{ type: "text" as const, text: `Error: Gate not found` }] };
7186
+ const blockedTaskId = (gate as any).task_id_blocked as string;
7187
+ const downstream = await _gateOpsCollectTransitiveSuccessors(supabase, userId, blockedTaskId);
7188
+ const blockedTitle = (gate as any).tasks?.title || "blocked task";
7189
+ return { content: [{ type: "text" as const, text: JSON.stringify({
7190
+ gate,
7191
+ impact_on_reject: {
7192
+ failed_task: { id: blockedTaskId, title: blockedTitle },
7193
+ blocked_downstream: downstream,
7194
+ summary: `Rejecting will FAIL '${blockedTitle}' and BLOCK ${downstream.length} downstream task${downstream.length === 1 ? "" : "s"}.`,
7195
+ },
7196
+ }, null, 2) }] };
7197
+ }
7198
+
7199
+ case "approve": {
7200
+ if (!params.gate_id) return { content: [{ type: "text" as const, text: `Error: Missing required: gate_id` }] };
7201
+ if (!params.agent_id && !fallbackAgentId) return { content: [{ type: "text" as const, text: `Error: Missing required: agent_id (calling agent identity — plugin has no implicit agent context)` }] };
7202
+ const actor = resolveActor();
7203
+ const { data, error } = await supabase
7204
+ .from("pm_gates")
7205
+ .update({
7206
+ approved_at: new Date().toISOString(),
7207
+ approved_by: actor,
7208
+ approval_comment: (params.comment as string) ?? null,
7209
+ })
7210
+ .eq("id", params.gate_id as string)
7211
+ .eq("user_id", userId)
7212
+ .is("approved_at", null)
7213
+ .is("rejected_at", null)
7214
+ .select("id");
7215
+ if (error) return { content: [{ type: "text" as const, text: `Error: ${error.message}` }] };
7216
+ if (!data || data.length === 0) return { content: [{ type: "text" as const, text: `Error: Gate not found or already resolved` }] };
7217
+ return { content: [{ type: "text" as const, text: JSON.stringify({ approved: true, gate_id: params.gate_id, approved_by: actor }, null, 2) }] };
7218
+ }
7219
+
7220
+ case "reject": {
7221
+ if (!params.gate_id) return { content: [{ type: "text" as const, text: `Error: Missing required: gate_id` }] };
7222
+ if (!params.agent_id && !fallbackAgentId) return { content: [{ type: "text" as const, text: `Error: Missing required: agent_id (calling agent identity — plugin has no implicit agent context)` }] };
7223
+ const reason = ((params.reason as string) || "").trim();
7224
+ if (reason.length < 5) return { content: [{ type: "text" as const, text: `Error: Missing required: reason (min 5 chars) — rejection requires explanation` }] };
7225
+ const actor = resolveActor();
7226
+ const { data, error } = await supabase.rpc("cascade_reject_gate", {
7227
+ p_gate_id: params.gate_id as string,
7228
+ p_user_id: userId,
7229
+ p_reason: reason,
7230
+ p_actor: actor,
7231
+ });
7232
+ if (error) return { content: [{ type: "text" as const, text: `Error: ${error.message}` }] };
7233
+ return { content: [{ type: "text" as const, text: JSON.stringify({ rejected: true, gate_id: params.gate_id, rejected_by: actor, result: data }, null, 2) }] };
7234
+ }
7235
+
7236
+ default:
7237
+ return { content: [{ type: "text" as const, text: `Error: Unknown gate action: ${action}` }] };
7238
+ }
7239
+ } catch (e) {
7240
+ return { content: [{ type: "text" as const, text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
7241
+ }
7242
+ },
7243
+ });
7244
+ }
7245
+
7084
7246
  // ── Server-Side Brain Extraction (agent_end hook) ─────────────────────────
7085
7247
  // Fires after EVERY agent turn completes, regardless of channel.
7086
7248
  // Extracts L1 raw fragments and lightweight L2 summaries.
@@ -1,169 +0,0 @@
1
- // src/gateOps.ts — OFIERE_GATE_OPS meta-tool
2
- // Approve / reject manual gates blocking task dispatch in the Planning Tab.
3
- //
4
- // Phase 0 finding: plugin factory carries no implicit agentId in closure.
5
- // The CALLING chief / staff agent MUST pass `agent_id` explicitly for approve/reject
6
- // so the audit trail records "agent:<id>" in approved_by / rejected_by.
7
- // Fallback order: args.agent_id → fallbackAgentId (gateway OFIERE_AGENT_ID env) → "user:<userId>".
8
- function ok(data) {
9
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
10
- }
11
- function err(message) {
12
- return { content: [{ type: "text", text: `Error: ${message}` }] };
13
- }
14
- // Bounded BFS — collect transitive successors so the agent sees cascade impact
15
- // before rejecting. Mirrors dashboard/lib/ofie/tool-executor.ts _collectTransitiveSuccessors.
16
- async function collectTransitiveSuccessors(supabase, userId, rootTaskId) {
17
- const visited = new Set([rootTaskId]);
18
- const out = [];
19
- let frontier = [rootTaskId];
20
- for (let depth = 0; depth < 20 && frontier.length > 0; depth++) {
21
- const { data: deps } = await supabase
22
- .from("pm_dependencies")
23
- .select("successor_id")
24
- .in("predecessor_id", frontier)
25
- .eq("user_id", userId);
26
- const next = [];
27
- for (const d of (deps || [])) {
28
- const sid = d.successor_id;
29
- if (!sid || visited.has(sid))
30
- continue;
31
- visited.add(sid);
32
- next.push(sid);
33
- }
34
- if (next.length === 0)
35
- break;
36
- const { data: tasks } = await supabase
37
- .from("tasks")
38
- .select("id, title")
39
- .in("id", next)
40
- .eq("user_id", userId);
41
- for (const t of (tasks || []))
42
- out.push({ id: t.id, title: t.title });
43
- frontier = next;
44
- }
45
- return out;
46
- }
47
- export function registerGateOps(api, supabase, userId, fallbackAgentId) {
48
- api.registerTool({
49
- name: "OFIERE_GATE_OPS",
50
- label: "Ofiere Gate Operations",
51
- description: `Approve or reject manual gates blocking task dispatch in the Planning Tab.\n\n` +
52
- `Actions:\n` +
53
- `- "list_pending": List manual gates pending approval. Optional: agent_id (filter to gates whose blocked task is assigned to this agent).\n` +
54
- `- "get": Inspect a specific gate + see the cascade impact of rejecting. Required: gate_id. ALWAYS call before "reject" so you understand what will fail.\n` +
55
- `- "approve": Approve a gate so the blocked task can dispatch. Required: gate_id, agent_id (the calling chief/staff). Optional: comment.\n` +
56
- `- "reject": Reject a gate — FAILS the blocked task and BLOCKS all transitive successors (cascade). Required: gate_id, agent_id, reason (min 5 chars).\n\n` +
57
- `Why agent_id is required: plugin runtime has no implicit agent identity. The agent invoking this tool MUST pass its own id so approval audit trail records 'agent:<id>' instead of 'user:<owner_uuid>'. Falls back to gateway OFIERE_AGENT_ID env when absent.`,
58
- parameters: {
59
- type: "object",
60
- required: ["action"],
61
- properties: {
62
- action: { type: "string", enum: ["list_pending", "get", "approve", "reject"], description: "list_pending | get | approve | reject" },
63
- gate_id: { type: "string", description: "Gate UUID (required for get, approve, reject)" },
64
- agent_id: { type: "string", description: "Calling agent id (required for approve, reject — recorded as 'agent:<id>' in approved_by / rejected_by). Also optional filter for list_pending." },
65
- comment: { type: "string", description: "Optional approval comment" },
66
- reason: { type: "string", description: "Rejection reason (required for reject, min 5 chars)" },
67
- },
68
- },
69
- async execute(_id, params) {
70
- try {
71
- const action = params.action;
72
- const resolveActor = () => {
73
- const id = params.agent_id || fallbackAgentId;
74
- return id ? `agent:${id}` : `user:${userId}`;
75
- };
76
- switch (action) {
77
- case "list_pending": {
78
- let q = supabase
79
- .from("pm_gates")
80
- .select("id, plan_id, plan_node_id, gate_label, task_id_blocked, created_at, tasks!inner(title, agent_id)")
81
- .eq("user_id", userId)
82
- .is("approved_at", null)
83
- .is("rejected_at", null)
84
- .order("created_at", { ascending: true });
85
- if (params.agent_id)
86
- q = q.eq("tasks.agent_id", params.agent_id);
87
- const { data, error } = await q;
88
- if (error)
89
- return err(error.message);
90
- return ok({ pending: data || [], count: (data || []).length });
91
- }
92
- case "get": {
93
- if (!params.gate_id)
94
- return err("Missing required: gate_id");
95
- const { data: gate, error } = await supabase
96
- .from("pm_gates")
97
- .select("*, tasks!inner(id, title, agent_id, status)")
98
- .eq("id", params.gate_id)
99
- .eq("user_id", userId)
100
- .maybeSingle();
101
- if (error)
102
- return err(error.message);
103
- if (!gate)
104
- return err("Gate not found");
105
- const blockedTaskId = gate.task_id_blocked;
106
- const downstream = await collectTransitiveSuccessors(supabase, userId, blockedTaskId);
107
- const blockedTitle = gate.tasks?.title || "blocked task";
108
- return ok({
109
- gate,
110
- impact_on_reject: {
111
- failed_task: { id: blockedTaskId, title: blockedTitle },
112
- blocked_downstream: downstream,
113
- summary: `Rejecting will FAIL '${blockedTitle}' and BLOCK ${downstream.length} downstream task${downstream.length === 1 ? "" : "s"}.`,
114
- },
115
- });
116
- }
117
- case "approve": {
118
- if (!params.gate_id)
119
- return err("Missing required: gate_id");
120
- if (!params.agent_id && !fallbackAgentId)
121
- return err("Missing required: agent_id (calling agent identity — plugin has no implicit agent context)");
122
- const actor = resolveActor();
123
- const { data, error } = await supabase
124
- .from("pm_gates")
125
- .update({
126
- approved_at: new Date().toISOString(),
127
- approved_by: actor,
128
- approval_comment: params.comment ?? null,
129
- })
130
- .eq("id", params.gate_id)
131
- .eq("user_id", userId)
132
- .is("approved_at", null)
133
- .is("rejected_at", null)
134
- .select("id");
135
- if (error)
136
- return err(error.message);
137
- if (!data || data.length === 0)
138
- return err("Gate not found or already resolved");
139
- return ok({ approved: true, gate_id: params.gate_id, approved_by: actor });
140
- }
141
- case "reject": {
142
- if (!params.gate_id)
143
- return err("Missing required: gate_id");
144
- if (!params.agent_id && !fallbackAgentId)
145
- return err("Missing required: agent_id (calling agent identity — plugin has no implicit agent context)");
146
- const reason = (params.reason || "").trim();
147
- if (reason.length < 5)
148
- return err("Missing required: reason (min 5 chars) — rejection requires explanation");
149
- const actor = resolveActor();
150
- const { data, error } = await supabase.rpc("cascade_reject_gate", {
151
- p_gate_id: params.gate_id,
152
- p_user_id: userId,
153
- p_reason: reason,
154
- p_actor: actor,
155
- });
156
- if (error)
157
- return err(error.message);
158
- return ok({ rejected: true, gate_id: params.gate_id, rejected_by: actor, result: data });
159
- }
160
- default:
161
- return err(`Unknown gate action: ${action}`);
162
- }
163
- }
164
- catch (e) {
165
- return err(e instanceof Error ? e.message : String(e));
166
- }
167
- },
168
- });
169
- }
package/src/gateOps.ts DELETED
@@ -1,177 +0,0 @@
1
- // src/gateOps.ts — OFIERE_GATE_OPS meta-tool
2
- // Approve / reject manual gates blocking task dispatch in the Planning Tab.
3
- //
4
- // Phase 0 finding: plugin factory carries no implicit agentId in closure.
5
- // The CALLING chief / staff agent MUST pass `agent_id` explicitly for approve/reject
6
- // so the audit trail records "agent:<id>" in approved_by / rejected_by.
7
- // Fallback order: args.agent_id → fallbackAgentId (gateway OFIERE_AGENT_ID env) → "user:<userId>".
8
-
9
- import type { SupabaseClient } from "@supabase/supabase-js";
10
-
11
- interface ToolResult {
12
- content: Array<{ type: "text"; text: string }>;
13
- }
14
-
15
- function ok(data: unknown): ToolResult {
16
- return { content: [{ type: "text" as const, text: JSON.stringify(data, null, 2) }] };
17
- }
18
-
19
- function err(message: string): ToolResult {
20
- return { content: [{ type: "text" as const, text: `Error: ${message}` }] };
21
- }
22
-
23
- // Bounded BFS — collect transitive successors so the agent sees cascade impact
24
- // before rejecting. Mirrors dashboard/lib/ofie/tool-executor.ts _collectTransitiveSuccessors.
25
- async function collectTransitiveSuccessors(
26
- supabase: SupabaseClient,
27
- userId: string,
28
- rootTaskId: string,
29
- ): Promise<{ id: string; title: string }[]> {
30
- const visited = new Set<string>([rootTaskId]);
31
- const out: { id: string; title: string }[] = [];
32
- let frontier: string[] = [rootTaskId];
33
- for (let depth = 0; depth < 20 && frontier.length > 0; depth++) {
34
- const { data: deps } = await supabase
35
- .from("pm_dependencies")
36
- .select("successor_id")
37
- .in("predecessor_id", frontier)
38
- .eq("user_id", userId);
39
- const next: string[] = [];
40
- for (const d of (deps || [])) {
41
- const sid = (d as any).successor_id as string;
42
- if (!sid || visited.has(sid)) continue;
43
- visited.add(sid);
44
- next.push(sid);
45
- }
46
- if (next.length === 0) break;
47
- const { data: tasks } = await supabase
48
- .from("tasks")
49
- .select("id, title")
50
- .in("id", next)
51
- .eq("user_id", userId);
52
- for (const t of (tasks || [])) out.push({ id: (t as any).id, title: (t as any).title });
53
- frontier = next;
54
- }
55
- return out;
56
- }
57
-
58
- export function registerGateOps(
59
- api: any,
60
- supabase: SupabaseClient,
61
- userId: string,
62
- fallbackAgentId: string | undefined,
63
- ): void {
64
- api.registerTool({
65
- name: "OFIERE_GATE_OPS",
66
- label: "Ofiere Gate Operations",
67
- description:
68
- `Approve or reject manual gates blocking task dispatch in the Planning Tab.\n\n` +
69
- `Actions:\n` +
70
- `- "list_pending": List manual gates pending approval. Optional: agent_id (filter to gates whose blocked task is assigned to this agent).\n` +
71
- `- "get": Inspect a specific gate + see the cascade impact of rejecting. Required: gate_id. ALWAYS call before "reject" so you understand what will fail.\n` +
72
- `- "approve": Approve a gate so the blocked task can dispatch. Required: gate_id, agent_id (the calling chief/staff). Optional: comment.\n` +
73
- `- "reject": Reject a gate — FAILS the blocked task and BLOCKS all transitive successors (cascade). Required: gate_id, agent_id, reason (min 5 chars).\n\n` +
74
- `Why agent_id is required: plugin runtime has no implicit agent identity. The agent invoking this tool MUST pass its own id so approval audit trail records 'agent:<id>' instead of 'user:<owner_uuid>'. Falls back to gateway OFIERE_AGENT_ID env when absent.`,
75
- parameters: {
76
- type: "object",
77
- required: ["action"],
78
- properties: {
79
- action: { type: "string", enum: ["list_pending", "get", "approve", "reject"], description: "list_pending | get | approve | reject" },
80
- gate_id: { type: "string", description: "Gate UUID (required for get, approve, reject)" },
81
- agent_id: { type: "string", description: "Calling agent id (required for approve, reject — recorded as 'agent:<id>' in approved_by / rejected_by). Also optional filter for list_pending." },
82
- comment: { type: "string", description: "Optional approval comment" },
83
- reason: { type: "string", description: "Rejection reason (required for reject, min 5 chars)" },
84
- },
85
- },
86
- async execute(_id: string, params: Record<string, unknown>) {
87
- try {
88
- const action = params.action as string;
89
- const resolveActor = (): string => {
90
- const id = (params.agent_id as string) || fallbackAgentId;
91
- return id ? `agent:${id}` : `user:${userId}`;
92
- };
93
-
94
- switch (action) {
95
- case "list_pending": {
96
- let q = supabase
97
- .from("pm_gates")
98
- .select("id, plan_id, plan_node_id, gate_label, task_id_blocked, created_at, tasks!inner(title, agent_id)")
99
- .eq("user_id", userId)
100
- .is("approved_at", null)
101
- .is("rejected_at", null)
102
- .order("created_at", { ascending: true });
103
- if (params.agent_id) q = q.eq("tasks.agent_id", params.agent_id as string);
104
- const { data, error } = await q;
105
- if (error) return err(error.message);
106
- return ok({ pending: data || [], count: (data || []).length });
107
- }
108
-
109
- case "get": {
110
- if (!params.gate_id) return err("Missing required: gate_id");
111
- const { data: gate, error } = await supabase
112
- .from("pm_gates")
113
- .select("*, tasks!inner(id, title, agent_id, status)")
114
- .eq("id", params.gate_id as string)
115
- .eq("user_id", userId)
116
- .maybeSingle();
117
- if (error) return err(error.message);
118
- if (!gate) return err("Gate not found");
119
- const blockedTaskId = (gate as any).task_id_blocked as string;
120
- const downstream = await collectTransitiveSuccessors(supabase, userId, blockedTaskId);
121
- const blockedTitle = (gate as any).tasks?.title || "blocked task";
122
- return ok({
123
- gate,
124
- impact_on_reject: {
125
- failed_task: { id: blockedTaskId, title: blockedTitle },
126
- blocked_downstream: downstream,
127
- summary: `Rejecting will FAIL '${blockedTitle}' and BLOCK ${downstream.length} downstream task${downstream.length === 1 ? "" : "s"}.`,
128
- },
129
- });
130
- }
131
-
132
- case "approve": {
133
- if (!params.gate_id) return err("Missing required: gate_id");
134
- if (!params.agent_id && !fallbackAgentId) return err("Missing required: agent_id (calling agent identity — plugin has no implicit agent context)");
135
- const actor = resolveActor();
136
- const { data, error } = await supabase
137
- .from("pm_gates")
138
- .update({
139
- approved_at: new Date().toISOString(),
140
- approved_by: actor,
141
- approval_comment: (params.comment as string) ?? null,
142
- })
143
- .eq("id", params.gate_id as string)
144
- .eq("user_id", userId)
145
- .is("approved_at", null)
146
- .is("rejected_at", null)
147
- .select("id");
148
- if (error) return err(error.message);
149
- if (!data || data.length === 0) return err("Gate not found or already resolved");
150
- return ok({ approved: true, gate_id: params.gate_id, approved_by: actor });
151
- }
152
-
153
- case "reject": {
154
- if (!params.gate_id) return err("Missing required: gate_id");
155
- if (!params.agent_id && !fallbackAgentId) return err("Missing required: agent_id (calling agent identity — plugin has no implicit agent context)");
156
- const reason = ((params.reason as string) || "").trim();
157
- if (reason.length < 5) return err("Missing required: reason (min 5 chars) — rejection requires explanation");
158
- const actor = resolveActor();
159
- const { data, error } = await supabase.rpc("cascade_reject_gate", {
160
- p_gate_id: params.gate_id as string,
161
- p_user_id: userId,
162
- p_reason: reason,
163
- p_actor: actor,
164
- });
165
- if (error) return err(error.message);
166
- return ok({ rejected: true, gate_id: params.gate_id, rejected_by: actor, result: data });
167
- }
168
-
169
- default:
170
- return err(`Unknown gate action: ${action}`);
171
- }
172
- } catch (e) {
173
- return err(e instanceof Error ? e.message : String(e));
174
- }
175
- },
176
- });
177
- }