ofiere-openclaw-plugin 4.24.0 → 4.26.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/package.json +1 -1
- package/src/prompt.ts +17 -3
- package/src/tools.ts +196 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ofiere-openclaw-plugin",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.26.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw plugin for Ofiere PM - 13 meta-tools covering tasks, agents, projects, scheduling, knowledge, workflows, notifications, memory, prompts, constellation, space file management, execution plan builder, and SOP management",
|
|
6
6
|
"keywords": ["openclaw", "ofiere", "project-management", "agents", "plugin"],
|
package/src/prompt.ts
CHANGED
|
@@ -62,14 +62,20 @@ const TOOL_DOCS: Record<string, string> = {
|
|
|
62
62
|
- Edge handles: condition edges use sourceHandle "condition-true"/"condition-false". Loop edges use "loop_body"/"done"
|
|
63
63
|
- Variables: Use {{prev.nodeId.outputText}} for prior outputs, {{variables.key}} for stored variables`,
|
|
64
64
|
|
|
65
|
-
OFIERE_NOTIFY_OPS: `- **OFIERE_NOTIFY_OPS** — Notifications & Channel Reports (action: "list", "mark_read", "mark_all_read", "delete", "send_report", "schedule_report", "list_schedules", "delete_schedule")
|
|
65
|
+
OFIERE_NOTIFY_OPS: `- **OFIERE_NOTIFY_OPS** — Notifications & Channel Reports (action: "list", "mark_read", "mark_all_read", "delete", "send_report", "get_task_detail", "schedule_report", "list_schedules", "delete_schedule")
|
|
66
66
|
- list: Recent notifications. unread_only=true for unread only
|
|
67
67
|
- mark_read: Mark one notification read by ID
|
|
68
68
|
- mark_all_read: Mark all as read
|
|
69
|
-
- send_report: Send a PM progress report to YOUR connected channels (Telegram, Discord, Slack, etc.)
|
|
69
|
+
- send_report: Send a numbered PM progress report to YOUR connected channels (Telegram, Discord, Slack, etc.)
|
|
70
70
|
- Required: scope_type (space|folder|project|task|all), agent_id (YOUR name, e.g. "thalia")
|
|
71
71
|
- Optional: scope_id, channel_types[] (filter to specific channels), include_completed
|
|
72
72
|
- The report is automatically generated from current PM data and sent through YOUR channel bindings ONLY
|
|
73
|
+
- Tasks are numbered globally (1, 2, 3...) across Active/Pending/Done sections. Recurring tasks that executed recently appear in BOTH Done (today's run) and Pending (next cycle)
|
|
74
|
+
- get_task_detail: Get the FULL result/content of a task by its report number
|
|
75
|
+
- Required: task_number (1-indexed number from the report)
|
|
76
|
+
- Optional: scope_type (default all), scope_id
|
|
77
|
+
- Returns: complete task description (where execution results live), custom_fields, status, tags, and all metadata
|
|
78
|
+
- Use when user asks "details on number X", "what's task 2 result", "show me #3", etc.
|
|
73
79
|
- schedule_report: Create a recurring/scheduled report
|
|
74
80
|
- Required: scope_type, recurrence_type (hourly|daily|weekly|monthly), agent_id (YOUR name)
|
|
75
81
|
- Optional: scope_id, scope_label, channel_types[], recurrence_time (HH:MM UTC, default 09:00), recurrence_interval, recurrence_days_of_week (e.g. "mon,wed,fri"), include_completed
|
|
@@ -202,7 +208,8 @@ ${toolDocs}
|
|
|
202
208
|
- When task instructions or system prompts contain file references like @[filename](file:FILE_ID), use OFIERE_FILE_OPS action:"read_text_file" file_id:"FILE_ID" to read the file content. Do NOT ask the user for the file — retrieve it yourself.
|
|
203
209
|
- Use OFIERE_FILE_OPS to create output files (reports, data, configs) in the Space Files explorer. Prefer create_text_file for text-based outputs.
|
|
204
210
|
- To save task output as a file, call OFIERE_FILE_OPS action:"create_text_file" with the space_id from the task context.
|
|
205
|
-
- CHANNEL REPORTS: When the user asks you to "send a report", "send progress", "update me on Telegram/Discord/Slack/WhatsApp", use OFIERE_NOTIFY_OPS action:"send_report" with agent_id set to YOUR name. ALWAYS include agent_id — without it the report will fail. The report is generated from live PM data and sent through YOUR connected channels ONLY — not other agents' channels.
|
|
211
|
+
- CHANNEL REPORTS: When the user asks you to "send a report", "send progress", "update me on Telegram/Discord/Slack/WhatsApp", use OFIERE_NOTIFY_OPS action:"send_report" with agent_id set to YOUR name. ALWAYS include agent_id — without it the report will fail. The report is generated from live PM data and sent through YOUR connected channels ONLY — not other agents' channels. Tasks are numbered (1, 2, 3...) for easy reference.
|
|
212
|
+
- TASK DRILL-DOWN: When the user asks "details on number X", "what's task X result", "show me #X", or any variation referencing a numbered task from a report, use OFIERE_NOTIFY_OPS action:"get_task_detail" with task_number set to the number they mentioned. The description field contains the actual execution result (e.g. scan findings, analysis output). Present the description content as the primary response — that IS the task result.
|
|
206
213
|
- To set up recurring reports (e.g. "send me a daily report at 9am"), use OFIERE_NOTIFY_OPS action:"schedule_report" with scope_type, recurrence_type, and agent_id set to YOUR name.
|
|
207
214
|
- If a report is too long for the channel's message limit, save the full report as a markdown file using OFIERE_FILE_OPS action:"create_text_file" and send a summary to the channel instead.
|
|
208
215
|
- PLANNING WORKFLOW: Use OFIERE_PLAN_OPS to build complex multi-step execution flows BEFORE creating individual tasks. Build the plan → let the user review in the Planning Tab → execute when approved.
|
|
@@ -215,6 +222,13 @@ ${toolDocs}
|
|
|
215
222
|
- When creating SOPs for department chiefs (Thalia=CMO, Ivy=COO, Daisy=CTO-Intel, Celia=CTO-Eng), tailor content to their domain expertise.
|
|
216
223
|
- Prerequisites should be actionable checklist items. Success criteria should be measurable outcomes.
|
|
217
224
|
- After creating an SOP, suggest the agent set it to "active" status when ready for execution.
|
|
225
|
+
- PLANNING GATE: Before creating ANY task with 3+ execution steps, a due_date, or when the user describes a multi-phase project, ALWAYS ask: "Should I create a Plan first so you can review the structure before I create individual tasks?" If the user says yes, use OFIERE_PLAN_OPS. If they say no or it's a simple one-shot task, proceed directly with OFIERE_TASK_OPS.
|
|
226
|
+
- TASK PLACEMENT — SOP-AWARE ROUTING: When creating a task, the system auto-assigns a PM space. But for FOLDER placement, follow this protocol:
|
|
227
|
+
1. If the user explicitly provides space_id or folder_id, use those directly.
|
|
228
|
+
2. If not, and you have active SOPs (loaded via 🔴 COMPLEX assessment), check if any SOP defines an operating structure or folder routing (e.g. "place marketing tasks in Marketing/Campaigns"). Follow the SOP's structure.
|
|
229
|
+
3. If no SOP guidance exists, check the Space Files tab for an operating map/structure document using OFIERE_FILE_OPS action:"list_files". If found, read it with "read_text_file" and follow its routing rules.
|
|
230
|
+
4. If no routing guidance exists at all, ask the user: "I can place this task in your default space root, or create a new folder/project for it. Which do you prefer?"
|
|
231
|
+
5. Do NOT blindly dump all SOPs to check routing — smart-select only SOPs whose title/department matches the task domain.
|
|
218
232
|
|
|
219
233
|
## SOP PROTOCOL — Adaptive Complexity Assessment
|
|
220
234
|
|
package/src/tools.ts
CHANGED
|
@@ -441,6 +441,61 @@ async function handleCreateTask(
|
|
|
441
441
|
cf.assignees = [{ id: assignee, type: "agent" }];
|
|
442
442
|
}
|
|
443
443
|
|
|
444
|
+
// ── Intelligent space_id resolution ──────────────────────────────────
|
|
445
|
+
// Priority: explicit param > agent's default_space_id > first existing space > auto-create
|
|
446
|
+
let resolvedSpaceId = (params.space_id as string) || null;
|
|
447
|
+
let resolvedFolderId = (params.folder_id as string) || null;
|
|
448
|
+
let spaceAutoCreated = false;
|
|
449
|
+
|
|
450
|
+
if (!resolvedSpaceId) {
|
|
451
|
+
try {
|
|
452
|
+
// 1. Check agent's configured default_space_id
|
|
453
|
+
if (assignee) {
|
|
454
|
+
const { data: agentRow } = await supabase
|
|
455
|
+
.from("agents")
|
|
456
|
+
.select("default_space_id")
|
|
457
|
+
.eq("id", assignee)
|
|
458
|
+
.eq("user_id", userId)
|
|
459
|
+
.single();
|
|
460
|
+
if (agentRow?.default_space_id) {
|
|
461
|
+
resolvedSpaceId = agentRow.default_space_id;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 2. Fallback: use the first existing space for this user
|
|
466
|
+
if (!resolvedSpaceId) {
|
|
467
|
+
const { data: existingSpaces } = await supabase
|
|
468
|
+
.from("pm_spaces")
|
|
469
|
+
.select("id")
|
|
470
|
+
.eq("user_id", userId)
|
|
471
|
+
.order("created_at", { ascending: true })
|
|
472
|
+
.limit(1);
|
|
473
|
+
if (existingSpaces && existingSpaces.length > 0) {
|
|
474
|
+
resolvedSpaceId = existingSpaces[0].id;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 3. Nuclear fallback: auto-create a space
|
|
479
|
+
if (!resolvedSpaceId) {
|
|
480
|
+
const newSpaceId = crypto.randomUUID();
|
|
481
|
+
const { error: spaceErr } = await supabase.from("pm_spaces").insert({
|
|
482
|
+
id: newSpaceId,
|
|
483
|
+
user_id: userId,
|
|
484
|
+
name: "Operations",
|
|
485
|
+
icon: "🏢",
|
|
486
|
+
icon_color: "#FF6D29",
|
|
487
|
+
sort_order: 0,
|
|
488
|
+
});
|
|
489
|
+
if (!spaceErr) {
|
|
490
|
+
resolvedSpaceId = newSpaceId;
|
|
491
|
+
spaceAutoCreated = true;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} catch {
|
|
495
|
+
// Non-fatal: task will still be created, just without space context
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
444
499
|
const insertData: Record<string, unknown> = {
|
|
445
500
|
id,
|
|
446
501
|
user_id: userId,
|
|
@@ -450,8 +505,8 @@ async function handleCreateTask(
|
|
|
450
505
|
assignee_type: "agent",
|
|
451
506
|
status: (params.status as string) || "PENDING",
|
|
452
507
|
priority: params.priority !== undefined ? params.priority : 1,
|
|
453
|
-
space_id:
|
|
454
|
-
folder_id:
|
|
508
|
+
space_id: resolvedSpaceId,
|
|
509
|
+
folder_id: resolvedFolderId,
|
|
455
510
|
start_date: (params.start_date as string) || null,
|
|
456
511
|
due_date: (params.due_date as string) || (params.start_date as string) || null,
|
|
457
512
|
tags: (params.tags as string[]) || [],
|
|
@@ -578,6 +633,17 @@ async function handleCreateTask(
|
|
|
578
633
|
id,
|
|
579
634
|
message: `Task "${params.title}" created and assigned to ${assignee || "no one"}${extrasStr}`,
|
|
580
635
|
task: insertData,
|
|
636
|
+
spacePlacement: resolvedSpaceId
|
|
637
|
+
? {
|
|
638
|
+
space_id: resolvedSpaceId,
|
|
639
|
+
auto_created: spaceAutoCreated,
|
|
640
|
+
note: spaceAutoCreated
|
|
641
|
+
? 'A new "Operations" space was auto-created because no PM spaces existed.'
|
|
642
|
+
: resolvedFolderId
|
|
643
|
+
? `Placed in folder ${resolvedFolderId}`
|
|
644
|
+
: "Placed in space root. To organize, check your SOPs/operating structure for folder routing, or specify folder_id.",
|
|
645
|
+
}
|
|
646
|
+
: undefined,
|
|
581
647
|
scheduledExecution: didSchedule ? `Will auto-execute on ${startDate}` : undefined,
|
|
582
648
|
recurrence: recurrenceInfo,
|
|
583
649
|
});
|
|
@@ -2156,9 +2222,13 @@ function registerNotifyOps(
|
|
|
2156
2222
|
`- "mark_read": Mark one as read. Required: id\n` +
|
|
2157
2223
|
`- "mark_all_read": Mark all as read\n` +
|
|
2158
2224
|
`- "delete": Delete a notification. Required: id\n` +
|
|
2159
|
-
`- "send_report": Send a PM progress report to YOUR connected channels (Telegram, Discord, etc.)\n` +
|
|
2225
|
+
`- "send_report": Send a numbered PM progress report to YOUR connected channels (Telegram, Discord, etc.)\n` +
|
|
2160
2226
|
` Required: scope_type (space|folder|project|task|all), agent_id (YOUR name e.g. "thalia")\n` +
|
|
2161
2227
|
` Optional: scope_id, channel_types[] (filter specific channels), include_completed (default true)\n` +
|
|
2228
|
+
` Tasks are numbered globally (1, 2, 3...) across Active/Pending/Done sections for easy reference\n` +
|
|
2229
|
+
`- "get_task_detail": Get the FULL result/content of a task by its report number. Use when user asks "details on number X"\n` +
|
|
2230
|
+
` Required: task_number (1-indexed from the report). Optional: scope_type, scope_id (defaults to all)\n` +
|
|
2231
|
+
` Returns the complete task description, execution results, status, and metadata\n` +
|
|
2162
2232
|
`- "schedule_report": Create a recurring/scheduled report\n` +
|
|
2163
2233
|
` Required: scope_type, recurrence_type (hourly|daily|weekly|monthly), agent_id (YOUR name)\n` +
|
|
2164
2234
|
` Optional: scope_id, scope_label, channel_types[], recurrence_time (HH:MM UTC, default 09:00),\n` +
|
|
@@ -2169,10 +2239,11 @@ function registerNotifyOps(
|
|
|
2169
2239
|
type: "object",
|
|
2170
2240
|
required: ["action"],
|
|
2171
2241
|
properties: {
|
|
2172
|
-
action: { type: "string", enum: ["list", "mark_read", "mark_all_read", "delete", "send_report", "schedule_report", "list_schedules", "delete_schedule"] },
|
|
2242
|
+
action: { type: "string", enum: ["list", "mark_read", "mark_all_read", "delete", "send_report", "get_task_detail", "schedule_report", "list_schedules", "delete_schedule"] },
|
|
2173
2243
|
agent_id: { type: "string", description: "Your agent name or ID (required for send_report/schedule_report)" },
|
|
2174
2244
|
id: { type: "string", description: "Notification ID" },
|
|
2175
2245
|
schedule_id: { type: "string", description: "Schedule ID (for delete_schedule)" },
|
|
2246
|
+
task_number: { type: "number", description: "1-indexed task number from the report (for get_task_detail)" },
|
|
2176
2247
|
unread_only: { type: "boolean", description: "Only show unread" },
|
|
2177
2248
|
limit: { type: "number", description: "Max results (default 50)" },
|
|
2178
2249
|
scope_type: { type: "string", enum: ["space", "folder", "project", "task", "all"], description: "Report scope" },
|
|
@@ -2257,6 +2328,69 @@ function registerNotifyOps(
|
|
|
2257
2328
|
});
|
|
2258
2329
|
}
|
|
2259
2330
|
|
|
2331
|
+
// ── GET TASK DETAIL — Drill into task result by report number ──
|
|
2332
|
+
case "get_task_detail": {
|
|
2333
|
+
const taskNumber = params.task_number as number;
|
|
2334
|
+
if (!taskNumber || taskNumber < 1) return err("Missing required: task_number (1-indexed number from the report)");
|
|
2335
|
+
const detailScopeType = (params.scope_type as string) || "all";
|
|
2336
|
+
const detailScopeId = params.scope_id as string | undefined;
|
|
2337
|
+
|
|
2338
|
+
// Re-run the same query the report uses to get consistent ordering
|
|
2339
|
+
let dq = supabase.from("tasks").select("id, title, description, status, priority, progress, completed_at, due_date, updated_at, custom_fields, tags, agent_id")
|
|
2340
|
+
.eq("user_id", userId).is("parent_task_id", null);
|
|
2341
|
+
if (detailScopeType === "space" && detailScopeId) dq = dq.eq("space_id", detailScopeId);
|
|
2342
|
+
else if (detailScopeType === "folder" && detailScopeId) dq = dq.eq("folder_id", detailScopeId);
|
|
2343
|
+
else if (detailScopeType === "project" && detailScopeId) dq = dq.eq("project_id", detailScopeId);
|
|
2344
|
+
else if (detailScopeType === "task" && detailScopeId) dq = dq.eq("id", detailScopeId);
|
|
2345
|
+
|
|
2346
|
+
const { data: detailTasks } = await dq.order("priority", { ascending: false }).limit(40);
|
|
2347
|
+
const dtAll = detailTasks || [];
|
|
2348
|
+
|
|
2349
|
+
// Build same ordered list as report: Active → Pending → Done
|
|
2350
|
+
const dtInProgress = dtAll.filter((t: any) => t.status === "IN_PROGRESS" || t.status === "RUNNING");
|
|
2351
|
+
const dtPending = dtAll.filter((t: any) => t.status === "PENDING" || t.status === "NEW");
|
|
2352
|
+
const dtCompleted = dtAll.filter((t: any) => t.status === "DONE");
|
|
2353
|
+
const dtRecentThreshold = 24 * 60 * 60 * 1000;
|
|
2354
|
+
const dtNowMs = Date.now();
|
|
2355
|
+
const dtRecentlyExecuted = dtPending.filter((t: any) =>
|
|
2356
|
+
t.description && t.description.length > 50 &&
|
|
2357
|
+
t.updated_at && (dtNowMs - new Date(t.updated_at).getTime()) < dtRecentThreshold
|
|
2358
|
+
);
|
|
2359
|
+
|
|
2360
|
+
// Same ordering as report formatter
|
|
2361
|
+
const dtOrdered: any[] = [];
|
|
2362
|
+
const dtSeen = new Set<string>();
|
|
2363
|
+
const dtAdd = (t: any) => { if (!dtSeen.has(t.id)) { dtSeen.add(t.id); dtOrdered.push(t); } };
|
|
2364
|
+
for (const t of dtInProgress.slice(0, 8)) dtAdd(t);
|
|
2365
|
+
for (const t of dtPending.slice(0, 6)) dtAdd(t);
|
|
2366
|
+
for (const t of dtRecentlyExecuted) dtAdd(t);
|
|
2367
|
+
for (const t of dtCompleted.slice(0, 5)) dtAdd(t);
|
|
2368
|
+
|
|
2369
|
+
if (taskNumber > dtOrdered.length) {
|
|
2370
|
+
return err(`Task #${taskNumber} not found. The report has ${dtOrdered.length} numbered task(s). Valid range: 1-${dtOrdered.length}.`);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
const target = dtOrdered[taskNumber - 1];
|
|
2374
|
+
const isRecurring = dtRecentlyExecuted.some((r: any) => r.id === target.id);
|
|
2375
|
+
|
|
2376
|
+
return ok({
|
|
2377
|
+
task_number: taskNumber,
|
|
2378
|
+
task_id: target.id,
|
|
2379
|
+
title: target.title,
|
|
2380
|
+
status: target.status,
|
|
2381
|
+
priority: target.priority === 3 ? "CRITICAL" : target.priority === 2 ? "HIGH" : target.priority === 1 ? "MEDIUM" : "LOW",
|
|
2382
|
+
progress: target.progress,
|
|
2383
|
+
is_recurring_today: isRecurring,
|
|
2384
|
+
description: target.description || "(no description)",
|
|
2385
|
+
custom_fields: target.custom_fields || {},
|
|
2386
|
+
tags: target.tags || [],
|
|
2387
|
+
agent_id: target.agent_id,
|
|
2388
|
+
due_date: target.due_date,
|
|
2389
|
+
completed_at: target.completed_at,
|
|
2390
|
+
updated_at: target.updated_at,
|
|
2391
|
+
});
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2260
2394
|
// ── SCHEDULE REPORT — Create recurring report ──
|
|
2261
2395
|
case "schedule_report": {
|
|
2262
2396
|
const scopeType = (params.scope_type as string) || "all";
|
|
@@ -2319,7 +2453,7 @@ function registerNotifyOps(
|
|
|
2319
2453
|
}
|
|
2320
2454
|
|
|
2321
2455
|
default:
|
|
2322
|
-
return err(`Unknown action "${action}". Valid: list, mark_read, mark_all_read, delete, send_report, schedule_report, list_schedules, delete_schedule`);
|
|
2456
|
+
return err(`Unknown action "${action}". Valid: list, mark_read, mark_all_read, delete, send_report, get_task_detail, schedule_report, list_schedules, delete_schedule`);
|
|
2323
2457
|
}
|
|
2324
2458
|
},
|
|
2325
2459
|
});
|
|
@@ -2336,7 +2470,7 @@ async function generatePMReportDirect(
|
|
|
2336
2470
|
includeCompleted: boolean,
|
|
2337
2471
|
): Promise<{ report: string; scopeLabel: string; taskCount: number }> {
|
|
2338
2472
|
let scopeLabel = "All Projects";
|
|
2339
|
-
let query = supabase.from("tasks").select("id, title, status, priority, progress, completed_at, due_date")
|
|
2473
|
+
let query = supabase.from("tasks").select("id, title, status, priority, progress, completed_at, due_date, description, updated_at")
|
|
2340
2474
|
.eq("user_id", userId).is("parent_task_id", null);
|
|
2341
2475
|
|
|
2342
2476
|
if (scopeType === "space" && scopeId) {
|
|
@@ -2367,24 +2501,53 @@ async function generatePMReportDirect(
|
|
|
2367
2501
|
.limit(1).single();
|
|
2368
2502
|
if (agentRow) agentName = agentRow.name || agentRow.codename || agentId;
|
|
2369
2503
|
|
|
2504
|
+
// Detect recently-executed recurring tasks: PENDING but updated within 24h with content
|
|
2505
|
+
const recentThreshold = 24 * 60 * 60 * 1000;
|
|
2506
|
+
const nowMs = Date.now();
|
|
2507
|
+
const isRecentlyExecuted = (t: any) =>
|
|
2508
|
+
(t.status === "PENDING" || t.status === "NEW") &&
|
|
2509
|
+
t.description && t.description.length > 50 &&
|
|
2510
|
+
t.updated_at && (nowMs - new Date(t.updated_at).getTime()) < recentThreshold;
|
|
2511
|
+
|
|
2512
|
+
const recentlyExecuted = all.filter(isRecentlyExecuted);
|
|
2370
2513
|
const completed = all.filter((t: any) => t.status === "DONE");
|
|
2371
2514
|
const inProgress = all.filter((t: any) => t.status === "IN_PROGRESS" || t.status === "RUNNING");
|
|
2372
2515
|
const pending = all.filter((t: any) => t.status === "PENDING" || t.status === "NEW");
|
|
2373
2516
|
const failed = all.filter((t: any) => t.status === "FAILED");
|
|
2374
2517
|
|
|
2518
|
+
// Build ordered task list for consistent numbering (same order as report sections)
|
|
2519
|
+
const orderedTaskIds: string[] = [];
|
|
2520
|
+
const taskNumMap = new Map<string, number>();
|
|
2521
|
+
const assignNum = (t: any) => {
|
|
2522
|
+
if (!taskNumMap.has(t.id)) {
|
|
2523
|
+
orderedTaskIds.push(t.id);
|
|
2524
|
+
taskNumMap.set(t.id, orderedTaskIds.length);
|
|
2525
|
+
}
|
|
2526
|
+
return taskNumMap.get(t.id)!;
|
|
2527
|
+
};
|
|
2528
|
+
|
|
2529
|
+
// Pre-assign numbers in display order: Active → Pending → Recently Executed → Done
|
|
2530
|
+
for (const t of inProgress.slice(0, 8)) assignNum(t);
|
|
2531
|
+
for (const t of pending.slice(0, 6)) assignNum(t);
|
|
2532
|
+
for (const t of recentlyExecuted) assignNum(t);
|
|
2533
|
+
for (const t of completed.slice(0, 5)) assignNum(t);
|
|
2534
|
+
|
|
2535
|
+
const doneCount = completed.length + recentlyExecuted.length;
|
|
2536
|
+
|
|
2375
2537
|
const lines: string[] = [];
|
|
2376
2538
|
lines.push(`📊 Project Report — ${scopeLabel}`);
|
|
2377
2539
|
lines.push(`Agent: ${agentName}`);
|
|
2378
2540
|
lines.push("");
|
|
2379
2541
|
lines.push("━━━ Summary ━━━");
|
|
2380
|
-
lines.push(`✅ ${
|
|
2542
|
+
lines.push(`✅ ${doneCount} | 🔄 ${inProgress.length} | ⏳ ${pending.length} | ❌ ${failed.length}`);
|
|
2381
2543
|
|
|
2382
2544
|
if (inProgress.length > 0) {
|
|
2383
2545
|
lines.push("");
|
|
2384
2546
|
lines.push("━━━ Active ━━━");
|
|
2385
2547
|
for (const t of inProgress.slice(0, 8)) {
|
|
2548
|
+
const n = taskNumMap.get(t.id)!;
|
|
2386
2549
|
const pl = t.priority === 3 ? "CRIT" : t.priority === 2 ? "HIGH" : t.priority === 1 ? "MED" : "LOW";
|
|
2387
|
-
lines.push(
|
|
2550
|
+
lines.push(`${n}. 🔄 [${pl}] ${t.title}${t.progress ? ` — ${t.progress}%` : ""}`);
|
|
2388
2551
|
}
|
|
2389
2552
|
if (inProgress.length > 8) lines.push(`... +${inProgress.length - 8} more`);
|
|
2390
2553
|
}
|
|
@@ -2393,26 +2556,41 @@ async function generatePMReportDirect(
|
|
|
2393
2556
|
lines.push("");
|
|
2394
2557
|
lines.push("━━━ Pending ━━━");
|
|
2395
2558
|
for (const t of pending.slice(0, 6)) {
|
|
2559
|
+
const n = taskNumMap.get(t.id)!;
|
|
2396
2560
|
const pl = t.priority === 3 ? "CRIT" : t.priority === 2 ? "HIGH" : t.priority === 1 ? "MED" : "LOW";
|
|
2397
|
-
|
|
2561
|
+
const suffix = isRecentlyExecuted(t) ? " (next cycle)" : "";
|
|
2562
|
+
lines.push(`${n}. ⏳ [${pl}] ${t.title}${suffix}`);
|
|
2398
2563
|
}
|
|
2399
2564
|
if (pending.length > 6) lines.push(`... +${pending.length - 6} more`);
|
|
2400
2565
|
}
|
|
2401
2566
|
|
|
2402
|
-
|
|
2567
|
+
// Done section: recently-executed recurring tasks + regular completed
|
|
2568
|
+
const doneItems: { task: any; isRecurring: boolean }[] = [
|
|
2569
|
+
...recentlyExecuted.map((t: any) => ({ task: t, isRecurring: true })),
|
|
2570
|
+
...(includeCompleted ? completed.filter((t: any) => t.completed_at)
|
|
2571
|
+
.sort((a: any, b: any) => new Date(b.completed_at).getTime() - new Date(a.completed_at).getTime())
|
|
2572
|
+
.slice(0, 5).map((t: any) => ({ task: t, isRecurring: false })) : []),
|
|
2573
|
+
];
|
|
2574
|
+
|
|
2575
|
+
if (doneItems.length > 0) {
|
|
2403
2576
|
lines.push("");
|
|
2404
2577
|
lines.push("━━━ Done ━━━");
|
|
2405
|
-
const
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2578
|
+
for (const { task: t, isRecurring } of doneItems) {
|
|
2579
|
+
const n = assignNum(t);
|
|
2580
|
+
if (isRecurring) {
|
|
2581
|
+
lines.push(`${n}. ✅ ${t.title} (today's run)`);
|
|
2582
|
+
} else {
|
|
2583
|
+
const diff = Date.now() - new Date(t.completed_at).getTime();
|
|
2584
|
+
const mins = Math.floor(diff / 60000);
|
|
2585
|
+
const ago = mins < 60 ? `${mins}m ago` : mins < 1440 ? `${Math.floor(mins / 60)}h ago` : `${Math.floor(mins / 1440)}d ago`;
|
|
2586
|
+
lines.push(`${n}. ✅ ${t.title} (${ago})`);
|
|
2587
|
+
}
|
|
2413
2588
|
}
|
|
2414
2589
|
}
|
|
2415
2590
|
|
|
2591
|
+
lines.push("");
|
|
2592
|
+
lines.push(`💡 Reply "details on #N" for full task result`);
|
|
2593
|
+
|
|
2416
2594
|
const now = new Date();
|
|
2417
2595
|
lines.push("");
|
|
2418
2596
|
lines.push(`Generated: ${now.toISOString().replace("T", " ").slice(0, 16)} UTC`);
|