ofiere-openclaw-plugin 4.56.9 → 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;
@@ -4936,7 +4958,7 @@ function registerPlanOps(
4936
4958
  `- "delete": Remove plan. Required: plan_id\n` +
4937
4959
  `- "add_nodes": Add nodes to plan. Required: plan_id, nodes[]. Optional: parent_node_id\n` +
4938
4960
  `- "execute": Deploy plan to real PM tasks. Required: plan_id. Optional: create_folder, create_scheduler, space_id, folder_id\n\n` +
4939
- `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? }`,
4940
4962
  parameters: {
4941
4963
  type: "object",
4942
4964
  required: ["action"],
@@ -4969,8 +4991,13 @@ function registerPlanOps(
4969
4991
  goals: { type: "array", items: { type: "object", properties: { label: { type: "string" }, type: { type: "string" } }, required: ["label"] } },
4970
4992
  constraints: { type: "array", items: { type: "object", properties: { label: { type: "string" }, type: { type: "string" } }, required: ["label"] } },
4971
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
+ },
4972
4999
  parallel: { type: "boolean" },
4973
- 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"] } },
4974
5001
  },
4975
5002
  required: ["title"],
4976
5003
  },
@@ -5003,6 +5030,17 @@ function generatePlanId(): string {
5003
5030
  return crypto.randomUUID();
5004
5031
  }
5005
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
+
5006
5044
  /** Normalize agent-provided node into the PlanNode shape stored in plan_data */
5007
5045
  function normalizeNode(raw: any): any {
5008
5046
  const id = raw.id || generatePlanNodeId();
@@ -5029,7 +5067,7 @@ function normalizeNode(raw: any): any {
5029
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) }))
5030
5068
  : undefined,
5031
5069
  systemPrompt: raw.system_prompt || raw.systemPrompt || undefined,
5032
- gateCondition: raw.gate_condition || raw.gateCondition || (raw.type === "gate" ? "all_predecessors_complete" : undefined),
5070
+ gateCondition: normalizeGateCondition(raw.gate_condition || raw.gateCondition, raw.type),
5033
5071
  children,
5034
5072
  parallel: raw.parallel || false,
5035
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
+ }