ofiere-openclaw-plugin 4.51.0 → 4.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/src/tools.js CHANGED
@@ -233,7 +233,7 @@ function registerTaskOps(api, supabase, userId, resolveAgent, timezone) {
233
233
  `Actions:\n` +
234
234
  `- "list": List/filter tasks. Optional: status, agent_id, space_id, folder_id, task_id, limit\n` +
235
235
  `- "get": Get a single task by ID. Required: task_id\n` +
236
- `- "create": Create a task. Required: title. Optional: agent_id, subagent_id, description, status, priority, space_id, folder_id, start_date, due_date, tags, instructions, execution_plan, goals, constraints, system_prompt, recurrence_type, recurrence_interval, scheduled_time. ⚠️ PLANNING GATE: If the task has 3+ execution steps, goals, or constraints, FIRST ask the user "Plan first or create directly?" If they choose to plan, use OFIERE_PLAN_OPS instead.\n` +
236
+ `- "create": Create a task in DRAFT mode. Required: title. Optional: agent_id, subagent_id, description, status, priority, space_id, folder_id, start_date, due_date, dispatch_immediately, tags, instructions, execution_plan, goals, constraints, system_prompt, recurrence_type, recurrence_interval, scheduled_time. ⚠️ EXECUTION SEMANTICS: omit start_date → task sits in agent's task list as draft (does NOT run). Set start_date to schedule. Pass dispatch_immediately:true to run now. ⚠️ PLANNING GATE: If the task has 3+ execution steps, goals, or constraints, FIRST ask the user "Plan first or create directly?" If they choose to plan, use OFIERE_PLAN_OPS instead.\n` +
237
237
  `- "update": Update a task. Required: task_id. Optional: all create fields + progress + subagent_id\n` +
238
238
  `- "delete": Delete task + subtasks. Required: task_id\n` +
239
239
  `- "add_approval": Request approval on a task. Required: task_id, approver_name. Optional: approver_type (human|agent, auto-detected), due_date, comment\n` +
@@ -279,8 +279,12 @@ function registerTaskOps(api, supabase, userId, resolveAgent, timezone) {
279
279
  progress: { type: "number", description: "Progress percentage 0-100 (update only)" },
280
280
  space_id: { type: "string", description: "PM Space ID" },
281
281
  folder_id: { type: "string", description: "PM Folder ID" },
282
- start_date: { type: "string", description: "Start date (ISO 8601). Required for scheduled/recurring tasks." },
282
+ start_date: { type: "string", description: "Start date (ISO 8601). Required for scheduled/recurring tasks. Omit to create as draft." },
283
283
  due_date: { type: "string", description: "Due date (ISO 8601)" },
284
+ dispatch_immediately: {
285
+ type: "boolean",
286
+ description: "When true and start_date is not provided, sets start_date to NOW so the task dispatches on the next cron tick. Default: false (task is created as draft and waits for an explicit start_date).",
287
+ },
284
288
  scheduled_time: { type: "string", description: "Time to execute in HH:MM format (user's LOCAL time, e.g. 10:00 for 10 AM WIB). The system converts to UTC automatically." },
285
289
  recurrence_type: {
286
290
  type: "string",
@@ -665,7 +669,18 @@ async function handleCreateTask(supabase, userId, resolveAgent, params, fallback
665
669
  // INSERTed the raw input then reassigned insertData.start_date afterward,
666
670
  // which left the DB row with the unconverted value and the response with
667
671
  // the converted one — confusing and unsafe for the scheduler.
668
- const rawStartDate = params.start_date || null;
672
+ //
673
+ // Cycle 13 (BUG 3+4): create is a DRAFT operation, NOT a dispatch trigger.
674
+ // No start_date → rawStartDate stays null → no scheduler_event auto-insert
675
+ // → task sits in agent's task list as draft. To execute, caller must pass
676
+ // an explicit start_date OR set dispatch_immediately:true (which we then
677
+ // resolve to "now" ISO here). Cycle 12's silent +5s default for staff
678
+ // delegation is REMOVED — it turned create into an unsafe execution
679
+ // trigger and bypassed the chief's approval flow.
680
+ const explicitStartDate = params.start_date || null;
681
+ const dispatchNow = params.dispatch_immediately === true;
682
+ const rawStartDate = explicitStartDate
683
+ ?? (dispatchNow ? new Date().toISOString() : null);
669
684
  const normalized = rawStartDate
670
685
  ? normalizeStartDate(rawStartDate, params.scheduled_time, TZ_OFFSET_HOURS)
671
686
  : null;
@@ -798,8 +813,18 @@ async function handleUpdateTask(supabase, userId, params) {
798
813
  if (params[f] !== undefined)
799
814
  updates[f] = params[f];
800
815
  }
801
- if (params.status === "DONE")
816
+ // Cycle 13 (BUG 2): keep status and completed_at consistent. Setting status
817
+ // to DONE stamps completed_at + progress=100. Setting status to anything
818
+ // other than DONE clears completed_at so the row never carries the
819
+ // "PENDING + completed_at" contradiction the dashboard reported.
820
+ if (params.status === "DONE") {
802
821
  updates.completed_at = new Date().toISOString();
822
+ if (params.progress === undefined)
823
+ updates.progress = 100;
824
+ }
825
+ else if (typeof params.status === "string") {
826
+ updates.completed_at = null;
827
+ }
803
828
  // ── Subagent + chief invariant ───────────────────────────────────────
804
829
  // Mirrors dashboard PATCH: chief change without explicit subagent_id
805
830
  // clears any existing subagent assignment; subagent_id alone validates
@@ -2762,11 +2787,68 @@ async function generatePMReportDirect(supabase, userId, scopeType, scopeId, agen
2762
2787
  }
2763
2788
  lines.push("");
2764
2789
  lines.push(`💡 Reply "details on #N" for full task result`);
2790
+ // Cycle 13 (BUG 1): render the "Generated" stamp in the user's IANA TZ
2791
+ // (from profiles.timezone) instead of UTC. The plugin can't import
2792
+ // dashboard helpers, so this uses Intl.DateTimeFormat directly.
2793
+ const userTz = await loadUserTimezoneForPlugin(supabase, userId);
2765
2794
  const now = new Date();
2766
2795
  lines.push("");
2767
- lines.push(`Generated: ${now.toISOString().replace("T", " ").slice(0, 16)} UTC`);
2796
+ lines.push(`Generated: ${formatInUserTzInline(now, userTz)}`);
2768
2797
  return { report: lines.join("\n"), scopeLabel, taskCount: all.length };
2769
2798
  }
2799
+ // ─── Plugin-local TZ helpers (Cycle 13, BUG 1) ──────────────────────────────
2800
+ // Mirror dashboard/lib/tz.ts; kept inline because the plugin is a separate
2801
+ // npm package loaded directly by the OpenClaw gateway and cannot import from
2802
+ // the dashboard.
2803
+ const PLUGIN_FALLBACK_TZ = "Asia/Jakarta";
2804
+ async function loadUserTimezoneForPlugin(supabase, userId) {
2805
+ if (!userId)
2806
+ return PLUGIN_FALLBACK_TZ;
2807
+ try {
2808
+ const { data } = await supabase
2809
+ .from("profiles")
2810
+ .select("timezone")
2811
+ .eq("id", userId)
2812
+ .maybeSingle();
2813
+ const tz = data?.timezone?.trim();
2814
+ return tz && isValidIanaZoneInline(tz) ? tz : PLUGIN_FALLBACK_TZ;
2815
+ }
2816
+ catch {
2817
+ return PLUGIN_FALLBACK_TZ;
2818
+ }
2819
+ }
2820
+ function formatInUserTzInline(date, timezone) {
2821
+ if (Number.isNaN(date.getTime()))
2822
+ return "";
2823
+ const tz = isValidIanaZoneInline(timezone) ? timezone : PLUGIN_FALLBACK_TZ;
2824
+ try {
2825
+ const fmt = new Intl.DateTimeFormat("en-US", {
2826
+ timeZone: tz,
2827
+ year: "numeric",
2828
+ month: "short",
2829
+ day: "numeric",
2830
+ hour: "2-digit",
2831
+ minute: "2-digit",
2832
+ hour12: false,
2833
+ timeZoneName: "short",
2834
+ });
2835
+ return fmt.format(date).replace(", ", " ");
2836
+ }
2837
+ catch {
2838
+ return date.toISOString();
2839
+ }
2840
+ }
2841
+ function isValidIanaZoneInline(tz) {
2842
+ if (!tz)
2843
+ return false;
2844
+ try {
2845
+ new Intl.DateTimeFormat("en-US", { timeZone: tz }).format(new Date());
2846
+ return true;
2847
+ }
2848
+ catch {
2849
+ return false;
2850
+ }
2851
+ }
2770
2852
  async function dispatchReportDirect(supabase, userId, agentId, report, channelTypes) {
2771
2853
  // Query agent's active channel bindings
2772
2854
  let query = supabase.from("channel_bindings").select("channel_type, channel_account_id, agent_id, channel_config, thread_id")
@@ -6581,6 +6663,52 @@ function registerBrainExtractionHook(api, supabase, userId, fallbackAgentId) {
6581
6663
  catch (logErr) {
6582
6664
  api.logger.debug?.(`[ofiere-staff-report] dispatch_log update failed: ${logErr instanceof Error ? logErr.message : String(logErr)}`);
6583
6665
  }
6666
+ // Cycle 13 (BUG 2): flip the tasks row so the dashboard reflects
6667
+ // execution outcome. Without this, tasks stay at PENDING/IN_PROGRESS
6668
+ // forever and the proof-of-execution string lives only in
6669
+ // task_dispatch_log.response_preview (invisible to PM views).
6670
+ // Recurring tasks: stash output but leave status untouched so the
6671
+ // next cron tick can re-fire. One-shot: mark DONE, set progress=100,
6672
+ // stamp completed_at, persist proof in custom_fields.last_output.
6673
+ try {
6674
+ const { data: schedRow } = await supabase
6675
+ .from("scheduler_events")
6676
+ .select("recurrence_type")
6677
+ .eq("task_id", taskId)
6678
+ .maybeSingle();
6679
+ const isRecurring = !!(schedRow?.recurrence_type && schedRow.recurrence_type !== "none");
6680
+ const { data: curTask } = await supabase
6681
+ .from("tasks")
6682
+ .select("custom_fields, status")
6683
+ .eq("id", taskId)
6684
+ .eq("user_id", userId)
6685
+ .maybeSingle();
6686
+ const cf = (curTask?.custom_fields ?? {});
6687
+ cf.last_output = excerpt;
6688
+ cf.last_completed_session = sessionKey ?? null;
6689
+ cf.last_run_at = new Date().toISOString();
6690
+ if (isRecurring) {
6691
+ await supabase.from("tasks")
6692
+ .update({ custom_fields: cf, updated_at: new Date().toISOString() })
6693
+ .eq("id", taskId)
6694
+ .eq("user_id", userId);
6695
+ }
6696
+ else if (curTask?.status !== "FAILED") {
6697
+ await supabase.from("tasks")
6698
+ .update({
6699
+ status: "DONE",
6700
+ completed_at: new Date().toISOString(),
6701
+ progress: 100,
6702
+ custom_fields: cf,
6703
+ updated_at: new Date().toISOString(),
6704
+ })
6705
+ .eq("id", taskId)
6706
+ .eq("user_id", userId);
6707
+ }
6708
+ }
6709
+ catch (tErr) {
6710
+ api.logger.debug?.(`[ofiere-staff-report] tasks update failed: ${tErr instanceof Error ? tErr.message : String(tErr)}`);
6711
+ }
6584
6712
  }
6585
6713
  // BUG 6 fix (BUGSHOOT-2): mirror the assistant turn into the
6586
6714
  // dashboard's hidden conversation_messages so the dashboard can
@@ -6664,6 +6792,50 @@ function registerBrainExtractionHook(api, supabase, userId, fallbackAgentId) {
6664
6792
  catch (logErr) {
6665
6793
  api.logger.debug?.(`[ofiere-brain] dispatch_log update failed: ${logErr instanceof Error ? logErr.message : String(logErr)}`);
6666
6794
  }
6795
+ // Cycle 13 (BUG 2): flip the tasks row for chief-direct (non-staff)
6796
+ // dispatches too — same logic as the staff path above. Recurring
6797
+ // tasks keep their status; one-shots flip to DONE with the proof
6798
+ // string stored in custom_fields.last_output.
6799
+ try {
6800
+ const excerpt = lastAssistant.slice(0, 1500);
6801
+ const { data: schedRow } = await supabase
6802
+ .from("scheduler_events")
6803
+ .select("recurrence_type")
6804
+ .eq("task_id", dispatchTaskIdFromCtx)
6805
+ .maybeSingle();
6806
+ const isRecurring = !!(schedRow?.recurrence_type && schedRow.recurrence_type !== "none");
6807
+ const { data: curTask } = await supabase
6808
+ .from("tasks")
6809
+ .select("custom_fields, status")
6810
+ .eq("id", dispatchTaskIdFromCtx)
6811
+ .eq("user_id", userId)
6812
+ .maybeSingle();
6813
+ const cf = (curTask?.custom_fields ?? {});
6814
+ cf.last_output = excerpt;
6815
+ cf.last_completed_session = sessionKey ?? null;
6816
+ cf.last_run_at = new Date().toISOString();
6817
+ if (isRecurring) {
6818
+ await supabase.from("tasks")
6819
+ .update({ custom_fields: cf, updated_at: new Date().toISOString() })
6820
+ .eq("id", dispatchTaskIdFromCtx)
6821
+ .eq("user_id", userId);
6822
+ }
6823
+ else if (curTask?.status !== "FAILED") {
6824
+ await supabase.from("tasks")
6825
+ .update({
6826
+ status: "DONE",
6827
+ completed_at: new Date().toISOString(),
6828
+ progress: 100,
6829
+ custom_fields: cf,
6830
+ updated_at: new Date().toISOString(),
6831
+ })
6832
+ .eq("id", dispatchTaskIdFromCtx)
6833
+ .eq("user_id", userId);
6834
+ }
6835
+ }
6836
+ catch (tErr) {
6837
+ api.logger.debug?.(`[ofiere-brain] tasks update failed: ${tErr instanceof Error ? tErr.message : String(tErr)}`);
6838
+ }
6667
6839
  }
6668
6840
  // Cycle 7b BUGSHOOT-4 — trivial-skip moved here from line ~6593.
6669
6841
  // Brain L1/L2/L3/L4 extraction skips trivial chit-chat to reduce
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ofiere-openclaw-plugin",
3
- "version": "4.51.0",
3
+ "version": "4.53.0",
4
4
  "type": "module",
5
5
  "description": "OpenClaw plugin for Ofiere PM - 16 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, and corporate frameworks",
6
6
  "keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
package/src/tools.ts CHANGED
@@ -271,7 +271,7 @@ function registerTaskOps(
271
271
  `Actions:\n` +
272
272
  `- "list": List/filter tasks. Optional: status, agent_id, space_id, folder_id, task_id, limit\n` +
273
273
  `- "get": Get a single task by ID. Required: task_id\n` +
274
- `- "create": Create a task. Required: title. Optional: agent_id, subagent_id, description, status, priority, space_id, folder_id, start_date, due_date, tags, instructions, execution_plan, goals, constraints, system_prompt, recurrence_type, recurrence_interval, scheduled_time. ⚠️ PLANNING GATE: If the task has 3+ execution steps, goals, or constraints, FIRST ask the user "Plan first or create directly?" If they choose to plan, use OFIERE_PLAN_OPS instead.\n` +
274
+ `- "create": Create a task in DRAFT mode. Required: title. Optional: agent_id, subagent_id, description, status, priority, space_id, folder_id, start_date, due_date, dispatch_immediately, tags, instructions, execution_plan, goals, constraints, system_prompt, recurrence_type, recurrence_interval, scheduled_time. ⚠️ EXECUTION SEMANTICS: omit start_date → task sits in agent's task list as draft (does NOT run). Set start_date to schedule. Pass dispatch_immediately:true to run now. ⚠️ PLANNING GATE: If the task has 3+ execution steps, goals, or constraints, FIRST ask the user "Plan first or create directly?" If they choose to plan, use OFIERE_PLAN_OPS instead.\n` +
275
275
  `- "update": Update a task. Required: task_id. Optional: all create fields + progress + subagent_id\n` +
276
276
  `- "delete": Delete task + subtasks. Required: task_id\n` +
277
277
  `- "add_approval": Request approval on a task. Required: task_id, approver_name. Optional: approver_type (human|agent, auto-detected), due_date, comment\n` +
@@ -317,8 +317,12 @@ function registerTaskOps(
317
317
  progress: { type: "number", description: "Progress percentage 0-100 (update only)" },
318
318
  space_id: { type: "string", description: "PM Space ID" },
319
319
  folder_id: { type: "string", description: "PM Folder ID" },
320
- start_date: { type: "string", description: "Start date (ISO 8601). Required for scheduled/recurring tasks." },
320
+ start_date: { type: "string", description: "Start date (ISO 8601). Required for scheduled/recurring tasks. Omit to create as draft." },
321
321
  due_date: { type: "string", description: "Due date (ISO 8601)" },
322
+ dispatch_immediately: {
323
+ type: "boolean",
324
+ description: "When true and start_date is not provided, sets start_date to NOW so the task dispatches on the next cron tick. Default: false (task is created as draft and waits for an explicit start_date).",
325
+ },
322
326
  scheduled_time: { type: "string", description: "Time to execute in HH:MM format (user's LOCAL time, e.g. 10:00 for 10 AM WIB). The system converts to UTC automatically." },
323
327
  recurrence_type: {
324
328
  type: "string",
@@ -748,7 +752,18 @@ async function handleCreateTask(
748
752
  // INSERTed the raw input then reassigned insertData.start_date afterward,
749
753
  // which left the DB row with the unconverted value and the response with
750
754
  // the converted one — confusing and unsafe for the scheduler.
751
- const rawStartDate = (params.start_date as string) || null;
755
+ //
756
+ // Cycle 13 (BUG 3+4): create is a DRAFT operation, NOT a dispatch trigger.
757
+ // No start_date → rawStartDate stays null → no scheduler_event auto-insert
758
+ // → task sits in agent's task list as draft. To execute, caller must pass
759
+ // an explicit start_date OR set dispatch_immediately:true (which we then
760
+ // resolve to "now" ISO here). Cycle 12's silent +5s default for staff
761
+ // delegation is REMOVED — it turned create into an unsafe execution
762
+ // trigger and bypassed the chief's approval flow.
763
+ const explicitStartDate = (params.start_date as string) || null;
764
+ const dispatchNow = params.dispatch_immediately === true;
765
+ const rawStartDate = explicitStartDate
766
+ ?? (dispatchNow ? new Date().toISOString() : null);
752
767
  const normalized = rawStartDate
753
768
  ? normalizeStartDate(rawStartDate, params.scheduled_time as string | undefined, TZ_OFFSET_HOURS)
754
769
  : null;
@@ -887,7 +902,16 @@ async function handleUpdateTask(
887
902
  for (const f of fields) {
888
903
  if (params[f] !== undefined) updates[f] = params[f];
889
904
  }
890
- if (params.status === "DONE") updates.completed_at = new Date().toISOString();
905
+ // Cycle 13 (BUG 2): keep status and completed_at consistent. Setting status
906
+ // to DONE stamps completed_at + progress=100. Setting status to anything
907
+ // other than DONE clears completed_at so the row never carries the
908
+ // "PENDING + completed_at" contradiction the dashboard reported.
909
+ if (params.status === "DONE") {
910
+ updates.completed_at = new Date().toISOString();
911
+ if (params.progress === undefined) updates.progress = 100;
912
+ } else if (typeof params.status === "string") {
913
+ updates.completed_at = null;
914
+ }
891
915
 
892
916
  // ── Subagent + chief invariant ───────────────────────────────────────
893
917
  // Mirrors dashboard PATCH: chief change without explicit subagent_id
@@ -2932,13 +2956,72 @@ async function generatePMReportDirect(
2932
2956
  lines.push("");
2933
2957
  lines.push(`💡 Reply "details on #N" for full task result`);
2934
2958
 
2959
+ // Cycle 13 (BUG 1): render the "Generated" stamp in the user's IANA TZ
2960
+ // (from profiles.timezone) instead of UTC. The plugin can't import
2961
+ // dashboard helpers, so this uses Intl.DateTimeFormat directly.
2962
+ const userTz = await loadUserTimezoneForPlugin(supabase, userId);
2935
2963
  const now = new Date();
2936
2964
  lines.push("");
2937
- lines.push(`Generated: ${now.toISOString().replace("T", " ").slice(0, 16)} UTC`);
2965
+ lines.push(`Generated: ${formatInUserTzInline(now, userTz)}`);
2938
2966
 
2939
2967
  return { report: lines.join("\n"), scopeLabel, taskCount: all.length };
2940
2968
  }
2941
2969
 
2970
+ // ─── Plugin-local TZ helpers (Cycle 13, BUG 1) ──────────────────────────────
2971
+ // Mirror dashboard/lib/tz.ts; kept inline because the plugin is a separate
2972
+ // npm package loaded directly by the OpenClaw gateway and cannot import from
2973
+ // the dashboard.
2974
+
2975
+ const PLUGIN_FALLBACK_TZ = "Asia/Jakarta";
2976
+
2977
+ async function loadUserTimezoneForPlugin(
2978
+ supabase: SupabaseClient,
2979
+ userId: string,
2980
+ ): Promise<string> {
2981
+ if (!userId) return PLUGIN_FALLBACK_TZ;
2982
+ try {
2983
+ const { data } = await supabase
2984
+ .from("profiles")
2985
+ .select("timezone")
2986
+ .eq("id", userId)
2987
+ .maybeSingle();
2988
+ const tz = ((data as any)?.timezone as string | undefined)?.trim();
2989
+ return tz && isValidIanaZoneInline(tz) ? tz : PLUGIN_FALLBACK_TZ;
2990
+ } catch {
2991
+ return PLUGIN_FALLBACK_TZ;
2992
+ }
2993
+ }
2994
+
2995
+ function formatInUserTzInline(date: Date, timezone: string): string {
2996
+ if (Number.isNaN(date.getTime())) return "";
2997
+ const tz = isValidIanaZoneInline(timezone) ? timezone : PLUGIN_FALLBACK_TZ;
2998
+ try {
2999
+ const fmt = new Intl.DateTimeFormat("en-US", {
3000
+ timeZone: tz,
3001
+ year: "numeric",
3002
+ month: "short",
3003
+ day: "numeric",
3004
+ hour: "2-digit",
3005
+ minute: "2-digit",
3006
+ hour12: false,
3007
+ timeZoneName: "short",
3008
+ });
3009
+ return fmt.format(date).replace(", ", " ");
3010
+ } catch {
3011
+ return date.toISOString();
3012
+ }
3013
+ }
3014
+
3015
+ function isValidIanaZoneInline(tz: string | undefined | null): boolean {
3016
+ if (!tz) return false;
3017
+ try {
3018
+ new Intl.DateTimeFormat("en-US", { timeZone: tz }).format(new Date());
3019
+ return true;
3020
+ } catch {
3021
+ return false;
3022
+ }
3023
+ }
3024
+
2942
3025
  async function dispatchReportDirect(
2943
3026
  supabase: SupabaseClient,
2944
3027
  userId: string,
@@ -6743,6 +6826,53 @@ function registerBrainExtractionHook(
6743
6826
  } catch (logErr) {
6744
6827
  api.logger.debug?.(`[ofiere-staff-report] dispatch_log update failed: ${logErr instanceof Error ? logErr.message : String(logErr)}`);
6745
6828
  }
6829
+
6830
+ // Cycle 13 (BUG 2): flip the tasks row so the dashboard reflects
6831
+ // execution outcome. Without this, tasks stay at PENDING/IN_PROGRESS
6832
+ // forever and the proof-of-execution string lives only in
6833
+ // task_dispatch_log.response_preview (invisible to PM views).
6834
+ // Recurring tasks: stash output but leave status untouched so the
6835
+ // next cron tick can re-fire. One-shot: mark DONE, set progress=100,
6836
+ // stamp completed_at, persist proof in custom_fields.last_output.
6837
+ try {
6838
+ const { data: schedRow } = await supabase
6839
+ .from("scheduler_events")
6840
+ .select("recurrence_type")
6841
+ .eq("task_id", taskId)
6842
+ .maybeSingle();
6843
+ const isRecurring = !!(schedRow?.recurrence_type && schedRow.recurrence_type !== "none");
6844
+
6845
+ const { data: curTask } = await supabase
6846
+ .from("tasks")
6847
+ .select("custom_fields, status")
6848
+ .eq("id", taskId)
6849
+ .eq("user_id", userId)
6850
+ .maybeSingle();
6851
+ const cf = ((curTask?.custom_fields as Record<string, any>) ?? {}) as Record<string, any>;
6852
+ cf.last_output = excerpt;
6853
+ cf.last_completed_session = sessionKey ?? null;
6854
+ cf.last_run_at = new Date().toISOString();
6855
+
6856
+ if (isRecurring) {
6857
+ await supabase.from("tasks")
6858
+ .update({ custom_fields: cf, updated_at: new Date().toISOString() })
6859
+ .eq("id", taskId)
6860
+ .eq("user_id", userId);
6861
+ } else if (curTask?.status !== "FAILED") {
6862
+ await supabase.from("tasks")
6863
+ .update({
6864
+ status: "DONE",
6865
+ completed_at: new Date().toISOString(),
6866
+ progress: 100,
6867
+ custom_fields: cf,
6868
+ updated_at: new Date().toISOString(),
6869
+ })
6870
+ .eq("id", taskId)
6871
+ .eq("user_id", userId);
6872
+ }
6873
+ } catch (tErr) {
6874
+ api.logger.debug?.(`[ofiere-staff-report] tasks update failed: ${tErr instanceof Error ? tErr.message : String(tErr)}`);
6875
+ }
6746
6876
  }
6747
6877
 
6748
6878
  // BUG 6 fix (BUGSHOOT-2): mirror the assistant turn into the
@@ -6825,6 +6955,51 @@ function registerBrainExtractionHook(
6825
6955
  } catch (logErr) {
6826
6956
  api.logger.debug?.(`[ofiere-brain] dispatch_log update failed: ${logErr instanceof Error ? logErr.message : String(logErr)}`);
6827
6957
  }
6958
+
6959
+ // Cycle 13 (BUG 2): flip the tasks row for chief-direct (non-staff)
6960
+ // dispatches too — same logic as the staff path above. Recurring
6961
+ // tasks keep their status; one-shots flip to DONE with the proof
6962
+ // string stored in custom_fields.last_output.
6963
+ try {
6964
+ const excerpt = lastAssistant.slice(0, 1500);
6965
+ const { data: schedRow } = await supabase
6966
+ .from("scheduler_events")
6967
+ .select("recurrence_type")
6968
+ .eq("task_id", dispatchTaskIdFromCtx)
6969
+ .maybeSingle();
6970
+ const isRecurring = !!(schedRow?.recurrence_type && schedRow.recurrence_type !== "none");
6971
+
6972
+ const { data: curTask } = await supabase
6973
+ .from("tasks")
6974
+ .select("custom_fields, status")
6975
+ .eq("id", dispatchTaskIdFromCtx)
6976
+ .eq("user_id", userId)
6977
+ .maybeSingle();
6978
+ const cf = ((curTask?.custom_fields as Record<string, any>) ?? {}) as Record<string, any>;
6979
+ cf.last_output = excerpt;
6980
+ cf.last_completed_session = sessionKey ?? null;
6981
+ cf.last_run_at = new Date().toISOString();
6982
+
6983
+ if (isRecurring) {
6984
+ await supabase.from("tasks")
6985
+ .update({ custom_fields: cf, updated_at: new Date().toISOString() })
6986
+ .eq("id", dispatchTaskIdFromCtx)
6987
+ .eq("user_id", userId);
6988
+ } else if (curTask?.status !== "FAILED") {
6989
+ await supabase.from("tasks")
6990
+ .update({
6991
+ status: "DONE",
6992
+ completed_at: new Date().toISOString(),
6993
+ progress: 100,
6994
+ custom_fields: cf,
6995
+ updated_at: new Date().toISOString(),
6996
+ })
6997
+ .eq("id", dispatchTaskIdFromCtx)
6998
+ .eq("user_id", userId);
6999
+ }
7000
+ } catch (tErr) {
7001
+ api.logger.debug?.(`[ofiere-brain] tasks update failed: ${tErr instanceof Error ? tErr.message : String(tErr)}`);
7002
+ }
6828
7003
  }
6829
7004
 
6830
7005
  // Cycle 7b BUGSHOOT-4 — trivial-skip moved here from line ~6593.