ofiere-openclaw-plugin 4.55.0 → 4.56.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +104 -104
- package/dist/src/gateOps.js +169 -0
- package/dist/src/planExecute.js +145 -0
- package/dist/src/prompt.js +130 -130
- package/dist/src/tools.js +32 -60
- package/index.ts +105 -105
- package/openclaw.plugin.json +2 -1
- package/package.json +2 -2
- package/src/agent-resolver.ts +90 -90
- package/src/agent-tier.ts +192 -192
- package/src/attach-token.ts +106 -106
- package/src/attachments.ts +601 -601
- package/src/cli.ts +247 -247
- package/src/config.ts +78 -78
- package/src/gateOps.ts +177 -0
- package/src/planExecute.ts +197 -0
- package/src/prompt.ts +267 -267
- package/src/sop-render.ts +216 -216
- package/src/supabase.ts +13 -13
- package/src/tools.ts +30 -59
- package/src/types/openclaw.d.ts +8 -8
- package/src/types.ts +10 -10
package/README.md
CHANGED
|
@@ -1,104 +1,104 @@
|
|
|
1
|
-
# Ofiere PM Plugin for OpenClaw
|
|
2
|
-
|
|
3
|
-
Manage your Ofiere PM dashboard directly from OpenClaw agents. Create tasks, manage projects, build workflows, store knowledge — all synced to the dashboard in real time.
|
|
4
|
-
|
|
5
|
-
## Quick Install (One-Click)
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
curl -sSL https://ofiere.com/scripts/install.sh | bash -s -- \
|
|
9
|
-
--supabase-url "https://xxx.supabase.co" \
|
|
10
|
-
--service-key "eyJ..." \
|
|
11
|
-
--user-id "your-uuid"
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
Only 3 parameters needed. All agents get the plugin automatically.
|
|
15
|
-
|
|
16
|
-
## Uninstall
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
curl -sSL https://ofiere.com/scripts/uninstall.sh | bash
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## How It Works
|
|
23
|
-
|
|
24
|
-
Once configured, the plugin connects to your Supabase database at gateway startup and registers 9 PM meta-tools directly into the agent. There's no separate MCP server process — it runs inside the OpenClaw gateway for maximum reliability.
|
|
25
|
-
|
|
26
|
-
Changes made by the agent are immediately visible on the Ofiere dashboard (Vercel) via Supabase real-time subscriptions.
|
|
27
|
-
|
|
28
|
-
## AI Meta-Tools
|
|
29
|
-
|
|
30
|
-
The plugin uses a scalable meta-tool architecture. Each tool handles one domain with an `action` parameter to select the operation.
|
|
31
|
-
|
|
32
|
-
| Tool | Actions | Description |
|
|
33
|
-
|---|---|---|
|
|
34
|
-
| `OFIERE_TASK_OPS` | `list`, `create`, `update`, `delete` | Rich task management with execution plans, goals, constraints |
|
|
35
|
-
| `OFIERE_AGENT_OPS` | `list` | Query available agents for task assignment |
|
|
36
|
-
| `OFIERE_PROJECT_OPS` | `list_spaces`, `create_space`, `create_folder`, `bulk_create_tasks`, etc. | Full project hierarchy: spaces → folders → tasks |
|
|
37
|
-
| `OFIERE_SCHEDULE_OPS` | `list`, `create`, `update`, `delete` | Calendar events with recurrence |
|
|
38
|
-
| `OFIERE_KNOWLEDGE_OPS` | `search`, `list`, `create`, `update`, `delete` | Knowledge library with full content retrieval |
|
|
39
|
-
| `OFIERE_WORKFLOW_OPS` | `list`, `get`, `create`, `update`, `delete`, `list_runs`, `trigger` | Visual workflow builder with 16 node types |
|
|
40
|
-
| `OFIERE_NOTIFY_OPS` | `list`, `mark_read`, `create` | In-app notifications |
|
|
41
|
-
| `OFIERE_MEMORY_OPS` | `list_conversations`, `search_knowledge` | Conversation history & agent memory |
|
|
42
|
-
| `OFIERE_PROMPT_OPS` | `list`, `get`, `create`, `update`, `delete` | System prompt chunk management |
|
|
43
|
-
|
|
44
|
-
### Example
|
|
45
|
-
|
|
46
|
-
```
|
|
47
|
-
// Create a task with execution plan
|
|
48
|
-
OFIERE_TASK_OPS({ action: "create", title: "Deploy v2", agent_id: "ivy",
|
|
49
|
-
execution_plan: [{ step: 1, action: "Build", detail: "Run production build" }] })
|
|
50
|
-
|
|
51
|
-
// Create a workflow with nodes
|
|
52
|
-
OFIERE_WORKFLOW_OPS({ action: "create", name: "Deploy Pipeline",
|
|
53
|
-
nodes: [
|
|
54
|
-
{ type: "agent_step", data: { label: "Build", task: "Run npm build" } },
|
|
55
|
-
{ type: "human_approval", data: { label: "Review", instructions: "Check build output" } },
|
|
56
|
-
{ type: "output", data: { label: "Done" } }
|
|
57
|
-
],
|
|
58
|
-
edges: [
|
|
59
|
-
{ source: "agent_step-...", target: "human_approval-..." },
|
|
60
|
-
{ source: "human_approval-...", target: "output-..." }
|
|
61
|
-
]
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
// Search knowledge library
|
|
65
|
-
OFIERE_KNOWLEDGE_OPS({ action: "search", query: "API rate limits" })
|
|
66
|
-
```
|
|
67
|
-
|
|
68
|
-
## CLI Commands
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
openclaw ofiere setup # Configure Supabase connection and agent identity
|
|
72
|
-
openclaw ofiere status # View current configuration
|
|
73
|
-
openclaw ofiere doctor # Test connection and list agents
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
## Configuration
|
|
77
|
-
|
|
78
|
-
Set via `openclaw ofiere setup` or environment variables:
|
|
79
|
-
|
|
80
|
-
| Option | Env Var | Description |
|
|
81
|
-
|---|---|---|
|
|
82
|
-
| `supabaseUrl` | `OFIERE_SUPABASE_URL` | Supabase project URL |
|
|
83
|
-
| `serviceRoleKey` | `OFIERE_SERVICE_ROLE_KEY` | Supabase service role key |
|
|
84
|
-
| `userId` | `OFIERE_USER_ID` | Your user UUID |
|
|
85
|
-
| `agentId` | `OFIERE_AGENT_ID` | This agent's ID (optional — auto-detected) |
|
|
86
|
-
| `enabled` | — | Enable/disable the plugin (default: `true`) |
|
|
87
|
-
|
|
88
|
-
## Architecture
|
|
89
|
-
|
|
90
|
-
```
|
|
91
|
-
OpenClaw Agent (VPS)
|
|
92
|
-
│ plugin runs IN-PROCESS
|
|
93
|
-
Ofiere Plugin ──► Supabase (shared database)
|
|
94
|
-
▲
|
|
95
|
-
Ofiere Dashboard ─────┘ (Vercel, real-time)
|
|
96
|
-
```
|
|
97
|
-
|
|
98
|
-
Both the agent plugin and the Vercel dashboard talk to the same Supabase instance. When the agent creates/updates a task, the dashboard sees it instantly through Supabase real-time subscriptions.
|
|
99
|
-
|
|
100
|
-
## Links
|
|
101
|
-
|
|
102
|
-
- [Ofiere Dashboard](https://github.com/gilanggemar/Ofiere)
|
|
103
|
-
- [OpenClaw](https://openclaw.ai)
|
|
104
|
-
- [Supabase](https://supabase.com)
|
|
1
|
+
# Ofiere PM Plugin for OpenClaw
|
|
2
|
+
|
|
3
|
+
Manage your Ofiere PM dashboard directly from OpenClaw agents. Create tasks, manage projects, build workflows, store knowledge — all synced to the dashboard in real time.
|
|
4
|
+
|
|
5
|
+
## Quick Install (One-Click)
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
curl -sSL https://ofiere.com/scripts/install.sh | bash -s -- \
|
|
9
|
+
--supabase-url "https://xxx.supabase.co" \
|
|
10
|
+
--service-key "eyJ..." \
|
|
11
|
+
--user-id "your-uuid"
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Only 3 parameters needed. All agents get the plugin automatically.
|
|
15
|
+
|
|
16
|
+
## Uninstall
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
curl -sSL https://ofiere.com/scripts/uninstall.sh | bash
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## How It Works
|
|
23
|
+
|
|
24
|
+
Once configured, the plugin connects to your Supabase database at gateway startup and registers 9 PM meta-tools directly into the agent. There's no separate MCP server process — it runs inside the OpenClaw gateway for maximum reliability.
|
|
25
|
+
|
|
26
|
+
Changes made by the agent are immediately visible on the Ofiere dashboard (Vercel) via Supabase real-time subscriptions.
|
|
27
|
+
|
|
28
|
+
## AI Meta-Tools
|
|
29
|
+
|
|
30
|
+
The plugin uses a scalable meta-tool architecture. Each tool handles one domain with an `action` parameter to select the operation.
|
|
31
|
+
|
|
32
|
+
| Tool | Actions | Description |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `OFIERE_TASK_OPS` | `list`, `create`, `update`, `delete` | Rich task management with execution plans, goals, constraints |
|
|
35
|
+
| `OFIERE_AGENT_OPS` | `list` | Query available agents for task assignment |
|
|
36
|
+
| `OFIERE_PROJECT_OPS` | `list_spaces`, `create_space`, `create_folder`, `bulk_create_tasks`, etc. | Full project hierarchy: spaces → folders → tasks |
|
|
37
|
+
| `OFIERE_SCHEDULE_OPS` | `list`, `create`, `update`, `delete` | Calendar events with recurrence |
|
|
38
|
+
| `OFIERE_KNOWLEDGE_OPS` | `search`, `list`, `create`, `update`, `delete` | Knowledge library with full content retrieval |
|
|
39
|
+
| `OFIERE_WORKFLOW_OPS` | `list`, `get`, `create`, `update`, `delete`, `list_runs`, `trigger` | Visual workflow builder with 16 node types |
|
|
40
|
+
| `OFIERE_NOTIFY_OPS` | `list`, `mark_read`, `create` | In-app notifications |
|
|
41
|
+
| `OFIERE_MEMORY_OPS` | `list_conversations`, `search_knowledge` | Conversation history & agent memory |
|
|
42
|
+
| `OFIERE_PROMPT_OPS` | `list`, `get`, `create`, `update`, `delete` | System prompt chunk management |
|
|
43
|
+
|
|
44
|
+
### Example
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
// Create a task with execution plan
|
|
48
|
+
OFIERE_TASK_OPS({ action: "create", title: "Deploy v2", agent_id: "ivy",
|
|
49
|
+
execution_plan: [{ step: 1, action: "Build", detail: "Run production build" }] })
|
|
50
|
+
|
|
51
|
+
// Create a workflow with nodes
|
|
52
|
+
OFIERE_WORKFLOW_OPS({ action: "create", name: "Deploy Pipeline",
|
|
53
|
+
nodes: [
|
|
54
|
+
{ type: "agent_step", data: { label: "Build", task: "Run npm build" } },
|
|
55
|
+
{ type: "human_approval", data: { label: "Review", instructions: "Check build output" } },
|
|
56
|
+
{ type: "output", data: { label: "Done" } }
|
|
57
|
+
],
|
|
58
|
+
edges: [
|
|
59
|
+
{ source: "agent_step-...", target: "human_approval-..." },
|
|
60
|
+
{ source: "human_approval-...", target: "output-..." }
|
|
61
|
+
]
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// Search knowledge library
|
|
65
|
+
OFIERE_KNOWLEDGE_OPS({ action: "search", query: "API rate limits" })
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## CLI Commands
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
openclaw ofiere setup # Configure Supabase connection and agent identity
|
|
72
|
+
openclaw ofiere status # View current configuration
|
|
73
|
+
openclaw ofiere doctor # Test connection and list agents
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Configuration
|
|
77
|
+
|
|
78
|
+
Set via `openclaw ofiere setup` or environment variables:
|
|
79
|
+
|
|
80
|
+
| Option | Env Var | Description |
|
|
81
|
+
|---|---|---|
|
|
82
|
+
| `supabaseUrl` | `OFIERE_SUPABASE_URL` | Supabase project URL |
|
|
83
|
+
| `serviceRoleKey` | `OFIERE_SERVICE_ROLE_KEY` | Supabase service role key |
|
|
84
|
+
| `userId` | `OFIERE_USER_ID` | Your user UUID |
|
|
85
|
+
| `agentId` | `OFIERE_AGENT_ID` | This agent's ID (optional — auto-detected) |
|
|
86
|
+
| `enabled` | — | Enable/disable the plugin (default: `true`) |
|
|
87
|
+
|
|
88
|
+
## Architecture
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
OpenClaw Agent (VPS)
|
|
92
|
+
│ plugin runs IN-PROCESS
|
|
93
|
+
Ofiere Plugin ──► Supabase (shared database)
|
|
94
|
+
▲
|
|
95
|
+
Ofiere Dashboard ─────┘ (Vercel, real-time)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Both the agent plugin and the Vercel dashboard talk to the same Supabase instance. When the agent creates/updates a task, the dashboard sees it instantly through Supabase real-time subscriptions.
|
|
99
|
+
|
|
100
|
+
## Links
|
|
101
|
+
|
|
102
|
+
- [Ofiere Dashboard](https://github.com/gilanggemar/Ofiere)
|
|
103
|
+
- [OpenClaw](https://openclaw.ai)
|
|
104
|
+
- [Supabase](https://supabase.com)
|
|
@@ -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
|
+
}
|