ofiere-openclaw-plugin 4.56.4 → 4.56.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/tools.js CHANGED
@@ -244,7 +244,17 @@ function registerTaskOps(api, supabase, userId, resolveAgent, timezone) {
244
244
  `For simple tasks, just provide title and optionally description.\n` +
245
245
  `agent_id: Pass your name to self-assign, another agent's name, or 'none'.\n` +
246
246
  `subagent_id: When delegating to one of your own staff, pass the staff UUID here AND set agent_id to yourself (or the chief). The staff's chief_agent_id MUST match agent_id or the call rejects with subagent_chief_mismatch. Use OFIERE_AGENT_OPS list_subagents to discover available staff under a chief.\n` +
247
- `For recurring tasks: set start_date + recurrence_type + recurrence_interval. Example: every 2 minutes = recurrence_type: "minutely", recurrence_interval: 2.\n` +
247
+ `\n` +
248
+ `For recurring tasks: set start_date + recurrence_type + recurrence_interval.\n` +
249
+ `⚠️ CADENCE MAPPING — match the user's spoken phrase EXACTLY (server-side cadence-intent guard rejects mismatches):\n` +
250
+ ` • "every N seconds" → recurrence_type:"minutely" + recurrence_interval:max(1, ceil(N/60)) ← cron resolution = 1 minute\n` +
251
+ ` • "every N minutes" → recurrence_type:"minutely" + recurrence_interval:N ← e.g. every 35 min → minutely+35\n` +
252
+ ` • "every N hours" → recurrence_type:"hourly" + recurrence_interval:N\n` +
253
+ ` • "every N days" → recurrence_type:"daily" + recurrence_interval:N ← daily+1 = 24h gap!\n` +
254
+ ` • "every N weeks" → recurrence_type:"weekly" + recurrence_interval:N\n` +
255
+ ` • "every N months" → recurrence_type:"monthly" + recurrence_interval:N\n` +
256
+ `🛑 NEVER use daily+1 (or hourly+24, etc.) when the user said a minute-level cadence — the task will fire ONCE per 24h instead of every N minutes. This was a real prod bug (v1 smoke 2026-05-17).\n` +
257
+ `\n` +
248
258
  `Approvals: Use add_approval to request sign-off from humans or agents. Approvals are separate from workflow gate nodes.\n` +
249
259
  `Status: PENDING, IN_PROGRESS, DONE, FAILED | Priority: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL`,
250
260
  parameters: {
@@ -561,10 +571,104 @@ function normalizeStartDate(startDate, explicitScheduledTime, tzOffsetHours) {
561
571
  scheduledDateLocal: `${year}-${pad(month)}-${pad(day)}`,
562
572
  };
563
573
  }
574
+ // ── Fix #10 (2026-05-17): Cadence-intent guard ──────────────────────────────
575
+ // v1 smoke incident: LLM picked recurrence_type='daily' + recurrence_interval=1
576
+ // for a task whose title said "every 35 minutes" — so it fired ONCE per 24h
577
+ // instead of every 35 min. Silent test failure. This guard inspects the task
578
+ // text for an explicit "every N <unit>" phrase and rejects the create when the
579
+ // chosen recurrence_type/interval doesn't match. Combined with the tightened
580
+ // OFIERE_TASK_OPS description (cadence-mapping table), prevents the drift at
581
+ // both the LLM-prompt layer and the server-runtime layer.
582
+ const RECURRENCE_TYPES = ["none", "minutely", "hourly", "daily", "weekly", "monthly"];
583
+ function humanizeCadence(type, interval) {
584
+ if (type === "none")
585
+ return "one-shot";
586
+ const unitMap = {
587
+ minutely: "minute",
588
+ hourly: "hour",
589
+ daily: "day",
590
+ weekly: "week",
591
+ monthly: "month",
592
+ };
593
+ const unit = unitMap[type] || type;
594
+ return interval === 1 ? `every ${unit}` : `every ${interval} ${unit}s`;
595
+ }
596
+ function validateRecurrence(params) {
597
+ const rtRaw = params.recurrence_type ?? "none";
598
+ const rt = rtRaw.toLowerCase();
599
+ const riRaw = params.recurrence_interval;
600
+ const ri = typeof riRaw === "number" ? riRaw : (riRaw === undefined ? 1 : Number(riRaw));
601
+ if (!RECURRENCE_TYPES.includes(rt)) {
602
+ return {
603
+ ok: false,
604
+ reason: `Invalid recurrence_type "${rtRaw}". Must be one of: ${RECURRENCE_TYPES.join(", ")}.`,
605
+ };
606
+ }
607
+ if (rt === "none")
608
+ return { ok: true };
609
+ if (!Number.isFinite(ri) || !Number.isInteger(ri) || ri < 1 || ri > 9999) {
610
+ return {
611
+ ok: false,
612
+ reason: `Invalid recurrence_interval ${riRaw}. Must be a positive integer between 1 and 9999.`,
613
+ };
614
+ }
615
+ // Cadence-intent guard: scan title + description for an "every N <unit>"
616
+ // phrase. If present, the chosen recurrence_type+interval MUST match.
617
+ const text = `${params.title || ""} ${params.description || ""}`.toLowerCase();
618
+ const cadenceMatch = text.match(/every\s+(\d+)\s*(seconds?|secs?|minutes?|mins?|hours?|hrs?|days?|weeks?|months?)\b/);
619
+ if (!cadenceMatch)
620
+ return { ok: true };
621
+ const n = parseInt(cadenceMatch[1], 10);
622
+ const unitRaw = cadenceMatch[2];
623
+ let expectedType;
624
+ let expectedInterval;
625
+ if (/^sec/.test(unitRaw)) {
626
+ expectedType = "minutely";
627
+ expectedInterval = Math.max(1, Math.ceil(n / 60));
628
+ }
629
+ else if (/^min/.test(unitRaw)) {
630
+ expectedType = "minutely";
631
+ expectedInterval = n;
632
+ }
633
+ else if (/^(hour|hr)/.test(unitRaw)) {
634
+ expectedType = "hourly";
635
+ expectedInterval = n;
636
+ }
637
+ else if (/^day/.test(unitRaw)) {
638
+ expectedType = "daily";
639
+ expectedInterval = n;
640
+ }
641
+ else if (/^week/.test(unitRaw)) {
642
+ expectedType = "weekly";
643
+ expectedInterval = n;
644
+ }
645
+ else if (/^month/.test(unitRaw)) {
646
+ expectedType = "monthly";
647
+ expectedInterval = n;
648
+ }
649
+ else {
650
+ return { ok: true };
651
+ }
652
+ if (rt !== expectedType || ri !== expectedInterval) {
653
+ return {
654
+ ok: false,
655
+ reason: `Recurrence mismatch: task text says "every ${n} ${unitRaw}" but you passed ` +
656
+ `recurrence_type="${rt}", recurrence_interval=${ri} (= ${humanizeCadence(rt, ri)}). ` +
657
+ `Correct combo: recurrence_type="${expectedType}", recurrence_interval=${expectedInterval} ` +
658
+ `(= ${humanizeCadence(expectedType, expectedInterval)}). ` +
659
+ `Re-call with the matching values, OR remove the "every N ${unitRaw}" phrase from the title/description if you intended a different cadence.`,
660
+ };
661
+ }
662
+ return { ok: true };
663
+ }
564
664
  async function handleCreateTask(supabase, userId, resolveAgent, params, fallbackTimezone = "Asia/Jakarta") {
565
665
  try {
566
666
  if (!params.title)
567
667
  return err("Missing required field: title");
668
+ // Fix #10 (2026-05-17): cadence-intent guard — see helper comment above.
669
+ const recurCheck = validateRecurrence(params);
670
+ if (!recurCheck.ok)
671
+ return err(recurCheck.reason);
568
672
  const id = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
569
673
  const now = new Date().toISOString();
570
674
  // BUG 5 fix: resolve user's IANA timezone from their profile row. Plugin
@@ -774,9 +878,16 @@ async function handleCreateTask(supabase, userId, resolveAgent, params, fallback
774
878
  extras.push(`${cf.constraints.length} constraints`);
775
879
  if (cf.system_prompt)
776
880
  extras.push("custom system prompt");
777
- // BUG 6 fix: surface recurrence fields in create response
881
+ // BUG 6 fix: surface recurrence fields in create response.
882
+ // Fix #10 (2026-05-17): also surface effective_cadence so the LLM sees the
883
+ // human-readable interpretation (e.g. "every 35 minutes") of the values it
884
+ // just passed — defense-in-depth alongside validateRecurrence().
778
885
  const recurrenceInfo = (params.recurrence_type && params.recurrence_type !== "none")
779
- ? { recurrence_type: params.recurrence_type, recurrence_interval: params.recurrence_interval || 1 }
886
+ ? {
887
+ recurrence_type: params.recurrence_type,
888
+ recurrence_interval: params.recurrence_interval || 1,
889
+ effective_cadence: humanizeCadence(params.recurrence_type, params.recurrence_interval || 1),
890
+ }
780
891
  : undefined;
781
892
  // BUG 7 fix: only claim scheduling when bridge actually fired
782
893
  const didSchedule = !!(startDate && effectiveAgentId);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.56.4",
3
+ "version": "4.56.5",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin for Ofiere PM - 18 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, execution plan builder, SOP management, agent brain, talent management, corporate frameworks, agent office canvas, and PM gate approvals",
6
6
  "keywords": [
package/src/tools.ts CHANGED
@@ -282,7 +282,17 @@ function registerTaskOps(
282
282
  `For simple tasks, just provide title and optionally description.\n` +
283
283
  `agent_id: Pass your name to self-assign, another agent's name, or 'none'.\n` +
284
284
  `subagent_id: When delegating to one of your own staff, pass the staff UUID here AND set agent_id to yourself (or the chief). The staff's chief_agent_id MUST match agent_id or the call rejects with subagent_chief_mismatch. Use OFIERE_AGENT_OPS list_subagents to discover available staff under a chief.\n` +
285
- `For recurring tasks: set start_date + recurrence_type + recurrence_interval. Example: every 2 minutes = recurrence_type: "minutely", recurrence_interval: 2.\n` +
285
+ `\n` +
286
+ `For recurring tasks: set start_date + recurrence_type + recurrence_interval.\n` +
287
+ `⚠️ CADENCE MAPPING — match the user's spoken phrase EXACTLY (server-side cadence-intent guard rejects mismatches):\n` +
288
+ ` • "every N seconds" → recurrence_type:"minutely" + recurrence_interval:max(1, ceil(N/60)) ← cron resolution = 1 minute\n` +
289
+ ` • "every N minutes" → recurrence_type:"minutely" + recurrence_interval:N ← e.g. every 35 min → minutely+35\n` +
290
+ ` • "every N hours" → recurrence_type:"hourly" + recurrence_interval:N\n` +
291
+ ` • "every N days" → recurrence_type:"daily" + recurrence_interval:N ← daily+1 = 24h gap!\n` +
292
+ ` • "every N weeks" → recurrence_type:"weekly" + recurrence_interval:N\n` +
293
+ ` • "every N months" → recurrence_type:"monthly" + recurrence_interval:N\n` +
294
+ `🛑 NEVER use daily+1 (or hourly+24, etc.) when the user said a minute-level cadence — the task will fire ONCE per 24h instead of every N minutes. This was a real prod bug (v1 smoke 2026-05-17).\n` +
295
+ `\n` +
286
296
  `Approvals: Use add_approval to request sign-off from humans or agents. Approvals are separate from workflow gate nodes.\n` +
287
297
  `Status: PENDING, IN_PROGRESS, DONE, FAILED | Priority: 0=LOW, 1=MEDIUM, 2=HIGH, 3=CRITICAL`,
288
298
  parameters: {
@@ -625,6 +635,106 @@ function normalizeStartDate(
625
635
  };
626
636
  }
627
637
 
638
+ // ── Fix #10 (2026-05-17): Cadence-intent guard ──────────────────────────────
639
+ // v1 smoke incident: LLM picked recurrence_type='daily' + recurrence_interval=1
640
+ // for a task whose title said "every 35 minutes" — so it fired ONCE per 24h
641
+ // instead of every 35 min. Silent test failure. This guard inspects the task
642
+ // text for an explicit "every N <unit>" phrase and rejects the create when the
643
+ // chosen recurrence_type/interval doesn't match. Combined with the tightened
644
+ // OFIERE_TASK_OPS description (cadence-mapping table), prevents the drift at
645
+ // both the LLM-prompt layer and the server-runtime layer.
646
+
647
+ const RECURRENCE_TYPES = ["none", "minutely", "hourly", "daily", "weekly", "monthly"] as const;
648
+ type RecurrenceType = (typeof RECURRENCE_TYPES)[number];
649
+
650
+ function humanizeCadence(type: string, interval: number): string {
651
+ if (type === "none") return "one-shot";
652
+ const unitMap: Record<string, string> = {
653
+ minutely: "minute",
654
+ hourly: "hour",
655
+ daily: "day",
656
+ weekly: "week",
657
+ monthly: "month",
658
+ };
659
+ const unit = unitMap[type] || type;
660
+ return interval === 1 ? `every ${unit}` : `every ${interval} ${unit}s`;
661
+ }
662
+
663
+ function validateRecurrence(
664
+ params: Record<string, unknown>,
665
+ ): { ok: true } | { ok: false; reason: string } {
666
+ const rtRaw = (params.recurrence_type as string | undefined) ?? "none";
667
+ const rt = rtRaw.toLowerCase() as RecurrenceType;
668
+ const riRaw = params.recurrence_interval;
669
+ const ri = typeof riRaw === "number" ? riRaw : (riRaw === undefined ? 1 : Number(riRaw));
670
+
671
+ if (!RECURRENCE_TYPES.includes(rt)) {
672
+ return {
673
+ ok: false,
674
+ reason:
675
+ `Invalid recurrence_type "${rtRaw}". Must be one of: ${RECURRENCE_TYPES.join(", ")}.`,
676
+ };
677
+ }
678
+
679
+ if (rt === "none") return { ok: true };
680
+
681
+ if (!Number.isFinite(ri) || !Number.isInteger(ri) || ri < 1 || ri > 9999) {
682
+ return {
683
+ ok: false,
684
+ reason:
685
+ `Invalid recurrence_interval ${riRaw}. Must be a positive integer between 1 and 9999.`,
686
+ };
687
+ }
688
+
689
+ // Cadence-intent guard: scan title + description for an "every N <unit>"
690
+ // phrase. If present, the chosen recurrence_type+interval MUST match.
691
+ const text = `${(params.title as string) || ""} ${(params.description as string) || ""}`.toLowerCase();
692
+ const cadenceMatch = text.match(
693
+ /every\s+(\d+)\s*(seconds?|secs?|minutes?|mins?|hours?|hrs?|days?|weeks?|months?)\b/,
694
+ );
695
+ if (!cadenceMatch) return { ok: true };
696
+
697
+ const n = parseInt(cadenceMatch[1], 10);
698
+ const unitRaw = cadenceMatch[2];
699
+ let expectedType: RecurrenceType;
700
+ let expectedInterval: number;
701
+ if (/^sec/.test(unitRaw)) {
702
+ expectedType = "minutely";
703
+ expectedInterval = Math.max(1, Math.ceil(n / 60));
704
+ } else if (/^min/.test(unitRaw)) {
705
+ expectedType = "minutely";
706
+ expectedInterval = n;
707
+ } else if (/^(hour|hr)/.test(unitRaw)) {
708
+ expectedType = "hourly";
709
+ expectedInterval = n;
710
+ } else if (/^day/.test(unitRaw)) {
711
+ expectedType = "daily";
712
+ expectedInterval = n;
713
+ } else if (/^week/.test(unitRaw)) {
714
+ expectedType = "weekly";
715
+ expectedInterval = n;
716
+ } else if (/^month/.test(unitRaw)) {
717
+ expectedType = "monthly";
718
+ expectedInterval = n;
719
+ } else {
720
+ return { ok: true };
721
+ }
722
+
723
+ if (rt !== expectedType || ri !== expectedInterval) {
724
+ return {
725
+ ok: false,
726
+ reason:
727
+ `Recurrence mismatch: task text says "every ${n} ${unitRaw}" but you passed ` +
728
+ `recurrence_type="${rt}", recurrence_interval=${ri} (= ${humanizeCadence(rt, ri)}). ` +
729
+ `Correct combo: recurrence_type="${expectedType}", recurrence_interval=${expectedInterval} ` +
730
+ `(= ${humanizeCadence(expectedType, expectedInterval)}). ` +
731
+ `Re-call with the matching values, OR remove the "every N ${unitRaw}" phrase from the title/description if you intended a different cadence.`,
732
+ };
733
+ }
734
+
735
+ return { ok: true };
736
+ }
737
+
628
738
  async function handleCreateTask(
629
739
  supabase: SupabaseClient,
630
740
  userId: string,
@@ -635,6 +745,10 @@ async function handleCreateTask(
635
745
  try {
636
746
  if (!params.title) return err("Missing required field: title");
637
747
 
748
+ // Fix #10 (2026-05-17): cadence-intent guard — see helper comment above.
749
+ const recurCheck = validateRecurrence(params);
750
+ if (!recurCheck.ok) return err(recurCheck.reason);
751
+
638
752
  const id = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
639
753
  const now = new Date().toISOString();
640
754
 
@@ -858,9 +972,19 @@ async function handleCreateTask(
858
972
  if (cf.constraints) extras.push(`${(cf.constraints as any[]).length} constraints`);
859
973
  if (cf.system_prompt) extras.push("custom system prompt");
860
974
 
861
- // BUG 6 fix: surface recurrence fields in create response
975
+ // BUG 6 fix: surface recurrence fields in create response.
976
+ // Fix #10 (2026-05-17): also surface effective_cadence so the LLM sees the
977
+ // human-readable interpretation (e.g. "every 35 minutes") of the values it
978
+ // just passed — defense-in-depth alongside validateRecurrence().
862
979
  const recurrenceInfo = (params.recurrence_type && params.recurrence_type !== "none")
863
- ? { recurrence_type: params.recurrence_type, recurrence_interval: (params.recurrence_interval as number) || 1 }
980
+ ? {
981
+ recurrence_type: params.recurrence_type,
982
+ recurrence_interval: (params.recurrence_interval as number) || 1,
983
+ effective_cadence: humanizeCadence(
984
+ params.recurrence_type as string,
985
+ (params.recurrence_interval as number) || 1,
986
+ ),
987
+ }
864
988
  : undefined;
865
989
 
866
990
  // BUG 7 fix: only claim scheduling when bridge actually fired