ofiere-openclaw-plugin 4.56.8 → 4.56.10

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/src/sop-render.ts CHANGED
@@ -1,216 +1,216 @@
1
- // src/sop-render.ts — Plugin-side markdown renderer for SOPs and Frameworks.
2
- // Mirrors dashboard/lib/sopRender.ts. Output is what gets prepended/appended to
3
- // the system prompt during `before_prompt_build`.
4
-
5
- interface SOPCheckItem { text?: string; checked?: boolean; }
6
- interface SOPStep {
7
- name?: string; action?: string; owner?: string; output?: string;
8
- decision_logic?: string; failure_state?: string; fallback_action?: string;
9
- estimated_duration?: string;
10
- }
11
- interface SOPEscalationRule { trigger?: string; escalateTo?: string; priority?: string; }
12
- interface SOPPrerequisites {
13
- conditions?: SOPCheckItem[]; required_tools?: string[];
14
- required_permissions?: string[]; safety_warnings?: string[];
15
- }
16
- export interface SOPData {
17
- title?: string;
18
- purpose?: string; scope?: string; applicability?: string;
19
- prerequisites?: SOPPrerequisites;
20
- procedure_steps?: SOPStep[];
21
- expected_outputs?: string[];
22
- acceptance_criteria?: SOPCheckItem[];
23
- escalation_rules?: SOPEscalationRule[];
24
- rollback_procedure?: string;
25
- notes?: string;
26
- }
27
-
28
- interface FrameworkObjective {
29
- objective?: string; target?: string; measurement?: string; frequency?: string;
30
- }
31
- interface FrameworkEscalation {
32
- trigger?: string; escalate_to?: string; priority?: string; response_sla?: string;
33
- }
34
- export interface FrameworkData {
35
- title?: string;
36
- mission?: string; vision?: string;
37
- core_principles?: string[];
38
- decision_authority?: string; budget_constraints?: string;
39
- resource_limits?: string; tool_stack?: string[];
40
- strategic_objectives?: FrameworkObjective[];
41
- risk_appetite?: string; compliance_requirements?: string[];
42
- escalation_matrix?: FrameworkEscalation[];
43
- team_composition?: string; integration_points?: string[];
44
- review_frequency?: string; last_reviewed?: string; next_review?: string;
45
- notes?: string;
46
- }
47
-
48
- function bullet(items: (string | undefined | null)[]): string {
49
- const cleaned = items.map((s) => (s ?? "").toString().trim()).filter(Boolean);
50
- return cleaned.length ? cleaned.map((s) => `- ${s}`).join("\n") : "";
51
- }
52
-
53
- function section(label: string, body: string): string {
54
- const trimmed = body.trim();
55
- return trimmed ? `### ${label}\n${trimmed}` : "";
56
- }
57
-
58
- function joinSections(parts: string[]): string {
59
- return parts.filter(Boolean).join("\n\n");
60
- }
61
-
62
- function safeParse<T = unknown>(s: string): T | null {
63
- try { return JSON.parse(s) as T; } catch { return null; }
64
- }
65
-
66
- export function renderSopMarkdown(input: string | SOPData | null | undefined): string {
67
- if (input == null) return "";
68
- const data: SOPData = typeof input === "string"
69
- ? (safeParse<SOPData>(input) || {})
70
- : input;
71
-
72
- const parts: string[] = [];
73
- if (data.purpose) parts.push(section("Purpose", data.purpose));
74
- if (data.scope) parts.push(section("Scope", data.scope));
75
- if (data.applicability) parts.push(section("Applicability", data.applicability));
76
-
77
- const prereq = data.prerequisites;
78
- if (prereq) {
79
- const pieces: string[] = [];
80
- const cond = (prereq.conditions || []).map((c) => c?.text).filter(Boolean) as string[];
81
- if (cond.length) pieces.push(`**Conditions:**\n${bullet(cond)}`);
82
- if (prereq.required_tools?.length) pieces.push(`**Required tools:**\n${bullet(prereq.required_tools)}`);
83
- if (prereq.required_permissions?.length) pieces.push(`**Required permissions:**\n${bullet(prereq.required_permissions)}`);
84
- if (prereq.safety_warnings?.length) pieces.push(`**Safety warnings:**\n${bullet(prereq.safety_warnings)}`);
85
- if (pieces.length) parts.push(section("Prerequisites", pieces.join("\n\n")));
86
- }
87
-
88
- if (data.procedure_steps?.length) {
89
- const steps = data.procedure_steps.map((step, i) => {
90
- const lines: string[] = [`${i + 1}. **${step.name || `Step ${i + 1}`}**`];
91
- if (step.action) lines.push(` - Action: ${step.action}`);
92
- if (step.owner) lines.push(` - Owner: ${step.owner}`);
93
- if (step.output) lines.push(` - Output: ${step.output}`);
94
- if (step.decision_logic) lines.push(` - Decision logic: ${step.decision_logic}`);
95
- if (step.failure_state) lines.push(` - Failure state: ${step.failure_state}`);
96
- if (step.fallback_action) lines.push(` - Fallback: ${step.fallback_action}`);
97
- if (step.estimated_duration) lines.push(` - ETA: ${step.estimated_duration}`);
98
- return lines.join("\n");
99
- }).join("\n");
100
- parts.push(section("Procedure", steps));
101
- }
102
-
103
- if (data.expected_outputs?.length) parts.push(section("Expected Outputs", bullet(data.expected_outputs)));
104
- if (data.acceptance_criteria?.length) {
105
- parts.push(section("Acceptance Criteria", bullet(data.acceptance_criteria.map((c) => c?.text || ""))));
106
- }
107
-
108
- if (data.escalation_rules?.length) {
109
- const rules = data.escalation_rules
110
- .filter((r) => r.trigger || r.escalateTo)
111
- .map((r) => `- [${r.priority || "P2"}] ${r.trigger || "(unspecified trigger)"} → ${r.escalateTo || "(unspecified)"}`)
112
- .join("\n");
113
- if (rules) parts.push(section("Escalation", rules));
114
- }
115
-
116
- if (data.rollback_procedure) parts.push(section("Rollback", data.rollback_procedure));
117
- if (data.notes) parts.push(section("Notes", data.notes));
118
-
119
- return joinSections(parts);
120
- }
121
-
122
- export function renderFrameworkMarkdown(input: string | FrameworkData | null | undefined): string {
123
- if (input == null) return "";
124
- const data: FrameworkData = typeof input === "string"
125
- ? (safeParse<FrameworkData>(input) || {})
126
- : input;
127
-
128
- const parts: string[] = [];
129
- if (data.mission) parts.push(section("Mission", data.mission));
130
- if (data.vision) parts.push(section("Vision", data.vision));
131
- if (data.core_principles?.length) parts.push(section("Core Principles", bullet(data.core_principles)));
132
-
133
- const authParts: string[] = [];
134
- if (data.decision_authority) authParts.push(`**Decision authority:** ${data.decision_authority}`);
135
- if (data.budget_constraints) authParts.push(`**Budget constraints:** ${data.budget_constraints}`);
136
- if (data.resource_limits) authParts.push(`**Resource limits:** ${data.resource_limits}`);
137
- if (data.tool_stack?.length) authParts.push(`**Tool stack:**\n${bullet(data.tool_stack)}`);
138
- if (authParts.length) parts.push(section("Authority & Constraints", authParts.join("\n\n")));
139
-
140
- if (data.strategic_objectives?.length) {
141
- const rows = data.strategic_objectives
142
- .filter((o) => o.objective || o.target)
143
- .map((o) => {
144
- const bits = [
145
- o.objective ? `**${o.objective}**` : "",
146
- o.target ? `target: ${o.target}` : "",
147
- o.measurement ? `measure: ${o.measurement}` : "",
148
- o.frequency ? `cadence: ${o.frequency}` : "",
149
- ].filter(Boolean);
150
- return `- ${bits.join(" · ")}`;
151
- })
152
- .join("\n");
153
- if (rows) parts.push(section("Strategic Objectives (KPIs)", rows));
154
- }
155
-
156
- const riskParts: string[] = [];
157
- if (data.risk_appetite) riskParts.push(`**Risk appetite:** ${data.risk_appetite}`);
158
- if (data.compliance_requirements?.length) riskParts.push(`**Compliance:**\n${bullet(data.compliance_requirements)}`);
159
- if (riskParts.length) parts.push(section("Risk & Compliance", riskParts.join("\n\n")));
160
-
161
- if (data.escalation_matrix?.length) {
162
- const rows = data.escalation_matrix
163
- .filter((e) => e.trigger || e.escalate_to)
164
- .map((e) => {
165
- const sla = e.response_sla ? ` (SLA ${e.response_sla})` : "";
166
- return `- [${e.priority || "P2"}] ${e.trigger || "(unspecified)"} → ${e.escalate_to || "(unspecified)"}${sla}`;
167
- })
168
- .join("\n");
169
- if (rows) parts.push(section("Escalation Matrix", rows));
170
- }
171
-
172
- const teamParts: string[] = [];
173
- if (data.team_composition) teamParts.push(`**Team:** ${data.team_composition}`);
174
- if (data.integration_points?.length) teamParts.push(`**Integration points:**\n${bullet(data.integration_points)}`);
175
- if (teamParts.length) parts.push(section("Team & Integrations", teamParts.join("\n\n")));
176
-
177
- if (data.review_frequency || data.next_review) {
178
- const lines = [
179
- data.review_frequency ? `**Review cadence:** ${data.review_frequency}` : "",
180
- data.last_reviewed ? `**Last reviewed:** ${data.last_reviewed}` : "",
181
- data.next_review ? `**Next review:** ${data.next_review}` : "",
182
- ].filter(Boolean);
183
- if (lines.length) parts.push(section("Review Cycle", lines.join("\n")));
184
- }
185
-
186
- if (data.notes) parts.push(section("Notes", data.notes));
187
-
188
- return joinSections(parts);
189
- }
190
-
191
- export function renderAttachmentBlock(args: {
192
- sops?: Array<{ title: string; content: string }>;
193
- frameworks?: Array<{ title: string; content: string }>;
194
- }): string {
195
- const blocks: string[] = [];
196
-
197
- if (args.frameworks?.length) {
198
- const sections = args.frameworks.map((fw) => {
199
- const md = renderFrameworkMarkdown(fw.content);
200
- if (!md) return `## ${fw.title}\n_(empty)_`;
201
- return `## ${fw.title}\n${md}`;
202
- }).join("\n\n");
203
- blocks.push(`[FRAMEWORKS — ATTACHED]\nThese frameworks are active for this run. Orient strategic decisions toward them.\n\n${sections}`);
204
- }
205
-
206
- if (args.sops?.length) {
207
- const sections = args.sops.map((sop) => {
208
- const md = renderSopMarkdown(sop.content);
209
- if (!md) return `## ${sop.title}\n_(empty)_`;
210
- return `## ${sop.title}\n${md}`;
211
- }).join("\n\n");
212
- blocks.push(`[SOPS — ATTACHED]\nThese SOPs are active for this run. Follow the procedure steps and escalation rules.\n\n${sections}`);
213
- }
214
-
215
- return blocks.join("\n\n");
216
- }
1
+ // src/sop-render.ts — Plugin-side markdown renderer for SOPs and Frameworks.
2
+ // Mirrors dashboard/lib/sopRender.ts. Output is what gets prepended/appended to
3
+ // the system prompt during `before_prompt_build`.
4
+
5
+ interface SOPCheckItem { text?: string; checked?: boolean; }
6
+ interface SOPStep {
7
+ name?: string; action?: string; owner?: string; output?: string;
8
+ decision_logic?: string; failure_state?: string; fallback_action?: string;
9
+ estimated_duration?: string;
10
+ }
11
+ interface SOPEscalationRule { trigger?: string; escalateTo?: string; priority?: string; }
12
+ interface SOPPrerequisites {
13
+ conditions?: SOPCheckItem[]; required_tools?: string[];
14
+ required_permissions?: string[]; safety_warnings?: string[];
15
+ }
16
+ export interface SOPData {
17
+ title?: string;
18
+ purpose?: string; scope?: string; applicability?: string;
19
+ prerequisites?: SOPPrerequisites;
20
+ procedure_steps?: SOPStep[];
21
+ expected_outputs?: string[];
22
+ acceptance_criteria?: SOPCheckItem[];
23
+ escalation_rules?: SOPEscalationRule[];
24
+ rollback_procedure?: string;
25
+ notes?: string;
26
+ }
27
+
28
+ interface FrameworkObjective {
29
+ objective?: string; target?: string; measurement?: string; frequency?: string;
30
+ }
31
+ interface FrameworkEscalation {
32
+ trigger?: string; escalate_to?: string; priority?: string; response_sla?: string;
33
+ }
34
+ export interface FrameworkData {
35
+ title?: string;
36
+ mission?: string; vision?: string;
37
+ core_principles?: string[];
38
+ decision_authority?: string; budget_constraints?: string;
39
+ resource_limits?: string; tool_stack?: string[];
40
+ strategic_objectives?: FrameworkObjective[];
41
+ risk_appetite?: string; compliance_requirements?: string[];
42
+ escalation_matrix?: FrameworkEscalation[];
43
+ team_composition?: string; integration_points?: string[];
44
+ review_frequency?: string; last_reviewed?: string; next_review?: string;
45
+ notes?: string;
46
+ }
47
+
48
+ function bullet(items: (string | undefined | null)[]): string {
49
+ const cleaned = items.map((s) => (s ?? "").toString().trim()).filter(Boolean);
50
+ return cleaned.length ? cleaned.map((s) => `- ${s}`).join("\n") : "";
51
+ }
52
+
53
+ function section(label: string, body: string): string {
54
+ const trimmed = body.trim();
55
+ return trimmed ? `### ${label}\n${trimmed}` : "";
56
+ }
57
+
58
+ function joinSections(parts: string[]): string {
59
+ return parts.filter(Boolean).join("\n\n");
60
+ }
61
+
62
+ function safeParse<T = unknown>(s: string): T | null {
63
+ try { return JSON.parse(s) as T; } catch { return null; }
64
+ }
65
+
66
+ export function renderSopMarkdown(input: string | SOPData | null | undefined): string {
67
+ if (input == null) return "";
68
+ const data: SOPData = typeof input === "string"
69
+ ? (safeParse<SOPData>(input) || {})
70
+ : input;
71
+
72
+ const parts: string[] = [];
73
+ if (data.purpose) parts.push(section("Purpose", data.purpose));
74
+ if (data.scope) parts.push(section("Scope", data.scope));
75
+ if (data.applicability) parts.push(section("Applicability", data.applicability));
76
+
77
+ const prereq = data.prerequisites;
78
+ if (prereq) {
79
+ const pieces: string[] = [];
80
+ const cond = (prereq.conditions || []).map((c) => c?.text).filter(Boolean) as string[];
81
+ if (cond.length) pieces.push(`**Conditions:**\n${bullet(cond)}`);
82
+ if (prereq.required_tools?.length) pieces.push(`**Required tools:**\n${bullet(prereq.required_tools)}`);
83
+ if (prereq.required_permissions?.length) pieces.push(`**Required permissions:**\n${bullet(prereq.required_permissions)}`);
84
+ if (prereq.safety_warnings?.length) pieces.push(`**Safety warnings:**\n${bullet(prereq.safety_warnings)}`);
85
+ if (pieces.length) parts.push(section("Prerequisites", pieces.join("\n\n")));
86
+ }
87
+
88
+ if (data.procedure_steps?.length) {
89
+ const steps = data.procedure_steps.map((step, i) => {
90
+ const lines: string[] = [`${i + 1}. **${step.name || `Step ${i + 1}`}**`];
91
+ if (step.action) lines.push(` - Action: ${step.action}`);
92
+ if (step.owner) lines.push(` - Owner: ${step.owner}`);
93
+ if (step.output) lines.push(` - Output: ${step.output}`);
94
+ if (step.decision_logic) lines.push(` - Decision logic: ${step.decision_logic}`);
95
+ if (step.failure_state) lines.push(` - Failure state: ${step.failure_state}`);
96
+ if (step.fallback_action) lines.push(` - Fallback: ${step.fallback_action}`);
97
+ if (step.estimated_duration) lines.push(` - ETA: ${step.estimated_duration}`);
98
+ return lines.join("\n");
99
+ }).join("\n");
100
+ parts.push(section("Procedure", steps));
101
+ }
102
+
103
+ if (data.expected_outputs?.length) parts.push(section("Expected Outputs", bullet(data.expected_outputs)));
104
+ if (data.acceptance_criteria?.length) {
105
+ parts.push(section("Acceptance Criteria", bullet(data.acceptance_criteria.map((c) => c?.text || ""))));
106
+ }
107
+
108
+ if (data.escalation_rules?.length) {
109
+ const rules = data.escalation_rules
110
+ .filter((r) => r.trigger || r.escalateTo)
111
+ .map((r) => `- [${r.priority || "P2"}] ${r.trigger || "(unspecified trigger)"} → ${r.escalateTo || "(unspecified)"}`)
112
+ .join("\n");
113
+ if (rules) parts.push(section("Escalation", rules));
114
+ }
115
+
116
+ if (data.rollback_procedure) parts.push(section("Rollback", data.rollback_procedure));
117
+ if (data.notes) parts.push(section("Notes", data.notes));
118
+
119
+ return joinSections(parts);
120
+ }
121
+
122
+ export function renderFrameworkMarkdown(input: string | FrameworkData | null | undefined): string {
123
+ if (input == null) return "";
124
+ const data: FrameworkData = typeof input === "string"
125
+ ? (safeParse<FrameworkData>(input) || {})
126
+ : input;
127
+
128
+ const parts: string[] = [];
129
+ if (data.mission) parts.push(section("Mission", data.mission));
130
+ if (data.vision) parts.push(section("Vision", data.vision));
131
+ if (data.core_principles?.length) parts.push(section("Core Principles", bullet(data.core_principles)));
132
+
133
+ const authParts: string[] = [];
134
+ if (data.decision_authority) authParts.push(`**Decision authority:** ${data.decision_authority}`);
135
+ if (data.budget_constraints) authParts.push(`**Budget constraints:** ${data.budget_constraints}`);
136
+ if (data.resource_limits) authParts.push(`**Resource limits:** ${data.resource_limits}`);
137
+ if (data.tool_stack?.length) authParts.push(`**Tool stack:**\n${bullet(data.tool_stack)}`);
138
+ if (authParts.length) parts.push(section("Authority & Constraints", authParts.join("\n\n")));
139
+
140
+ if (data.strategic_objectives?.length) {
141
+ const rows = data.strategic_objectives
142
+ .filter((o) => o.objective || o.target)
143
+ .map((o) => {
144
+ const bits = [
145
+ o.objective ? `**${o.objective}**` : "",
146
+ o.target ? `target: ${o.target}` : "",
147
+ o.measurement ? `measure: ${o.measurement}` : "",
148
+ o.frequency ? `cadence: ${o.frequency}` : "",
149
+ ].filter(Boolean);
150
+ return `- ${bits.join(" · ")}`;
151
+ })
152
+ .join("\n");
153
+ if (rows) parts.push(section("Strategic Objectives (KPIs)", rows));
154
+ }
155
+
156
+ const riskParts: string[] = [];
157
+ if (data.risk_appetite) riskParts.push(`**Risk appetite:** ${data.risk_appetite}`);
158
+ if (data.compliance_requirements?.length) riskParts.push(`**Compliance:**\n${bullet(data.compliance_requirements)}`);
159
+ if (riskParts.length) parts.push(section("Risk & Compliance", riskParts.join("\n\n")));
160
+
161
+ if (data.escalation_matrix?.length) {
162
+ const rows = data.escalation_matrix
163
+ .filter((e) => e.trigger || e.escalate_to)
164
+ .map((e) => {
165
+ const sla = e.response_sla ? ` (SLA ${e.response_sla})` : "";
166
+ return `- [${e.priority || "P2"}] ${e.trigger || "(unspecified)"} → ${e.escalate_to || "(unspecified)"}${sla}`;
167
+ })
168
+ .join("\n");
169
+ if (rows) parts.push(section("Escalation Matrix", rows));
170
+ }
171
+
172
+ const teamParts: string[] = [];
173
+ if (data.team_composition) teamParts.push(`**Team:** ${data.team_composition}`);
174
+ if (data.integration_points?.length) teamParts.push(`**Integration points:**\n${bullet(data.integration_points)}`);
175
+ if (teamParts.length) parts.push(section("Team & Integrations", teamParts.join("\n\n")));
176
+
177
+ if (data.review_frequency || data.next_review) {
178
+ const lines = [
179
+ data.review_frequency ? `**Review cadence:** ${data.review_frequency}` : "",
180
+ data.last_reviewed ? `**Last reviewed:** ${data.last_reviewed}` : "",
181
+ data.next_review ? `**Next review:** ${data.next_review}` : "",
182
+ ].filter(Boolean);
183
+ if (lines.length) parts.push(section("Review Cycle", lines.join("\n")));
184
+ }
185
+
186
+ if (data.notes) parts.push(section("Notes", data.notes));
187
+
188
+ return joinSections(parts);
189
+ }
190
+
191
+ export function renderAttachmentBlock(args: {
192
+ sops?: Array<{ title: string; content: string }>;
193
+ frameworks?: Array<{ title: string; content: string }>;
194
+ }): string {
195
+ const blocks: string[] = [];
196
+
197
+ if (args.frameworks?.length) {
198
+ const sections = args.frameworks.map((fw) => {
199
+ const md = renderFrameworkMarkdown(fw.content);
200
+ if (!md) return `## ${fw.title}\n_(empty)_`;
201
+ return `## ${fw.title}\n${md}`;
202
+ }).join("\n\n");
203
+ blocks.push(`[FRAMEWORKS — ATTACHED]\nThese frameworks are active for this run. Orient strategic decisions toward them.\n\n${sections}`);
204
+ }
205
+
206
+ if (args.sops?.length) {
207
+ const sections = args.sops.map((sop) => {
208
+ const md = renderSopMarkdown(sop.content);
209
+ if (!md) return `## ${sop.title}\n_(empty)_`;
210
+ return `## ${sop.title}\n${md}`;
211
+ }).join("\n\n");
212
+ blocks.push(`[SOPS — ATTACHED]\nThese SOPs are active for this run. Follow the procedure steps and escalation rules.\n\n${sections}`);
213
+ }
214
+
215
+ return blocks.join("\n\n");
216
+ }
package/src/supabase.ts CHANGED
@@ -1,13 +1,13 @@
1
- import { createClient, type SupabaseClient } from "@supabase/supabase-js";
2
-
3
- let _client: SupabaseClient | null = null;
4
-
5
- export function getSupabase(supabaseUrl: string, serviceRoleKey: string): SupabaseClient {
6
- if (_client) return _client;
7
-
8
- _client = createClient(supabaseUrl, serviceRoleKey, {
9
- auth: { persistSession: false },
10
- });
11
-
12
- return _client;
13
- }
1
+ import { createClient, type SupabaseClient } from "@supabase/supabase-js";
2
+
3
+ let _client: SupabaseClient | null = null;
4
+
5
+ export function getSupabase(supabaseUrl: string, serviceRoleKey: string): SupabaseClient {
6
+ if (_client) return _client;
7
+
8
+ _client = createClient(supabaseUrl, serviceRoleKey, {
9
+ auth: { persistSession: false },
10
+ });
11
+
12
+ return _client;
13
+ }
package/src/tools.ts CHANGED
@@ -69,6 +69,20 @@ function err(message: string): ToolResult {
69
69
  };
70
70
  }
71
71
 
72
+ /**
73
+ * D1 fix (2026-05-23): true when an LLM-supplied value is an "empty echo" —
74
+ * a field the model parroted back from a prior read with no real new content
75
+ * (null/undefined, empty or whitespace-only string, or empty array). The task
76
+ * update path is LLM-only; without this guard an echoed blank overwrites a
77
+ * real column. Mirrors isEmptyEcho in dashboard/lib/ofie/tool-executor.ts.
78
+ */
79
+ function isEmptyEcho(v: unknown): boolean {
80
+ if (v === null || v === undefined) return true;
81
+ if (typeof v === "string") return v.trim() === "";
82
+ if (Array.isArray(v)) return v.length === 0;
83
+ return false;
84
+ }
85
+
72
86
  // ─── Subagent ↔ chief invariant ──────────────────────────────────────────────
73
87
  // Mirrors dashboard/lib/subagentValidation.ts. Used by TASK_OPS + SCHEDULE_OPS
74
88
  // when a chief delegates work to one of their staff via tool call.
@@ -1095,8 +1109,16 @@ async function handleUpdateTask(
1095
1109
  // error instead of nulling the column. Coerce empty/whitespace → null here
1096
1110
  // to match the dashboard native executor (see tool-executor.ts handleTaskOps).
1097
1111
  const TIMESTAMP_FIELDS = ["start_date", "due_date"];
1112
+ // D1 parity fix (2026-05-23): this update path is LLM-only. A completing
1113
+ // agent routinely echoes the whole task object back with blank values for
1114
+ // fields it did not change; without this guard those blanks overwrite real
1115
+ // columns. An empty echo on a structural field means "leave unchanged".
1116
+ // status/priority/progress are excluded (0 is valid); start_date/due_date
1117
+ // keep the Fix #8 empty-string -> null coercion below.
1118
+ const STRUCTURE_FIELDS = ["title", "description", "agent_id", "tags"];
1098
1119
  for (const f of fields) {
1099
1120
  if (params[f] !== undefined) {
1121
+ if (STRUCTURE_FIELDS.includes(f) && isEmptyEcho(params[f])) continue;
1100
1122
  if (TIMESTAMP_FIELDS.includes(f)) {
1101
1123
  const v = params[f];
1102
1124
  updates[f] = (typeof v === "string" && v.trim() === "") ? null : v;
@@ -1147,12 +1169,15 @@ async function handleUpdateTask(
1147
1169
  }
1148
1170
  }
1149
1171
 
1150
- // If task is being marked DONE or FAILED, auto-complete any linked scheduler events
1172
+ // If task is being marked DONE or FAILED, auto-complete any linked scheduler events.
1173
+ // A-4 plugin parity (2026-05-18) — scope by user_id so cross-tenant task_id
1174
+ // can't trigger foreign scheduler flip via plugin tool path.
1151
1175
  if (params.status === "DONE" || params.status === "FAILED") {
1152
1176
  try {
1153
1177
  await supabase
1154
1178
  .from("scheduler_events")
1155
1179
  .update({ status: "completed", next_run_at: null, updated_at: new Date().toISOString() })
1180
+ .eq("user_id", userId)
1156
1181
  .eq("task_id", params.task_id as string);
1157
1182
  } catch (_schedErr) {
1158
1183
  // Non-fatal: task update should still proceed
@@ -2464,7 +2489,9 @@ function registerWorkflowOps(
2464
2489
  case "delete": {
2465
2490
  const wfId = (params.id || params.workflow_id) as string;
2466
2491
  if (!wfId) return err("Missing required: id");
2467
- await supabase.from("workflow_runs").delete().eq("workflow_id", wfId);
2492
+ // A-4 plugin parity (2026-05-18) — scope cascade so cross-tenant
2493
+ // workflow_id can't trigger foreign workflow_runs wipe via plugin.
2494
+ await supabase.from("workflow_runs").delete().eq("user_id", userId).eq("workflow_id", wfId);
2468
2495
  const { error } = await supabase.from("workflows").delete().eq("id", wfId).eq("user_id", userId);
2469
2496
  if (error) return err(error.message);
2470
2497
  return ok({ message: "Workflow and associated runs deleted", ok: true });
@@ -4931,7 +4958,7 @@ function registerPlanOps(
4931
4958
  `- "delete": Remove plan. Required: plan_id\n` +
4932
4959
  `- "add_nodes": Add nodes to plan. Required: plan_id, nodes[]. Optional: parent_node_id\n` +
4933
4960
  `- "execute": Deploy plan to real PM tasks. Required: plan_id. Optional: create_folder, create_scheduler, space_id, folder_id\n\n` +
4934
- `Node structure: { type, title, description?, agent_id?, priority?, status?, start_date?, due_date?, tags?, execution_steps?[{text}], goals?[{label,type?}], constraints?[{label,type?}], system_prompt?, children?[], parallel? }`,
4961
+ `Node structure: { type, title, description?, agent_id?, priority?, status?, start_date?, due_date?, tags?, execution_steps?[{text}], goals?[{label,type?}], constraints?[{label,type?}], system_prompt?, gate_condition?, children?[], parallel? }`,
4935
4962
  parameters: {
4936
4963
  type: "object",
4937
4964
  required: ["action"],
@@ -4964,8 +4991,13 @@ function registerPlanOps(
4964
4991
  goals: { type: "array", items: { type: "object", properties: { label: { type: "string" }, type: { type: "string" } }, required: ["label"] } },
4965
4992
  constraints: { type: "array", items: { type: "object", properties: { label: { type: "string" }, type: { type: "string" } }, required: ["label"] } },
4966
4993
  system_prompt: { type: "string" },
4994
+ gate_condition: {
4995
+ type: "string",
4996
+ enum: ["manual", "all_predecessors_complete", "any_predecessor_complete"],
4997
+ description: 'For type:"gate" nodes only. manual = human approval required; all_predecessors_complete = AND-join (default for gates); any_predecessor_complete = OR-join.',
4998
+ },
4967
4999
  parallel: { type: "boolean" },
4968
- children: { type: "array", description: "Nested child nodes (recursive)", items: { type: "object", properties: { title: { type: "string" }, type: { type: "string", enum: ["task", "gate", "milestone"] }, description: { type: "string" }, agent_id: { type: "string" }, priority: { type: "number" }, status: { type: "string" }, start_date: { type: "string" }, due_date: { type: "string" }, parallel: { type: "boolean" } }, required: ["title"] } },
5000
+ children: { type: "array", description: "Nested child nodes (recursive)", items: { type: "object", properties: { title: { type: "string" }, type: { type: "string", enum: ["task", "gate", "milestone"] }, description: { type: "string" }, agent_id: { type: "string" }, priority: { type: "number" }, status: { type: "string" }, start_date: { type: "string" }, due_date: { type: "string" }, parallel: { type: "boolean" }, gate_condition: { type: "string", enum: ["manual", "all_predecessors_complete", "any_predecessor_complete"] } }, required: ["title"] } },
4969
5001
  },
4970
5002
  required: ["title"],
4971
5003
  },
@@ -4998,6 +5030,17 @@ function generatePlanId(): string {
4998
5030
  return crypto.randomUUID();
4999
5031
  }
5000
5032
 
5033
+ const VALID_GATE_CONDITIONS = ["manual", "all_predecessors_complete", "any_predecessor_complete"] as const;
5034
+
5035
+ /** Validate a plan node's gate_condition. Invalid/absent values fall back to the
5036
+ * existing default — gates get "all_predecessors_complete", non-gates get nothing. */
5037
+ function normalizeGateCondition(v: unknown, nodeType: unknown): string | undefined {
5038
+ if (typeof v === "string" && (VALID_GATE_CONDITIONS as readonly string[]).includes(v)) {
5039
+ return v;
5040
+ }
5041
+ return nodeType === "gate" ? "all_predecessors_complete" : undefined;
5042
+ }
5043
+
5001
5044
  /** Normalize agent-provided node into the PlanNode shape stored in plan_data */
5002
5045
  function normalizeNode(raw: any): any {
5003
5046
  const id = raw.id || generatePlanNodeId();
@@ -5024,7 +5067,7 @@ function normalizeNode(raw: any): any {
5024
5067
  ? raw.constraints.map((c: any, i: number) => ({ id: `cstr-${Date.now()}-${i}`, type: c.type || "custom", label: typeof c === "string" ? c : c.label || String(c) }))
5025
5068
  : undefined,
5026
5069
  systemPrompt: raw.system_prompt || raw.systemPrompt || undefined,
5027
- gateCondition: raw.gate_condition || raw.gateCondition || (raw.type === "gate" ? "all_predecessors_complete" : undefined),
5070
+ gateCondition: normalizeGateCondition(raw.gate_condition || raw.gateCondition, raw.type),
5028
5071
  children,
5029
5072
  parallel: raw.parallel || false,
5030
5073
  collapsed: false,
@@ -1,8 +1,8 @@
1
- // Ambient declaration for the OpenClaw plugin SDK. The gateway resolves this
2
- // module at runtime; npm has no published types. Keep loose — the plugin only
3
- // uses a small surface (logger, on, registerTool, registerCommand, etc.).
4
-
5
- declare module "openclaw/plugin-sdk" {
6
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
- export type OpenClawPluginApi = any;
8
- }
1
+ // Ambient declaration for the OpenClaw plugin SDK. The gateway resolves this
2
+ // module at runtime; npm has no published types. Keep loose — the plugin only
3
+ // uses a small surface (logger, on, registerTool, registerCommand, etc.).
4
+
5
+ declare module "openclaw/plugin-sdk" {
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ export type OpenClawPluginApi = any;
8
+ }
package/src/types.ts CHANGED
@@ -1,10 +1,10 @@
1
- export interface OfiereConfig {
2
- enabled: boolean;
3
- supabaseUrl: string;
4
- serviceRoleKey: string;
5
- userId: string;
6
- /** Optional — if not set, agent identity is resolved at runtime from OpenClaw context */
7
- agentId: string;
8
- /** IANA timezone string for the user (e.g. 'Asia/Jakarta', 'America/New_York'). Default: 'Asia/Jakarta' */
9
- timezone: string;
10
- }
1
+ export interface OfiereConfig {
2
+ enabled: boolean;
3
+ supabaseUrl: string;
4
+ serviceRoleKey: string;
5
+ userId: string;
6
+ /** Optional — if not set, agent identity is resolved at runtime from OpenClaw context */
7
+ agentId: string;
8
+ /** IANA timezone string for the user (e.g. 'Asia/Jakarta', 'America/New_York'). Default: 'Asia/Jakarta' */
9
+ timezone: string;
10
+ }