ofiere-openclaw-plugin 4.54.2 → 4.56.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.
- package/dist/src/gateOps.js +169 -0
- package/dist/src/planExecute.js +145 -0
- package/dist/src/staffPersona.js +3 -1
- package/dist/src/tools.js +143 -60
- package/package.json +13 -3
- package/src/gateOps.ts +177 -0
- package/src/planExecute.ts +197 -0
- package/src/staffPersona.ts +299 -289
- package/src/tools.ts +142 -59
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
// MIRROR — sync manually with dashboard/lib/pm/planExecute.ts
|
|
2
|
+
// Plugin is a separate npm package and cannot import from dashboard/.
|
|
3
|
+
// If you edit either file, update both. Helper algorithm must stay byte-identical
|
|
4
|
+
// across runtimes so pm_dependencies + pm_gates rows have the same shape regardless
|
|
5
|
+
// of whether the plan was executed via dashboard / native ofiere_plan_ops / plugin OFIERE_PLAN_OPS.
|
|
6
|
+
/**
|
|
7
|
+
* Walk plan tree → emit concrete predecessor edges + manual-gate rows.
|
|
8
|
+
*
|
|
9
|
+
* Predecessor resolution (per task/milestone node, walking UP from succ):
|
|
10
|
+
* - Root + idx=0 → no predecessor
|
|
11
|
+
* - Root + idx>0 → leaves of rootSibling[idx-1]
|
|
12
|
+
* - Parent.parallel = true + parent is task → predecessor = parent
|
|
13
|
+
* - Parent.parallel = true + parent is gate → recurse upward through gate
|
|
14
|
+
* - Non-parallel parent + idx>0 → leaves of parent.children[idx-1]
|
|
15
|
+
* - Non-parallel parent + idx=0 → parent (recurse if gate)
|
|
16
|
+
*
|
|
17
|
+
* Group-id rule: if any gate on the traversal path has
|
|
18
|
+
* `gateCondition='any_predecessor_complete'`, all resolved sources for this
|
|
19
|
+
* successor share one UUID-flavored group_id (dispatcher treats as OR-join).
|
|
20
|
+
*
|
|
21
|
+
* Manual-gate emission: for each task/milestone, walk UP through `parentOf`
|
|
22
|
+
* chain and emit one pm_gates row for every `manual` gate ancestor. Nested
|
|
23
|
+
* manual gates produce N rows; dispatcher AND-joins them.
|
|
24
|
+
*/
|
|
25
|
+
export function expandPlanToDependencyEdges(rootNodes, idMap) {
|
|
26
|
+
const edges = [];
|
|
27
|
+
const gates = [];
|
|
28
|
+
const emptyGates = [];
|
|
29
|
+
const seenEdgeKey = new Set();
|
|
30
|
+
const seenGateKey = new Set();
|
|
31
|
+
const parentOf = new Map();
|
|
32
|
+
const indexInParent = new Map();
|
|
33
|
+
const allNodes = [];
|
|
34
|
+
function indexTree(nodes, parent) {
|
|
35
|
+
nodes.forEach((n, i) => {
|
|
36
|
+
parentOf.set(n.id, parent);
|
|
37
|
+
indexInParent.set(n.id, i);
|
|
38
|
+
allNodes.push(n);
|
|
39
|
+
if (Array.isArray(n.children) && n.children.length > 0)
|
|
40
|
+
indexTree(n.children, n);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
indexTree(rootNodes, null);
|
|
44
|
+
// Resolve a node (possibly a gate) to its task-leaf set, tracking gates traversed.
|
|
45
|
+
function leavesOf(n, gatesAbove) {
|
|
46
|
+
if (n.type === 'task' || n.type === 'milestone') {
|
|
47
|
+
const tid = idMap.get(n.id);
|
|
48
|
+
return tid ? [{ taskId: tid, gateChain: gatesAbove }] : [];
|
|
49
|
+
}
|
|
50
|
+
if (n.type === 'gate') {
|
|
51
|
+
const nextGates = [...gatesAbove, n];
|
|
52
|
+
const out = [];
|
|
53
|
+
for (const c of (n.children || []))
|
|
54
|
+
out.push(...leavesOf(c, nextGates));
|
|
55
|
+
if (out.length === 0)
|
|
56
|
+
emptyGates.push(n.id);
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
// Walk UP from succ to find predecessor leaves + whether any gate on the path is OR-join.
|
|
62
|
+
function predecessorsFor(succ) {
|
|
63
|
+
let orJoin = false;
|
|
64
|
+
let cursor = succ;
|
|
65
|
+
while (cursor) {
|
|
66
|
+
const parent = parentOf.get(cursor.id) ?? null;
|
|
67
|
+
const idx = indexInParent.get(cursor.id) ?? 0;
|
|
68
|
+
if (!parent) {
|
|
69
|
+
// root-level sibling chain
|
|
70
|
+
if (idx === 0)
|
|
71
|
+
return { sources: [], orJoin };
|
|
72
|
+
return { sources: leavesOf(rootNodes[idx - 1], []), orJoin };
|
|
73
|
+
}
|
|
74
|
+
// OR-detection happens whenever we traverse into a gate ancestor
|
|
75
|
+
if (parent.type === 'gate' && parent.gateCondition === 'any_predecessor_complete') {
|
|
76
|
+
orJoin = true;
|
|
77
|
+
}
|
|
78
|
+
if (parent.parallel) {
|
|
79
|
+
// parallel fan-out: predecessor IS the parent
|
|
80
|
+
if (parent.type === 'gate') {
|
|
81
|
+
cursor = parent;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const pid = idMap.get(parent.id);
|
|
85
|
+
return pid ? { sources: [{ taskId: pid, gateChain: [] }], orJoin } : { sources: [], orJoin };
|
|
86
|
+
}
|
|
87
|
+
if (idx > 0) {
|
|
88
|
+
// non-parallel: pred is the previous sibling (may itself be a gate)
|
|
89
|
+
return { sources: leavesOf(parent.children[idx - 1], []), orJoin };
|
|
90
|
+
}
|
|
91
|
+
// first child of non-parallel parent → pred is the parent (recurse if gate)
|
|
92
|
+
if (parent.type === 'gate') {
|
|
93
|
+
cursor = parent;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const pid = idMap.get(parent.id);
|
|
97
|
+
return pid ? { sources: [{ taskId: pid, gateChain: [] }], orJoin } : { sources: [], orJoin };
|
|
98
|
+
}
|
|
99
|
+
return { sources: [], orJoin };
|
|
100
|
+
}
|
|
101
|
+
for (const node of allNodes) {
|
|
102
|
+
if (node.type !== 'task' && node.type !== 'milestone')
|
|
103
|
+
continue;
|
|
104
|
+
const succTaskId = idMap.get(node.id);
|
|
105
|
+
if (!succTaskId)
|
|
106
|
+
continue;
|
|
107
|
+
const { sources, orJoin: orJoinFromWalk } = predecessorsFor(node);
|
|
108
|
+
// OR-join also fires if the resolved leaves passed THROUGH an OR gate on their way up
|
|
109
|
+
// (e.g., succ is a root-sibling downstream of an OR gate — leavesOf walks INTO the gate).
|
|
110
|
+
const orJoinFromLeaves = sources.some(s => s.gateChain.some(g => g.type === 'gate' && g.gateCondition === 'any_predecessor_complete'));
|
|
111
|
+
const orJoin = orJoinFromWalk || orJoinFromLeaves;
|
|
112
|
+
const groupId = (orJoin && sources.length > 1) ? `grp-${succTaskId}-${node.id.slice(-6)}` : null;
|
|
113
|
+
for (const s of sources) {
|
|
114
|
+
const key = `${s.taskId}->${succTaskId}|${groupId ?? ''}`;
|
|
115
|
+
if (seenEdgeKey.has(key))
|
|
116
|
+
continue;
|
|
117
|
+
seenEdgeKey.add(key);
|
|
118
|
+
edges.push({
|
|
119
|
+
predTaskId: s.taskId,
|
|
120
|
+
succTaskId,
|
|
121
|
+
groupId,
|
|
122
|
+
lagDays: 0,
|
|
123
|
+
dependencyType: 'finish_to_start',
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
// Manual-gate rows: walk UP through every ancestor gate; emit one row per manual gate.
|
|
127
|
+
let cur = parentOf.get(node.id) || null;
|
|
128
|
+
while (cur) {
|
|
129
|
+
if (cur.type === 'gate' && cur.gateCondition === 'manual') {
|
|
130
|
+
const k = `${cur.id}::${succTaskId}`;
|
|
131
|
+
if (!seenGateKey.has(k)) {
|
|
132
|
+
seenGateKey.add(k);
|
|
133
|
+
gates.push({
|
|
134
|
+
planNodeId: cur.id,
|
|
135
|
+
gateLabel: cur.title || 'Manual gate',
|
|
136
|
+
gateCondition: 'manual',
|
|
137
|
+
taskIdBlocked: succTaskId,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
cur = parentOf.get(cur.id) || null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { edges, gates, emptyGates };
|
|
145
|
+
}
|
package/dist/src/staffPersona.js
CHANGED
|
@@ -36,7 +36,7 @@ export async function loadDispatchContextBySession(supabase, userId, sessionKey)
|
|
|
36
36
|
return null;
|
|
37
37
|
const { data, error } = await supabase
|
|
38
38
|
.from("dispatch_context")
|
|
39
|
-
.select("subagent_id, attached_sop_ids, attached_framework_ids, task_id, conversation_id")
|
|
39
|
+
.select("subagent_id, attached_sop_ids, attached_framework_ids, task_id, conversation_id, source_agent_id, a2a_pair_id")
|
|
40
40
|
.eq("user_id", userId)
|
|
41
41
|
.eq("session_key", sessionKey)
|
|
42
42
|
.order("created_at", { ascending: false })
|
|
@@ -54,6 +54,8 @@ export async function loadDispatchContextBySession(supabase, userId, sessionKey)
|
|
|
54
54
|
: [],
|
|
55
55
|
task_id: data.task_id ?? null,
|
|
56
56
|
conversation_id: data.conversation_id ?? null,
|
|
57
|
+
source_agent_id: data.source_agent_id ?? null,
|
|
58
|
+
a2a_pair_id: data.a2a_pair_id ?? null,
|
|
57
59
|
};
|
|
58
60
|
}
|
|
59
61
|
const SLOTS = [
|
package/dist/src/tools.js
CHANGED
|
@@ -16,6 +16,8 @@ import { resolveAgentId } from "./agent-resolver.js";
|
|
|
16
16
|
import { handleProposeAttach, handleCommitAttach, registerAttachmentContextHook, } from "./attachments.js";
|
|
17
17
|
import { invalidateAgentTier } from "./agent-tier.js";
|
|
18
18
|
import { loadSubagentRow, readDispatchSubagentId, loadDispatchContextBySession } from "./staffPersona.js";
|
|
19
|
+
import { expandPlanToDependencyEdges } from "./planExecute.js";
|
|
20
|
+
import { registerGateOps } from "./gateOps.js";
|
|
19
21
|
function ok(data) {
|
|
20
22
|
return {
|
|
21
23
|
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
@@ -4867,7 +4869,7 @@ async function handleExecutePlan(supabase, userId, resolveAgent, params) {
|
|
|
4867
4869
|
let tasksCreated = 0;
|
|
4868
4870
|
while (queue.length > 0) {
|
|
4869
4871
|
const { node, parentTaskId } = queue.shift();
|
|
4870
|
-
if (node.type === "task") {
|
|
4872
|
+
if (node.type === "task" || node.type === "milestone") {
|
|
4871
4873
|
const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
4872
4874
|
const now = new Date().toISOString();
|
|
4873
4875
|
const agentId = node.agentId ? await resolveAgent(node.agentId) : null;
|
|
@@ -4928,76 +4930,45 @@ async function handleExecutePlan(supabase, userId, resolveAgent, params) {
|
|
|
4928
4930
|
}
|
|
4929
4931
|
}
|
|
4930
4932
|
}
|
|
4931
|
-
// Step 3:
|
|
4932
|
-
|
|
4933
|
+
// Step 3: Build dependencies + manual-gate rows via shared helper.
|
|
4934
|
+
// The helper handles gate-skip-through (Option A), OR-join via group_id (Option B),
|
|
4935
|
+
// and manual gates via pm_gates rows (Option C). See dashboard/lib/pm/planExecute.ts.
|
|
4936
|
+
const { edges, gates: gateRows, emptyGates } = expandPlanToDependencyEdges(rootNodes, idMap);
|
|
4933
4937
|
let depsCreated = 0;
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
return idMap.get(n.id);
|
|
4937
|
-
}
|
|
4938
|
-
// Helper: insert a dependency row into pm_dependencies
|
|
4939
|
-
async function insertDep(predId, succId) {
|
|
4938
|
+
let gatesCreated = 0;
|
|
4939
|
+
for (const e of edges) {
|
|
4940
4940
|
const { error } = await supabase.from("pm_dependencies").insert({
|
|
4941
4941
|
id: `dep-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
4942
|
-
user_id: userId,
|
|
4943
|
-
|
|
4942
|
+
user_id: userId,
|
|
4943
|
+
predecessor_id: e.predTaskId,
|
|
4944
|
+
successor_id: e.succTaskId,
|
|
4945
|
+
dependency_type: e.dependencyType,
|
|
4946
|
+
lag_days: e.lagDays,
|
|
4947
|
+
group_id: e.groupId,
|
|
4944
4948
|
});
|
|
4945
|
-
|
|
4946
|
-
}
|
|
4947
|
-
while (depsQueue.length > 0) {
|
|
4948
|
-
const node = depsQueue.shift();
|
|
4949
|
-
const nodeRealId = resolveTaskId(node);
|
|
4950
|
-
// Chain siblings: sequential children of this node get chained left-to-right
|
|
4951
|
-
if (Array.isArray(node.children) && node.children.length > 1 && !node.parallel) {
|
|
4952
|
-
const taskChildren = [];
|
|
4953
|
-
for (const child of node.children) {
|
|
4954
|
-
const cid = resolveTaskId(child);
|
|
4955
|
-
if (cid)
|
|
4956
|
-
taskChildren.push(cid);
|
|
4957
|
-
}
|
|
4958
|
-
for (let i = 1; i < taskChildren.length; i++) {
|
|
4959
|
-
if (await insertDep(taskChildren[i - 1], taskChildren[i]))
|
|
4960
|
-
depsCreated++;
|
|
4961
|
-
}
|
|
4962
|
-
}
|
|
4963
|
-
// Parent → first child link (sequential only)
|
|
4964
|
-
if (nodeRealId && !node.parallel && node.children?.length > 0) {
|
|
4965
|
-
const firstChildId = resolveTaskId(node.children[0]);
|
|
4966
|
-
if (firstChildId) {
|
|
4967
|
-
if (await insertDep(nodeRealId, firstChildId))
|
|
4968
|
-
depsCreated++;
|
|
4969
|
-
}
|
|
4970
|
-
}
|
|
4971
|
-
// Parallel fan-out: parent → each child
|
|
4972
|
-
if (nodeRealId && node.parallel && node.children?.length > 0) {
|
|
4973
|
-
for (const child of node.children) {
|
|
4974
|
-
const childId = resolveTaskId(child);
|
|
4975
|
-
if (childId) {
|
|
4976
|
-
if (await insertDep(nodeRealId, childId))
|
|
4977
|
-
depsCreated++;
|
|
4978
|
-
}
|
|
4979
|
-
}
|
|
4980
|
-
}
|
|
4981
|
-
if (Array.isArray(node.children))
|
|
4982
|
-
depsQueue.push(...node.children);
|
|
4983
|
-
}
|
|
4984
|
-
// Chain root-level siblings (sequential root nodes)
|
|
4985
|
-
const rootTaskIds = [];
|
|
4986
|
-
for (const rn of rootNodes) {
|
|
4987
|
-
const rid = resolveTaskId(rn);
|
|
4988
|
-
if (rid)
|
|
4989
|
-
rootTaskIds.push(rid);
|
|
4990
|
-
}
|
|
4991
|
-
for (let i = 1; i < rootTaskIds.length; i++) {
|
|
4992
|
-
if (await insertDep(rootTaskIds[i - 1], rootTaskIds[i]))
|
|
4949
|
+
if (!error)
|
|
4993
4950
|
depsCreated++;
|
|
4994
4951
|
}
|
|
4952
|
+
for (const g of gateRows) {
|
|
4953
|
+
const { error } = await supabase.from("pm_gates").insert({
|
|
4954
|
+
user_id: userId,
|
|
4955
|
+
plan_id: params.plan_id,
|
|
4956
|
+
plan_node_id: g.planNodeId,
|
|
4957
|
+
gate_label: g.gateLabel,
|
|
4958
|
+
gate_condition: g.gateCondition,
|
|
4959
|
+
task_id_blocked: g.taskIdBlocked,
|
|
4960
|
+
});
|
|
4961
|
+
if (!error)
|
|
4962
|
+
gatesCreated++;
|
|
4963
|
+
}
|
|
4995
4964
|
// Mark plan as deployed
|
|
4996
4965
|
await supabase.from("pm_plans").update({ is_deployed: true, deployed_at: new Date().toISOString(), updated_at: new Date().toISOString() }).eq("id", params.plan_id).eq("user_id", userId);
|
|
4997
4966
|
return ok({
|
|
4998
4967
|
message: `Plan "${data.name || plan.name}" executed successfully`,
|
|
4999
4968
|
tasks_created: tasksCreated,
|
|
5000
4969
|
dependencies_created: depsCreated,
|
|
4970
|
+
gates_created: gatesCreated,
|
|
4971
|
+
empty_gates: emptyGates,
|
|
5001
4972
|
folder_id: folderId,
|
|
5002
4973
|
space_id: spaceId,
|
|
5003
4974
|
...(folderSkipped ? { folder_skipped_reason: "No space_id provided — assign the plan to a space to enable folder creation" } : {}),
|
|
@@ -6927,6 +6898,7 @@ supabase, config) {
|
|
|
6927
6898
|
registerTalentOps(api, supabase, userId); // 15
|
|
6928
6899
|
registerFrameworkOps(api, supabase, userId, resolveAgent); // 16
|
|
6929
6900
|
registerOfficeOps(api, supabase, userId, resolveAgent); // 17
|
|
6901
|
+
registerGateOps(api, supabase, userId, fallbackAgentId); // 18
|
|
6930
6902
|
// ── Register dynamic brain context hook ──
|
|
6931
6903
|
registerBrainContextHook(api, supabase, userId, fallbackAgentId);
|
|
6932
6904
|
// ── Register talent context hook ──
|
|
@@ -6936,7 +6908,7 @@ supabase, config) {
|
|
|
6936
6908
|
// ── Register agent_end hook for server-side brain extraction ──
|
|
6937
6909
|
registerBrainExtractionHook(api, supabase, userId, fallbackAgentId);
|
|
6938
6910
|
// ── Count and log ──
|
|
6939
|
-
const toolCount =
|
|
6911
|
+
const toolCount = 18;
|
|
6940
6912
|
const callerName = getCallingAgentName(api);
|
|
6941
6913
|
const agentLabel = fallbackAgentId || callerName || "auto-detect";
|
|
6942
6914
|
api.logger.info(`[ofiere] ${toolCount} meta-tools registered (agent: ${agentLabel})`);
|
|
@@ -7214,6 +7186,117 @@ function registerBrainExtractionHook(api, supabase, userId, fallbackAgentId) {
|
|
|
7214
7186
|
// not the chief's. The chief gets a single L4 report entry above.
|
|
7215
7187
|
return;
|
|
7216
7188
|
}
|
|
7189
|
+
// FIX-A2A-LATE-REPLY — direct chief↔chief A2A late reply.
|
|
7190
|
+
// Dashboard's relayToOpenClaw caps the synchronous wait at 45s; when
|
|
7191
|
+
// the responding chief takes longer (e.g. while running its own tool
|
|
7192
|
+
// calls), the dashboard WS closes and the eventual reply is dropped.
|
|
7193
|
+
// dispatch_context was pre-written with { conversation_id,
|
|
7194
|
+
// source_agent_id, a2a_pair_id } so we can route the late reply back:
|
|
7195
|
+
// 1. Insert the assistant turn into conversation_messages for the
|
|
7196
|
+
// A2A conversation so reloading the page shows the reply.
|
|
7197
|
+
// 2. POST a `chief_reply` webhook so the dashboard SSE can swap the
|
|
7198
|
+
// "still processing" placeholder bubble in the active chat UI.
|
|
7199
|
+
// Guarded by: a2a_pair_id is set AND subagent_id is null AND task_id
|
|
7200
|
+
// is null — so neither the staff nor the scheduled branches above
|
|
7201
|
+
// can collide with this path.
|
|
7202
|
+
const a2aPairId = dispatchCtxRow?.a2a_pair_id ?? null;
|
|
7203
|
+
const a2aSourceAgentId = dispatchCtxRow?.source_agent_id ?? null;
|
|
7204
|
+
const a2aConversationId = dispatchCtxRow?.conversation_id ?? null;
|
|
7205
|
+
if (a2aPairId &&
|
|
7206
|
+
!staffDispatchSubagentId &&
|
|
7207
|
+
!dispatchTaskIdFromCtx &&
|
|
7208
|
+
a2aConversationId) {
|
|
7209
|
+
(async () => {
|
|
7210
|
+
try {
|
|
7211
|
+
const excerpt = lastAssistant.slice(0, 4000);
|
|
7212
|
+
if (!excerpt) {
|
|
7213
|
+
api.logger.warn?.(`[ofiere-a2a-reply] empty assistant text — skipping late-reply persistence`);
|
|
7214
|
+
return;
|
|
7215
|
+
}
|
|
7216
|
+
// Insert assistant turn into the A2A conversation. The
|
|
7217
|
+
// dashboard's synchronous path also writes the assistant row
|
|
7218
|
+
// when the reply lands in-window — UI dedup is by content +
|
|
7219
|
+
// a2a_conversation_id so a duplicate from a race is benign.
|
|
7220
|
+
try {
|
|
7221
|
+
await supabase.from("conversation_messages").insert({
|
|
7222
|
+
conversation_id: a2aConversationId,
|
|
7223
|
+
user_id: userId,
|
|
7224
|
+
role: "assistant",
|
|
7225
|
+
content: excerpt,
|
|
7226
|
+
metadata: {
|
|
7227
|
+
sender_agent_id: resolvedAgentId,
|
|
7228
|
+
target_agent_id: a2aSourceAgentId,
|
|
7229
|
+
late_reply: true,
|
|
7230
|
+
},
|
|
7231
|
+
branch_id: "main",
|
|
7232
|
+
});
|
|
7233
|
+
await supabase.from("conversations")
|
|
7234
|
+
.update({ updated_at: new Date().toISOString() })
|
|
7235
|
+
.eq("id", a2aConversationId)
|
|
7236
|
+
.eq("user_id", userId);
|
|
7237
|
+
// Best-effort message-count bump. RPC may not exist on older
|
|
7238
|
+
// schemas — swallow the error if it 404s.
|
|
7239
|
+
try {
|
|
7240
|
+
await supabase.rpc("increment_conversation_message_count", {
|
|
7241
|
+
conv_id: a2aConversationId,
|
|
7242
|
+
});
|
|
7243
|
+
}
|
|
7244
|
+
catch { /* RPC missing — count drift is acceptable */ }
|
|
7245
|
+
}
|
|
7246
|
+
catch (cmErr) {
|
|
7247
|
+
api.logger.warn?.(`[ofiere-a2a-reply] conversation_messages insert failed: ${cmErr instanceof Error ? cmErr.message : String(cmErr)}`);
|
|
7248
|
+
}
|
|
7249
|
+
// Resolve responder agent name for the webhook payload so the
|
|
7250
|
+
// UI bubble can be attributed correctly without another DB hit.
|
|
7251
|
+
let responderName = null;
|
|
7252
|
+
try {
|
|
7253
|
+
const { data: agentRow } = await supabase
|
|
7254
|
+
.from("agents")
|
|
7255
|
+
.select("name")
|
|
7256
|
+
.eq("id", resolvedAgentId)
|
|
7257
|
+
.eq("user_id", userId)
|
|
7258
|
+
.maybeSingle();
|
|
7259
|
+
responderName = agentRow?.name ?? null;
|
|
7260
|
+
}
|
|
7261
|
+
catch { /* fall back to agent id */ }
|
|
7262
|
+
const webhookUrl = process.env.OFIERE_DASHBOARD_WEBHOOK_URL || "";
|
|
7263
|
+
const webhookSecret = process.env.OPENCLAW_WEBHOOK_SECRET || "";
|
|
7264
|
+
if (webhookUrl && webhookSecret) {
|
|
7265
|
+
try {
|
|
7266
|
+
await fetch(webhookUrl, {
|
|
7267
|
+
method: "POST",
|
|
7268
|
+
headers: {
|
|
7269
|
+
"content-type": "application/json",
|
|
7270
|
+
authorization: `Bearer ${webhookSecret}`,
|
|
7271
|
+
},
|
|
7272
|
+
body: JSON.stringify({
|
|
7273
|
+
type: "chief_reply",
|
|
7274
|
+
payload: {
|
|
7275
|
+
user_id: userId,
|
|
7276
|
+
source_agent_id: a2aSourceAgentId,
|
|
7277
|
+
target_agent_id: resolvedAgentId,
|
|
7278
|
+
target_agent_name: responderName,
|
|
7279
|
+
conversation_id: a2aConversationId,
|
|
7280
|
+
a2a_pair_id: a2aPairId,
|
|
7281
|
+
response: excerpt,
|
|
7282
|
+
},
|
|
7283
|
+
}),
|
|
7284
|
+
});
|
|
7285
|
+
api.logger.info?.(`[ofiere-a2a-reply] chief_reply webhook posted (conv=${a2aConversationId})`);
|
|
7286
|
+
}
|
|
7287
|
+
catch (wErr) {
|
|
7288
|
+
api.logger.debug?.(`[ofiere-a2a-reply] webhook post failed: ${wErr instanceof Error ? wErr.message : String(wErr)}`);
|
|
7289
|
+
}
|
|
7290
|
+
}
|
|
7291
|
+
}
|
|
7292
|
+
catch (aErr) {
|
|
7293
|
+
api.logger.warn?.(`[ofiere-a2a-reply] failed: ${aErr instanceof Error ? aErr.message : String(aErr)}`);
|
|
7294
|
+
}
|
|
7295
|
+
})();
|
|
7296
|
+
// Continue to brain extraction below — A2A late-reply is purely
|
|
7297
|
+
// additive bookkeeping; the responding chief still owns the L1/L2
|
|
7298
|
+
// memory writes for its own turn.
|
|
7299
|
+
}
|
|
7217
7300
|
// BUG 9 fix (BUGSHOOT-2): mark task_dispatch_log row complete for
|
|
7218
7301
|
// non-staff scheduled dispatches too. Without this the log row stays
|
|
7219
7302
|
// at 'dispatched' even after the chief finished the run.
|
package/package.json
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.56.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "OpenClaw plugin for Ofiere PM -
|
|
6
|
-
"keywords": [
|
|
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
|
+
"keywords": [
|
|
7
|
+
"openclaw",
|
|
8
|
+
"ofiere",
|
|
9
|
+
"project-management",
|
|
10
|
+
"agents",
|
|
11
|
+
"plugin"
|
|
12
|
+
],
|
|
7
13
|
"homepage": "https://github.com/gilanggemar/Ofiere",
|
|
8
14
|
"repository": {
|
|
9
15
|
"type": "git",
|
|
10
16
|
"url": "https://github.com/gilanggemar/Ofiere.git"
|
|
11
17
|
},
|
|
12
18
|
"license": "MIT",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=20"
|
|
21
|
+
},
|
|
13
22
|
"main": "./dist/index.js",
|
|
14
23
|
"files": [
|
|
15
24
|
"index.ts",
|
|
@@ -45,6 +54,7 @@
|
|
|
45
54
|
"zod": "^3.25.11"
|
|
46
55
|
},
|
|
47
56
|
"devDependencies": {
|
|
57
|
+
"@types/node": "^25.8.0",
|
|
48
58
|
"typescript": "^5.6.0"
|
|
49
59
|
}
|
|
50
60
|
}
|