ofiere-openclaw-plugin 4.56.1 → 4.56.3

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) }],
@@ -811,9 +810,21 @@ async function handleUpdateTask(supabase, userId, params) {
811
810
  "title", "description", "status", "priority", "progress",
812
811
  "agent_id", "start_date", "due_date", "tags",
813
812
  ];
813
+ // Fix #8 (2026-05-17): Postgres rejects empty-string for timestamptz columns,
814
+ // so LLM callers passing start_date/due_date="" to clear a stale value would
815
+ // error instead of nulling the column. Coerce empty/whitespace → null here
816
+ // to match the dashboard native executor (see tool-executor.ts handleTaskOps).
817
+ const TIMESTAMP_FIELDS = ["start_date", "due_date"];
814
818
  for (const f of fields) {
815
- if (params[f] !== undefined)
816
- updates[f] = params[f];
819
+ if (params[f] !== undefined) {
820
+ if (TIMESTAMP_FIELDS.includes(f)) {
821
+ const v = params[f];
822
+ updates[f] = (typeof v === "string" && v.trim() === "") ? null : v;
823
+ }
824
+ else {
825
+ updates[f] = params[f];
826
+ }
827
+ }
817
828
  }
818
829
  // Cycle 13 (BUG 2): keep status and completed_at consistent. Setting status
819
830
  // to DONE stamps completed_at + progress=100. Setting status to anything
@@ -6914,6 +6925,168 @@ supabase, config) {
6914
6925
  api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
6915
6926
  return toolCount;
6916
6927
  }
6928
+ // ═══════════════════════════════════════════════════════════════════════════════
6929
+ // META-TOOL 18: OFIERE_GATE_OPS — Manual approval gates (PM Planning)
6930
+ // ═══════════════════════════════════════════════════════════════════════════════
6931
+ // v4.56.2: inlined from gateOps.ts. OpenClaw host 2026.5.7 tool-discovery only
6932
+ // indexes api.registerTool calls present in the same module its plugin entry
6933
+ // imports; registrations inside transitive modules (e.g. ./gateOps.ts) are
6934
+ // reachable at runtime but invisible to the discovery scan, so the tool router
6935
+ // throws `plugin tool runtime missing` even though boot logs show registration.
6936
+ async function _gateOpsCollectTransitiveSuccessors(supabase, userId, rootTaskId) {
6937
+ const visited = new Set([rootTaskId]);
6938
+ const out = [];
6939
+ let frontier = [rootTaskId];
6940
+ for (let depth = 0; depth < 20 && frontier.length > 0; depth++) {
6941
+ const { data: deps } = await supabase
6942
+ .from("pm_dependencies")
6943
+ .select("successor_id")
6944
+ .in("predecessor_id", frontier)
6945
+ .eq("user_id", userId);
6946
+ const next = [];
6947
+ for (const d of (deps || [])) {
6948
+ const sid = d.successor_id;
6949
+ if (!sid || visited.has(sid))
6950
+ continue;
6951
+ visited.add(sid);
6952
+ next.push(sid);
6953
+ }
6954
+ if (next.length === 0)
6955
+ break;
6956
+ const { data: tasks } = await supabase
6957
+ .from("tasks")
6958
+ .select("id, title")
6959
+ .in("id", next)
6960
+ .eq("user_id", userId);
6961
+ for (const t of (tasks || []))
6962
+ out.push({ id: t.id, title: t.title });
6963
+ frontier = next;
6964
+ }
6965
+ return out;
6966
+ }
6967
+ function registerGateOps(api, supabase, userId, fallbackAgentId) {
6968
+ api.registerTool({
6969
+ name: "OFIERE_GATE_OPS",
6970
+ label: "Ofiere Gate Operations",
6971
+ description: `Approve or reject manual gates blocking task dispatch in the Planning Tab.\n\n` +
6972
+ `Actions:\n` +
6973
+ `- "list_pending": List manual gates pending approval. Optional: agent_id (filter to gates whose blocked task is assigned to this agent).\n` +
6974
+ `- "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` +
6975
+ `- "approve": Approve a gate so the blocked task can dispatch. Required: gate_id, agent_id (the calling chief/staff). Optional: comment.\n` +
6976
+ `- "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` +
6977
+ `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.`,
6978
+ parameters: {
6979
+ type: "object",
6980
+ required: ["action"],
6981
+ properties: {
6982
+ action: { type: "string", enum: ["list_pending", "get", "approve", "reject"], description: "list_pending | get | approve | reject" },
6983
+ gate_id: { type: "string", description: "Gate UUID (required for get, approve, reject)" },
6984
+ 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." },
6985
+ comment: { type: "string", description: "Optional approval comment" },
6986
+ reason: { type: "string", description: "Rejection reason (required for reject, min 5 chars)" },
6987
+ },
6988
+ },
6989
+ async execute(_id, params) {
6990
+ try {
6991
+ const action = params.action;
6992
+ const resolveActor = () => {
6993
+ const id = params.agent_id || fallbackAgentId;
6994
+ return id ? `agent:${id}` : `user:${userId}`;
6995
+ };
6996
+ switch (action) {
6997
+ case "list_pending": {
6998
+ let q = supabase
6999
+ .from("pm_gates")
7000
+ .select("id, plan_id, plan_node_id, gate_label, task_id_blocked, created_at, tasks!inner(title, agent_id)")
7001
+ .eq("user_id", userId)
7002
+ .is("approved_at", null)
7003
+ .is("rejected_at", null)
7004
+ .order("created_at", { ascending: true });
7005
+ if (params.agent_id)
7006
+ q = q.eq("tasks.agent_id", params.agent_id);
7007
+ const { data, error } = await q;
7008
+ if (error)
7009
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
7010
+ return { content: [{ type: "text", text: JSON.stringify({ pending: data || [], count: (data || []).length }, null, 2) }] };
7011
+ }
7012
+ case "get": {
7013
+ if (!params.gate_id)
7014
+ return { content: [{ type: "text", text: `Error: Missing required: gate_id` }] };
7015
+ const { data: gate, error } = await supabase
7016
+ .from("pm_gates")
7017
+ .select("*, tasks!inner(id, title, agent_id, status)")
7018
+ .eq("id", params.gate_id)
7019
+ .eq("user_id", userId)
7020
+ .maybeSingle();
7021
+ if (error)
7022
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
7023
+ if (!gate)
7024
+ return { content: [{ type: "text", text: `Error: Gate not found` }] };
7025
+ const blockedTaskId = gate.task_id_blocked;
7026
+ const downstream = await _gateOpsCollectTransitiveSuccessors(supabase, userId, blockedTaskId);
7027
+ const blockedTitle = gate.tasks?.title || "blocked task";
7028
+ return { content: [{ type: "text", text: JSON.stringify({
7029
+ gate,
7030
+ impact_on_reject: {
7031
+ failed_task: { id: blockedTaskId, title: blockedTitle },
7032
+ blocked_downstream: downstream,
7033
+ summary: `Rejecting will FAIL '${blockedTitle}' and BLOCK ${downstream.length} downstream task${downstream.length === 1 ? "" : "s"}.`,
7034
+ },
7035
+ }, null, 2) }] };
7036
+ }
7037
+ case "approve": {
7038
+ if (!params.gate_id)
7039
+ return { content: [{ type: "text", text: `Error: Missing required: gate_id` }] };
7040
+ if (!params.agent_id && !fallbackAgentId)
7041
+ return { content: [{ type: "text", text: `Error: Missing required: agent_id (calling agent identity — plugin has no implicit agent context)` }] };
7042
+ const actor = resolveActor();
7043
+ const { data, error } = await supabase
7044
+ .from("pm_gates")
7045
+ .update({
7046
+ approved_at: new Date().toISOString(),
7047
+ approved_by: actor,
7048
+ approval_comment: params.comment ?? null,
7049
+ })
7050
+ .eq("id", params.gate_id)
7051
+ .eq("user_id", userId)
7052
+ .is("approved_at", null)
7053
+ .is("rejected_at", null)
7054
+ .select("id");
7055
+ if (error)
7056
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
7057
+ if (!data || data.length === 0)
7058
+ return { content: [{ type: "text", text: `Error: Gate not found or already resolved` }] };
7059
+ return { content: [{ type: "text", text: JSON.stringify({ approved: true, gate_id: params.gate_id, approved_by: actor }, null, 2) }] };
7060
+ }
7061
+ case "reject": {
7062
+ if (!params.gate_id)
7063
+ return { content: [{ type: "text", text: `Error: Missing required: gate_id` }] };
7064
+ if (!params.agent_id && !fallbackAgentId)
7065
+ return { content: [{ type: "text", text: `Error: Missing required: agent_id (calling agent identity — plugin has no implicit agent context)` }] };
7066
+ const reason = (params.reason || "").trim();
7067
+ if (reason.length < 5)
7068
+ return { content: [{ type: "text", text: `Error: Missing required: reason (min 5 chars) — rejection requires explanation` }] };
7069
+ const actor = resolveActor();
7070
+ const { data, error } = await supabase.rpc("cascade_reject_gate", {
7071
+ p_gate_id: params.gate_id,
7072
+ p_user_id: userId,
7073
+ p_reason: reason,
7074
+ p_actor: actor,
7075
+ });
7076
+ if (error)
7077
+ return { content: [{ type: "text", text: `Error: ${error.message}` }] };
7078
+ return { content: [{ type: "text", text: JSON.stringify({ rejected: true, gate_id: params.gate_id, rejected_by: actor, result: data }, null, 2) }] };
7079
+ }
7080
+ default:
7081
+ return { content: [{ type: "text", text: `Error: Unknown gate action: ${action}` }] };
7082
+ }
7083
+ }
7084
+ catch (e) {
7085
+ return { content: [{ type: "text", text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
7086
+ }
7087
+ },
7088
+ });
7089
+ }
6917
7090
  // ── Server-Side Brain Extraction (agent_end hook) ─────────────────────────
6918
7091
  // Fires after EVERY agent turn completes, regardless of channel.
6919
7092
  // Extracts L1 raw fragments and lightweight L2 summaries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.56.1",
3
+ "version": "4.56.3",
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
 
@@ -901,8 +900,20 @@ async function handleUpdateTask(
901
900
  "title", "description", "status", "priority", "progress",
902
901
  "agent_id", "start_date", "due_date", "tags",
903
902
  ];
903
+ // Fix #8 (2026-05-17): Postgres rejects empty-string for timestamptz columns,
904
+ // so LLM callers passing start_date/due_date="" to clear a stale value would
905
+ // error instead of nulling the column. Coerce empty/whitespace → null here
906
+ // to match the dashboard native executor (see tool-executor.ts handleTaskOps).
907
+ const TIMESTAMP_FIELDS = ["start_date", "due_date"];
904
908
  for (const f of fields) {
905
- if (params[f] !== undefined) updates[f] = params[f];
909
+ if (params[f] !== undefined) {
910
+ if (TIMESTAMP_FIELDS.includes(f)) {
911
+ const v = params[f];
912
+ updates[f] = (typeof v === "string" && v.trim() === "") ? null : v;
913
+ } else {
914
+ updates[f] = params[f];
915
+ }
916
+ }
906
917
  }
907
918
  // Cycle 13 (BUG 2): keep status and completed_at consistent. Setting status
908
919
  // to DONE stamps completed_at + progress=100. Setting status to anything
@@ -7081,6 +7092,169 @@ export function registerTools(
7081
7092
  return toolCount;
7082
7093
  }
7083
7094
 
7095
+ // ═══════════════════════════════════════════════════════════════════════════════
7096
+ // META-TOOL 18: OFIERE_GATE_OPS — Manual approval gates (PM Planning)
7097
+ // ═══════════════════════════════════════════════════════════════════════════════
7098
+ // v4.56.2: inlined from gateOps.ts. OpenClaw host 2026.5.7 tool-discovery only
7099
+ // indexes api.registerTool calls present in the same module its plugin entry
7100
+ // imports; registrations inside transitive modules (e.g. ./gateOps.ts) are
7101
+ // reachable at runtime but invisible to the discovery scan, so the tool router
7102
+ // throws `plugin tool runtime missing` even though boot logs show registration.
7103
+
7104
+ async function _gateOpsCollectTransitiveSuccessors(
7105
+ supabase: SupabaseClient,
7106
+ userId: string,
7107
+ rootTaskId: string,
7108
+ ): Promise<{ id: string; title: string }[]> {
7109
+ const visited = new Set<string>([rootTaskId]);
7110
+ const out: { id: string; title: string }[] = [];
7111
+ let frontier: string[] = [rootTaskId];
7112
+ for (let depth = 0; depth < 20 && frontier.length > 0; depth++) {
7113
+ const { data: deps } = await supabase
7114
+ .from("pm_dependencies")
7115
+ .select("successor_id")
7116
+ .in("predecessor_id", frontier)
7117
+ .eq("user_id", userId);
7118
+ const next: string[] = [];
7119
+ for (const d of (deps || [])) {
7120
+ const sid = (d as any).successor_id as string;
7121
+ if (!sid || visited.has(sid)) continue;
7122
+ visited.add(sid);
7123
+ next.push(sid);
7124
+ }
7125
+ if (next.length === 0) break;
7126
+ const { data: tasks } = await supabase
7127
+ .from("tasks")
7128
+ .select("id, title")
7129
+ .in("id", next)
7130
+ .eq("user_id", userId);
7131
+ for (const t of (tasks || [])) out.push({ id: (t as any).id, title: (t as any).title });
7132
+ frontier = next;
7133
+ }
7134
+ return out;
7135
+ }
7136
+
7137
+ function registerGateOps(
7138
+ api: any,
7139
+ supabase: SupabaseClient,
7140
+ userId: string,
7141
+ fallbackAgentId: string | undefined,
7142
+ ): void {
7143
+ api.registerTool({
7144
+ name: "OFIERE_GATE_OPS",
7145
+ label: "Ofiere Gate Operations",
7146
+ description:
7147
+ `Approve or reject manual gates blocking task dispatch in the Planning Tab.\n\n` +
7148
+ `Actions:\n` +
7149
+ `- "list_pending": List manual gates pending approval. Optional: agent_id (filter to gates whose blocked task is assigned to this agent).\n` +
7150
+ `- "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` +
7151
+ `- "approve": Approve a gate so the blocked task can dispatch. Required: gate_id, agent_id (the calling chief/staff). Optional: comment.\n` +
7152
+ `- "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` +
7153
+ `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.`,
7154
+ parameters: {
7155
+ type: "object",
7156
+ required: ["action"],
7157
+ properties: {
7158
+ action: { type: "string", enum: ["list_pending", "get", "approve", "reject"], description: "list_pending | get | approve | reject" },
7159
+ gate_id: { type: "string", description: "Gate UUID (required for get, approve, reject)" },
7160
+ 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." },
7161
+ comment: { type: "string", description: "Optional approval comment" },
7162
+ reason: { type: "string", description: "Rejection reason (required for reject, min 5 chars)" },
7163
+ },
7164
+ },
7165
+ async execute(_id: string, params: Record<string, unknown>) {
7166
+ try {
7167
+ const action = params.action as string;
7168
+ const resolveActor = (): string => {
7169
+ const id = (params.agent_id as string) || fallbackAgentId;
7170
+ return id ? `agent:${id}` : `user:${userId}`;
7171
+ };
7172
+
7173
+ switch (action) {
7174
+ case "list_pending": {
7175
+ let q = supabase
7176
+ .from("pm_gates")
7177
+ .select("id, plan_id, plan_node_id, gate_label, task_id_blocked, created_at, tasks!inner(title, agent_id)")
7178
+ .eq("user_id", userId)
7179
+ .is("approved_at", null)
7180
+ .is("rejected_at", null)
7181
+ .order("created_at", { ascending: true });
7182
+ if (params.agent_id) q = q.eq("tasks.agent_id", params.agent_id as string);
7183
+ const { data, error } = await q;
7184
+ if (error) return { content: [{ type: "text" as const, text: `Error: ${error.message}` }] };
7185
+ return { content: [{ type: "text" as const, text: JSON.stringify({ pending: data || [], count: (data || []).length }, null, 2) }] };
7186
+ }
7187
+
7188
+ case "get": {
7189
+ if (!params.gate_id) return { content: [{ type: "text" as const, text: `Error: Missing required: gate_id` }] };
7190
+ const { data: gate, error } = await supabase
7191
+ .from("pm_gates")
7192
+ .select("*, tasks!inner(id, title, agent_id, status)")
7193
+ .eq("id", params.gate_id as string)
7194
+ .eq("user_id", userId)
7195
+ .maybeSingle();
7196
+ if (error) return { content: [{ type: "text" as const, text: `Error: ${error.message}` }] };
7197
+ if (!gate) return { content: [{ type: "text" as const, text: `Error: Gate not found` }] };
7198
+ const blockedTaskId = (gate as any).task_id_blocked as string;
7199
+ const downstream = await _gateOpsCollectTransitiveSuccessors(supabase, userId, blockedTaskId);
7200
+ const blockedTitle = (gate as any).tasks?.title || "blocked task";
7201
+ return { content: [{ type: "text" as const, text: JSON.stringify({
7202
+ gate,
7203
+ impact_on_reject: {
7204
+ failed_task: { id: blockedTaskId, title: blockedTitle },
7205
+ blocked_downstream: downstream,
7206
+ summary: `Rejecting will FAIL '${blockedTitle}' and BLOCK ${downstream.length} downstream task${downstream.length === 1 ? "" : "s"}.`,
7207
+ },
7208
+ }, null, 2) }] };
7209
+ }
7210
+
7211
+ case "approve": {
7212
+ if (!params.gate_id) return { content: [{ type: "text" as const, text: `Error: Missing required: gate_id` }] };
7213
+ 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)` }] };
7214
+ const actor = resolveActor();
7215
+ const { data, error } = await supabase
7216
+ .from("pm_gates")
7217
+ .update({
7218
+ approved_at: new Date().toISOString(),
7219
+ approved_by: actor,
7220
+ approval_comment: (params.comment as string) ?? null,
7221
+ })
7222
+ .eq("id", params.gate_id as string)
7223
+ .eq("user_id", userId)
7224
+ .is("approved_at", null)
7225
+ .is("rejected_at", null)
7226
+ .select("id");
7227
+ if (error) return { content: [{ type: "text" as const, text: `Error: ${error.message}` }] };
7228
+ if (!data || data.length === 0) return { content: [{ type: "text" as const, text: `Error: Gate not found or already resolved` }] };
7229
+ return { content: [{ type: "text" as const, text: JSON.stringify({ approved: true, gate_id: params.gate_id, approved_by: actor }, null, 2) }] };
7230
+ }
7231
+
7232
+ case "reject": {
7233
+ if (!params.gate_id) return { content: [{ type: "text" as const, text: `Error: Missing required: gate_id` }] };
7234
+ 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)` }] };
7235
+ const reason = ((params.reason as string) || "").trim();
7236
+ if (reason.length < 5) return { content: [{ type: "text" as const, text: `Error: Missing required: reason (min 5 chars) — rejection requires explanation` }] };
7237
+ const actor = resolveActor();
7238
+ const { data, error } = await supabase.rpc("cascade_reject_gate", {
7239
+ p_gate_id: params.gate_id as string,
7240
+ p_user_id: userId,
7241
+ p_reason: reason,
7242
+ p_actor: actor,
7243
+ });
7244
+ if (error) return { content: [{ type: "text" as const, text: `Error: ${error.message}` }] };
7245
+ return { content: [{ type: "text" as const, text: JSON.stringify({ rejected: true, gate_id: params.gate_id, rejected_by: actor, result: data }, null, 2) }] };
7246
+ }
7247
+
7248
+ default:
7249
+ return { content: [{ type: "text" as const, text: `Error: Unknown gate action: ${action}` }] };
7250
+ }
7251
+ } catch (e) {
7252
+ return { content: [{ type: "text" as const, text: `Error: ${e instanceof Error ? e.message : String(e)}` }] };
7253
+ }
7254
+ },
7255
+ });
7256
+ }
7257
+
7084
7258
  // ── Server-Side Brain Extraction (agent_end hook) ─────────────────────────
7085
7259
  // Fires after EVERY agent turn completes, regardless of channel.
7086
7260
  // 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
- }