forge-openclaw-plugin 0.2.4 → 0.2.7

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.
Files changed (117) hide show
  1. package/README.md +113 -5
  2. package/dist/assets/board-CzgvdLO8.js +6 -0
  3. package/dist/assets/board-CzgvdLO8.js.map +1 -0
  4. package/dist/assets/favicon-BCHm9dUV.ico +0 -0
  5. package/dist/assets/index-8d_oM8fL.js +27 -0
  6. package/dist/assets/index-8d_oM8fL.js.map +1 -0
  7. package/dist/assets/index-D4A_bq8m.css +1 -0
  8. package/dist/assets/motion-STUd1O46.js +10 -0
  9. package/dist/assets/motion-STUd1O46.js.map +1 -0
  10. package/dist/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
  11. package/dist/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
  12. package/dist/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
  13. package/dist/assets/sora-latin-ext-wght-normal-CawQDOvP.woff2 +0 -0
  14. package/dist/assets/sora-latin-wght-normal-DdqRvwsR.woff2 +0 -0
  15. package/dist/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
  16. package/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
  17. package/dist/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
  18. package/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
  19. package/dist/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
  20. package/dist/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
  21. package/dist/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
  22. package/dist/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
  23. package/dist/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
  24. package/dist/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
  25. package/dist/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
  26. package/dist/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
  27. package/dist/assets/table-CtNlETLc.js +23 -0
  28. package/dist/assets/table-CtNlETLc.js.map +1 -0
  29. package/dist/assets/ui-ThzkR_oW.js +46 -0
  30. package/dist/assets/ui-ThzkR_oW.js.map +1 -0
  31. package/dist/assets/vendor-CRS-psbw.css +1 -0
  32. package/dist/assets/vendor-DyHAI6nk.js +423 -0
  33. package/dist/assets/vendor-DyHAI6nk.js.map +1 -0
  34. package/dist/assets/viz-BJuBCz_G.js +34 -0
  35. package/dist/assets/viz-BJuBCz_G.js.map +1 -0
  36. package/dist/favicon.ico +0 -0
  37. package/dist/favicon.png +0 -0
  38. package/dist/index.html +29 -0
  39. package/dist/openclaw/api-client.d.ts +8 -0
  40. package/dist/openclaw/api-client.js +31 -4
  41. package/dist/openclaw/local-runtime.d.ts +3 -0
  42. package/dist/openclaw/local-runtime.js +135 -0
  43. package/dist/openclaw/parity.d.ts +4 -4
  44. package/dist/openclaw/parity.js +23 -33
  45. package/dist/openclaw/plugin-entry-shared.d.ts +4 -2
  46. package/dist/openclaw/plugin-entry-shared.js +51 -9
  47. package/dist/openclaw/routes.d.ts +12 -3
  48. package/dist/openclaw/routes.js +156 -924
  49. package/dist/openclaw/tools.js +242 -1100
  50. package/dist/server/app.js +2450 -0
  51. package/dist/server/db.js +313 -0
  52. package/dist/server/e2e-server.js +20 -0
  53. package/dist/server/errors.js +15 -0
  54. package/dist/server/index.js +16 -0
  55. package/dist/server/managers/base.js +17 -0
  56. package/dist/server/managers/contracts.js +47 -0
  57. package/dist/server/managers/platform/api-gateway-manager.js +11 -0
  58. package/dist/server/managers/platform/audit-manager.js +15 -0
  59. package/dist/server/managers/platform/authentication-manager.js +56 -0
  60. package/dist/server/managers/platform/authorization-manager.js +56 -0
  61. package/dist/server/managers/platform/background-job-manager.js +10 -0
  62. package/dist/server/managers/platform/configuration-manager.js +33 -0
  63. package/dist/server/managers/platform/database-manager.js +14 -0
  64. package/dist/server/managers/platform/event-bus-manager.js +7 -0
  65. package/dist/server/managers/platform/external-service-manager.js +11 -0
  66. package/dist/server/managers/platform/health-manager.js +7 -0
  67. package/dist/server/managers/platform/migration-manager.js +8 -0
  68. package/dist/server/managers/platform/search-index-manager.js +4 -0
  69. package/dist/server/managers/platform/secrets-manager.js +19 -0
  70. package/dist/server/managers/platform/session-manager.js +121 -0
  71. package/dist/server/managers/platform/storage-manager.js +16 -0
  72. package/dist/server/managers/platform/token-manager.js +37 -0
  73. package/dist/server/managers/platform/transaction-manager.js +8 -0
  74. package/dist/server/managers/platform/trusted-network.js +39 -0
  75. package/dist/server/managers/runtime.js +56 -0
  76. package/dist/server/managers/type-guards.js +4 -0
  77. package/dist/server/openapi.js +3512 -0
  78. package/dist/server/psyche-types.js +395 -0
  79. package/dist/server/repositories/activity-events.js +157 -0
  80. package/dist/server/repositories/collaboration.js +497 -0
  81. package/dist/server/repositories/comments.js +176 -0
  82. package/dist/server/repositories/deleted-entities.js +192 -0
  83. package/dist/server/repositories/domains.js +30 -0
  84. package/dist/server/repositories/event-log.js +64 -0
  85. package/dist/server/repositories/goals.js +159 -0
  86. package/dist/server/repositories/projects.js +214 -0
  87. package/dist/server/repositories/psyche.js +1356 -0
  88. package/dist/server/repositories/rewards.js +675 -0
  89. package/dist/server/repositories/settings.js +399 -0
  90. package/dist/server/repositories/tags.js +160 -0
  91. package/dist/server/repositories/task-runs.js +488 -0
  92. package/dist/server/repositories/tasks.js +413 -0
  93. package/dist/server/services/context.js +214 -0
  94. package/dist/server/services/dashboard.js +170 -0
  95. package/dist/server/services/entity-crud.js +576 -0
  96. package/dist/server/services/gamification.js +215 -0
  97. package/dist/server/services/insights.js +91 -0
  98. package/dist/server/services/projects.js +75 -0
  99. package/dist/server/services/psyche.js +63 -0
  100. package/dist/server/services/relations.js +28 -0
  101. package/dist/server/services/reviews.js +88 -0
  102. package/dist/server/services/run-recovery.js +13 -0
  103. package/dist/server/services/tagging.js +49 -0
  104. package/dist/server/services/task-run-watchdog.js +92 -0
  105. package/dist/server/services/work-time.js +176 -0
  106. package/dist/server/types.js +999 -0
  107. package/dist/server/web.js +91 -0
  108. package/openclaw.plugin.json +21 -9
  109. package/package.json +17 -4
  110. package/server/migrations/001_core.sql +333 -0
  111. package/server/migrations/002_psyche.sql +241 -0
  112. package/server/migrations/003_timer_execution.sql +18 -0
  113. package/server/migrations/004_psyche_linked_entities.sql +5 -0
  114. package/server/migrations/005_adaptive_schemas.sql +157 -0
  115. package/server/migrations/006_psyche_auth_setting.sql +4 -0
  116. package/server/migrations/007_deleted_entities.sql +16 -0
  117. package/skills/forge-openclaw/SKILL.md +189 -275
@@ -0,0 +1,413 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { getDatabase } from "../db.js";
3
+ import { runInTransaction } from "../db.js";
4
+ import { HttpError } from "../errors.js";
5
+ import { recordActivityEvent } from "./activity-events.js";
6
+ import { filterDeletedEntities, filterDeletedIds, isEntityDeleted } from "./deleted-entities.js";
7
+ import { ensureDefaultProjectForGoal, getProjectById } from "./projects.js";
8
+ import { pruneLinkedEntityReferences } from "./psyche.js";
9
+ import { awardTaskCompletionReward, reverseLatestTaskCompletionReward } from "./rewards.js";
10
+ import { assertTaskRelations } from "../services/relations.js";
11
+ import { computeWorkTime, emptyTaskTimeSummary } from "../services/work-time.js";
12
+ import { taskSchema } from "../types.js";
13
+ function readTaskTagIds(taskId) {
14
+ const rows = getDatabase()
15
+ .prepare(`SELECT tag_id FROM task_tags WHERE task_id = ? ORDER BY tag_id`)
16
+ .all(taskId);
17
+ return filterDeletedIds("tag", rows.map((row) => row.tag_id));
18
+ }
19
+ function mapTask(row, time = emptyTaskTimeSummary()) {
20
+ return taskSchema.parse({
21
+ id: row.id,
22
+ title: row.title,
23
+ description: row.description,
24
+ status: row.status,
25
+ priority: row.priority,
26
+ owner: row.owner,
27
+ goalId: row.goal_id,
28
+ projectId: row.project_id,
29
+ dueDate: row.due_date,
30
+ effort: row.effort,
31
+ energy: row.energy,
32
+ points: row.points,
33
+ sortOrder: row.sort_order,
34
+ completedAt: row.completed_at,
35
+ createdAt: row.created_at,
36
+ updatedAt: row.updated_at,
37
+ tagIds: readTaskTagIds(row.id),
38
+ time
39
+ });
40
+ }
41
+ function replaceTaskTags(taskId, tagIds) {
42
+ const database = getDatabase();
43
+ database.prepare(`DELETE FROM task_tags WHERE task_id = ?`).run(taskId);
44
+ const insert = database.prepare(`INSERT INTO task_tags (task_id, tag_id) VALUES (?, ?)`);
45
+ for (const tagId of tagIds) {
46
+ insert.run(taskId, tagId);
47
+ }
48
+ }
49
+ function nextSortOrder(status) {
50
+ const row = getDatabase()
51
+ .prepare(`SELECT COALESCE(MAX(sort_order), -1) AS max_sort FROM tasks WHERE status = ?`)
52
+ .get(status);
53
+ return row.max_sort + 1;
54
+ }
55
+ function normalizeCompletedAt(status, existingCompletedAt) {
56
+ if (status === "done") {
57
+ return existingCompletedAt ?? new Date().toISOString();
58
+ }
59
+ return null;
60
+ }
61
+ function resolveProjectAndGoalIds(input, current) {
62
+ const requestedGoalId = input.goalId === undefined ? current?.goalId ?? null : input.goalId;
63
+ const goalChangedWithoutProjectOverride = current !== undefined && input.goalId !== undefined && input.goalId !== current.goalId && input.projectId === undefined;
64
+ const requestedProjectId = input.projectId === undefined ? (goalChangedWithoutProjectOverride ? null : current?.projectId ?? null) : input.projectId;
65
+ if (requestedProjectId) {
66
+ const project = getProjectById(requestedProjectId);
67
+ if (!project) {
68
+ throw new HttpError(404, "project_not_found", `Project ${requestedProjectId} does not exist`);
69
+ }
70
+ if (requestedGoalId && project.goalId !== requestedGoalId) {
71
+ throw new HttpError(409, "project_goal_mismatch", `Project ${requestedProjectId} does not belong to goal ${requestedGoalId}`);
72
+ }
73
+ return {
74
+ goalId: project.goalId,
75
+ projectId: project.id
76
+ };
77
+ }
78
+ if (requestedGoalId) {
79
+ const defaultProject = ensureDefaultProjectForGoal(requestedGoalId);
80
+ return {
81
+ goalId: requestedGoalId,
82
+ projectId: defaultProject.id
83
+ };
84
+ }
85
+ return {
86
+ goalId: null,
87
+ projectId: null
88
+ };
89
+ }
90
+ function inferReopenStatus(taskId) {
91
+ const row = getDatabase()
92
+ .prepare(`SELECT metadata_json
93
+ FROM activity_events
94
+ WHERE entity_type = 'task'
95
+ AND entity_id = ?
96
+ AND event_type = 'task_completed'
97
+ AND NOT EXISTS (
98
+ SELECT 1
99
+ FROM activity_event_corrections
100
+ WHERE activity_event_corrections.corrected_event_id = activity_events.id
101
+ )
102
+ ORDER BY created_at DESC
103
+ LIMIT 1`)
104
+ .get(taskId);
105
+ if (!row) {
106
+ return "focus";
107
+ }
108
+ const metadata = JSON.parse(row.metadata_json);
109
+ return metadata.previousStatus && metadata.previousStatus !== "done" ? metadata.previousStatus : "focus";
110
+ }
111
+ function updateTaskRecord(current, input, activity) {
112
+ const relationState = resolveProjectAndGoalIds(input, current);
113
+ const nextGoalId = relationState.goalId;
114
+ const nextProjectId = relationState.projectId;
115
+ const nextTagIds = input.tagIds ?? current.tagIds;
116
+ assertTaskRelations({ goalId: nextGoalId, tagIds: nextTagIds });
117
+ const nextStatus = input.status ?? current.status;
118
+ const movedColumns = nextStatus !== current.status;
119
+ const nextSort = input.sortOrder ?? (movedColumns ? nextSortOrder(nextStatus) : current.sortOrder);
120
+ const completedAt = normalizeCompletedAt(nextStatus, current.completedAt);
121
+ const updatedAt = new Date().toISOString();
122
+ getDatabase()
123
+ .prepare(`UPDATE tasks
124
+ SET title = ?, description = ?, status = ?, priority = ?, owner = ?, goal_id = ?, due_date = ?, effort = ?,
125
+ energy = ?, points = ?, sort_order = ?, completed_at = ?, updated_at = ?, project_id = ?
126
+ WHERE id = ?`)
127
+ .run(input.title ?? current.title, input.description ?? current.description, nextStatus, input.priority ?? current.priority, input.owner ?? current.owner, nextGoalId, input.dueDate === undefined ? current.dueDate : input.dueDate, input.effort ?? current.effort, input.energy ?? current.energy, input.points ?? current.points, nextSort, completedAt, updatedAt, nextProjectId, current.id);
128
+ replaceTaskTags(current.id, nextTagIds);
129
+ const updated = getTaskById(current.id);
130
+ if (updated && activity) {
131
+ const statusChanged = current.status !== updated.status;
132
+ const ownerChanged = current.owner !== updated.owner;
133
+ const goalChanged = current.goalId !== updated.goalId;
134
+ const projectChanged = current.projectId !== updated.projectId;
135
+ const pointsChanged = current.points !== updated.points;
136
+ const eventType = statusChanged && updated.status === "done"
137
+ ? "task_completed"
138
+ : statusChanged && current.status === "done"
139
+ ? "task_uncompleted"
140
+ : statusChanged
141
+ ? "task_status_changed"
142
+ : "task_updated";
143
+ const title = eventType === "task_completed"
144
+ ? `Task completed: ${updated.title}`
145
+ : eventType === "task_uncompleted"
146
+ ? `Task reopened: ${updated.title}`
147
+ : eventType === "task_status_changed"
148
+ ? `Task moved to ${updated.status.replaceAll("_", " ")}: ${updated.title}`
149
+ : `Task updated: ${updated.title}`;
150
+ recordActivityEvent({
151
+ entityType: "task",
152
+ entityId: updated.id,
153
+ eventType,
154
+ title,
155
+ description: goalChanged
156
+ ? `Goal link updated${updated.goalId ? ` to ${updated.goalId}` : ""}.`
157
+ : projectChanged
158
+ ? `Project link updated${updated.projectId ? ` to ${updated.projectId}` : ""}.`
159
+ : ownerChanged
160
+ ? `Ownership changed to ${updated.owner}.`
161
+ : statusChanged
162
+ ? `Status changed from ${current.status} to ${updated.status}.`
163
+ : "Task details were edited.",
164
+ actor: activity.actor ?? null,
165
+ source: activity.source,
166
+ metadata: {
167
+ previousStatus: current.status,
168
+ status: updated.status,
169
+ owner: updated.owner,
170
+ previousOwner: current.owner,
171
+ goalId: updated.goalId,
172
+ previousGoalId: current.goalId,
173
+ projectId: updated.projectId,
174
+ previousProjectId: current.projectId,
175
+ points: updated.points,
176
+ previousPoints: current.points,
177
+ pointsChanged
178
+ }
179
+ });
180
+ if (current.status !== "done" && updated.status === "done") {
181
+ awardTaskCompletionReward(updated, activity);
182
+ }
183
+ else if (current.status === "done" && updated.status !== "done") {
184
+ reverseLatestTaskCompletionReward(updated, activity);
185
+ }
186
+ }
187
+ return updated;
188
+ }
189
+ function fingerprintTaskCreate(input) {
190
+ return createHash("sha256")
191
+ .update(JSON.stringify({
192
+ title: input.title,
193
+ description: input.description,
194
+ status: input.status,
195
+ priority: input.priority,
196
+ owner: input.owner,
197
+ goalId: input.goalId,
198
+ projectId: input.projectId,
199
+ dueDate: input.dueDate,
200
+ effort: input.effort,
201
+ energy: input.energy,
202
+ points: input.points,
203
+ sortOrder: input.sortOrder ?? null,
204
+ tagIds: input.tagIds
205
+ }))
206
+ .digest("hex");
207
+ }
208
+ function insertTaskRecord(input, activity) {
209
+ const relationState = resolveProjectAndGoalIds(input);
210
+ assertTaskRelations({ goalId: relationState.goalId, tagIds: input.tagIds });
211
+ if (!relationState.projectId) {
212
+ throw new HttpError(400, "project_required", "Tasks must belong to a project");
213
+ }
214
+ const now = new Date().toISOString();
215
+ const id = `task_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
216
+ const sortOrder = input.sortOrder ?? nextSortOrder(input.status);
217
+ const completedAt = normalizeCompletedAt(input.status, null);
218
+ getDatabase()
219
+ .prepare(`INSERT INTO tasks (
220
+ id, title, description, status, priority, owner, goal_id, project_id, due_date, effort, energy, points, sort_order,
221
+ completed_at, created_at, updated_at
222
+ )
223
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
224
+ .run(id, input.title, input.description, input.status, input.priority, input.owner, relationState.goalId, relationState.projectId, input.dueDate, input.effort, input.energy, input.points, sortOrder, completedAt, now, now);
225
+ replaceTaskTags(id, input.tagIds);
226
+ const task = getTaskById(id);
227
+ if (activity) {
228
+ recordActivityEvent({
229
+ entityType: "task",
230
+ entityId: task.id,
231
+ eventType: "task_created",
232
+ title: `Task created: ${task.title}`,
233
+ description: task.goalId ? `Linked to ${task.goalId} and assigned to ${task.owner}.` : `Assigned to ${task.owner}.`,
234
+ actor: activity.actor ?? null,
235
+ source: activity.source,
236
+ metadata: {
237
+ status: task.status,
238
+ owner: task.owner,
239
+ goalId: task.goalId,
240
+ projectId: task.projectId,
241
+ points: task.points
242
+ }
243
+ });
244
+ if (task.status === "done") {
245
+ awardTaskCompletionReward(task, activity);
246
+ }
247
+ }
248
+ return task;
249
+ }
250
+ export function listTasks(filters = {}) {
251
+ const whereClauses = [];
252
+ const params = [];
253
+ const todayIso = new Date().toISOString().slice(0, 10);
254
+ const weekIso = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
255
+ if (filters.status) {
256
+ whereClauses.push("status = ?");
257
+ params.push(filters.status);
258
+ }
259
+ if (filters.owner) {
260
+ whereClauses.push("owner = ?");
261
+ params.push(filters.owner);
262
+ }
263
+ if (filters.goalId) {
264
+ whereClauses.push("goal_id = ?");
265
+ params.push(filters.goalId);
266
+ }
267
+ if (filters.projectId) {
268
+ whereClauses.push("project_id = ?");
269
+ params.push(filters.projectId);
270
+ }
271
+ if (filters.tagId) {
272
+ whereClauses.push("EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = tasks.id AND task_tags.tag_id = ?)");
273
+ params.push(filters.tagId);
274
+ }
275
+ if (filters.due === "overdue") {
276
+ whereClauses.push("status != 'done' AND due_date IS NOT NULL AND due_date < ?");
277
+ params.push(todayIso);
278
+ }
279
+ if (filters.due === "today") {
280
+ whereClauses.push("status != 'done' AND due_date = ?");
281
+ params.push(todayIso);
282
+ }
283
+ if (filters.due === "week") {
284
+ whereClauses.push("status != 'done' AND due_date IS NOT NULL AND due_date >= ? AND due_date <= ?");
285
+ params.push(todayIso, weekIso);
286
+ }
287
+ const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
288
+ const limitSql = filters.limit ? "LIMIT ?" : "";
289
+ if (filters.limit) {
290
+ params.push(filters.limit);
291
+ }
292
+ const rows = getDatabase()
293
+ .prepare(`SELECT id, title, description, status, priority, owner, goal_id, project_id, due_date, effort, energy, points, sort_order,
294
+ completed_at, created_at, updated_at
295
+ FROM tasks
296
+ ${whereSql}
297
+ ORDER BY
298
+ CASE status
299
+ WHEN 'backlog' THEN 0
300
+ WHEN 'focus' THEN 1
301
+ WHEN 'in_progress' THEN 2
302
+ WHEN 'blocked' THEN 3
303
+ ELSE 4
304
+ END,
305
+ sort_order,
306
+ created_at
307
+ ${limitSql}`)
308
+ .all(...params);
309
+ const workTime = computeWorkTime();
310
+ return filterDeletedEntities("task", rows.map((row) => mapTask(row, workTime.taskSummaries.get(row.id) ?? emptyTaskTimeSummary())));
311
+ }
312
+ export function getTaskById(taskId) {
313
+ if (isEntityDeleted("task", taskId)) {
314
+ return undefined;
315
+ }
316
+ const row = getDatabase()
317
+ .prepare(`SELECT id, title, description, status, priority, owner, goal_id, project_id, due_date, effort, energy, points, sort_order,
318
+ completed_at, created_at, updated_at
319
+ FROM tasks
320
+ WHERE id = ?`)
321
+ .get(taskId);
322
+ const workTime = computeWorkTime();
323
+ return row ? mapTask(row, workTime.taskSummaries.get(row.id) ?? emptyTaskTimeSummary()) : undefined;
324
+ }
325
+ export function createTask(input, activity) {
326
+ return runInTransaction(() => insertTaskRecord(input, activity));
327
+ }
328
+ export function createTaskWithIdempotency(input, idempotencyKey, activity) {
329
+ return runInTransaction(() => {
330
+ const fingerprint = fingerprintTaskCreate(input);
331
+ const existing = getDatabase()
332
+ .prepare(`SELECT task_id, request_fingerprint
333
+ FROM task_create_idempotency
334
+ WHERE idempotency_key = ?`)
335
+ .get(idempotencyKey);
336
+ if (existing) {
337
+ if (existing.request_fingerprint !== fingerprint) {
338
+ throw new HttpError(409, "idempotency_conflict", "Idempotency key was already used for a different task creation payload");
339
+ }
340
+ const task = getTaskById(existing.task_id);
341
+ if (!task) {
342
+ throw new HttpError(500, "idempotency_corruption", `Stored task ${existing.task_id} for idempotency key is missing`);
343
+ }
344
+ return { task, replayed: true };
345
+ }
346
+ const task = insertTaskRecord(input, activity);
347
+ getDatabase()
348
+ .prepare(`INSERT INTO task_create_idempotency (idempotency_key, request_fingerprint, task_id, created_at)
349
+ VALUES (?, ?, ?, ?)`)
350
+ .run(idempotencyKey, fingerprint, task.id, new Date().toISOString());
351
+ return { task, replayed: false };
352
+ });
353
+ }
354
+ export function updateTask(taskId, input, activity) {
355
+ const current = getTaskById(taskId);
356
+ if (!current) {
357
+ return undefined;
358
+ }
359
+ return runInTransaction(() => updateTaskRecord(current, input, activity));
360
+ }
361
+ export function updateTaskInTransaction(taskId, input, activity) {
362
+ const current = getTaskById(taskId);
363
+ if (!current) {
364
+ return undefined;
365
+ }
366
+ return updateTaskRecord(current, input, activity);
367
+ }
368
+ export function uncompleteTask(taskId, activity) {
369
+ const current = getTaskById(taskId);
370
+ if (!current) {
371
+ return undefined;
372
+ }
373
+ if (current.status !== "done") {
374
+ return current;
375
+ }
376
+ return runInTransaction(() => updateTaskRecord(current, { status: inferReopenStatus(taskId) }, activity));
377
+ }
378
+ export function deleteTask(taskId, activity) {
379
+ const current = getTaskById(taskId);
380
+ if (!current) {
381
+ return undefined;
382
+ }
383
+ return runInTransaction(() => {
384
+ pruneLinkedEntityReferences("task", taskId);
385
+ getDatabase()
386
+ .prepare(`DELETE FROM entity_comments
387
+ WHERE entity_type = 'task'
388
+ AND entity_id = ?`)
389
+ .run(taskId);
390
+ getDatabase()
391
+ .prepare(`DELETE FROM tasks WHERE id = ?`)
392
+ .run(taskId);
393
+ if (activity) {
394
+ recordActivityEvent({
395
+ entityType: "task",
396
+ entityId: current.id,
397
+ eventType: "task_deleted",
398
+ title: `Task deleted: ${current.title}`,
399
+ description: "Task removed from the system.",
400
+ actor: activity.actor ?? null,
401
+ source: activity.source,
402
+ metadata: {
403
+ status: current.status,
404
+ owner: current.owner,
405
+ goalId: current.goalId,
406
+ projectId: current.projectId,
407
+ points: current.points
408
+ }
409
+ });
410
+ }
411
+ return current;
412
+ });
413
+ }
@@ -0,0 +1,214 @@
1
+ import { listActivityEvents } from "../repositories/activity-events.js";
2
+ import { listGoals } from "../repositories/goals.js";
3
+ import { listTags, listTagsByIds } from "../repositories/tags.js";
4
+ import { listTasks } from "../repositories/tasks.js";
5
+ import { getDashboard } from "./dashboard.js";
6
+ import { buildAchievementSignals, buildGamificationProfile, buildMilestoneRewards } from "./gamification.js";
7
+ import { overviewContextSchema, riskContextSchema, todayContextSchema } from "../types.js";
8
+ function priorityWeight(task) {
9
+ switch (task.priority) {
10
+ case "critical":
11
+ return 4;
12
+ case "high":
13
+ return 3;
14
+ case "medium":
15
+ return 2;
16
+ default:
17
+ return 1;
18
+ }
19
+ }
20
+ function dueWeight(task) {
21
+ return task.dueDate ? Date.parse(`${task.dueDate}T00:00:00.000Z`) : Number.POSITIVE_INFINITY;
22
+ }
23
+ function taskSignalRank(task) {
24
+ const statusBoost = task.status === "in_progress" ? 30 : task.status === "focus" ? 20 : 0;
25
+ const dueBoost = task.dueDate ? Math.max(0, 20 - Math.floor((dueWeight(task) - Date.now()) / 86_400_000)) : 0;
26
+ return priorityWeight(task) * 20 + statusBoost + dueBoost + task.points;
27
+ }
28
+ function sortStrategicTasks(tasks) {
29
+ return [...tasks].sort((left, right) => {
30
+ const signalDelta = taskSignalRank(right) - taskSignalRank(left);
31
+ if (signalDelta !== 0) {
32
+ return signalDelta;
33
+ }
34
+ return dueWeight(left) - dueWeight(right);
35
+ });
36
+ }
37
+ function latestGoalActivity(goal, tasks) {
38
+ const goalTasks = tasks.filter((task) => task.goalId === goal.id);
39
+ const timestamps = goalTasks
40
+ .flatMap((task) => [task.updatedAt, task.completedAt].filter((value) => value !== null))
41
+ .sort((left, right) => Date.parse(right) - Date.parse(left));
42
+ return timestamps[0] ?? null;
43
+ }
44
+ function buildNeglectedGoals(goals, tasks, now) {
45
+ return goals
46
+ .filter((goal) => goal.status === "active")
47
+ .map((goal) => {
48
+ const relatedTasks = tasks.filter((task) => task.goalId === goal.id);
49
+ const completedCount = relatedTasks.filter((task) => task.status === "done").length;
50
+ const activeCount = relatedTasks.filter((task) => task.status !== "done").length;
51
+ const latestActivity = latestGoalActivity(goal, tasks);
52
+ const ageDays = latestActivity ? Math.floor((now.getTime() - Date.parse(latestActivity)) / 86_400_000) : 999;
53
+ const risk = activeCount === 0 || ageDays >= 7 ? "high" : ageDays >= 4 || completedCount === 0 ? "medium" : "low";
54
+ const summary = activeCount === 0
55
+ ? "No active projects are attached right now."
56
+ : ageDays >= 7
57
+ ? `No meaningful movement in ${ageDays} days.`
58
+ : ageDays >= 4
59
+ ? `Momentum is cooling after ${ageDays} quiet days.`
60
+ : "Still receiving enough activity to stay alive.";
61
+ return {
62
+ goalId: goal.id,
63
+ title: goal.title,
64
+ summary,
65
+ risk
66
+ };
67
+ })
68
+ .sort((left, right) => {
69
+ const riskWeight = { high: 3, medium: 2, low: 1 };
70
+ return riskWeight[right.risk] - riskWeight[left.risk];
71
+ })
72
+ .slice(0, 4);
73
+ }
74
+ function chooseDomainTag(goal, tagsById) {
75
+ const tags = listTagsByIds(goal.tagIds);
76
+ return tags.find((tag) => tag.kind === "value") ?? tags.find((tag) => tag.kind === "category") ?? tagsById.get(goal.tagIds[0] ?? "") ?? null;
77
+ }
78
+ function buildDomainBalance(goals, tasks) {
79
+ const allTags = listTags();
80
+ const tagsById = new Map(allTags.map((tag) => [tag.id, tag]));
81
+ const domainRows = new Map();
82
+ for (const goal of goals) {
83
+ const domainTag = chooseDomainTag(goal, tagsById);
84
+ if (!domainTag) {
85
+ continue;
86
+ }
87
+ const relatedTasks = tasks.filter((task) => task.goalId === goal.id);
88
+ const activeTaskCount = relatedTasks.filter((task) => task.status !== "done").length;
89
+ const completedPoints = relatedTasks
90
+ .filter((task) => task.status === "done")
91
+ .reduce((sum, task) => sum + task.points, 0);
92
+ const existing = domainRows.get(domainTag.id);
93
+ const nextGoalCount = (existing?.goalCount ?? 0) + 1;
94
+ const nextActiveCount = (existing?.activeTaskCount ?? 0) + activeTaskCount;
95
+ const nextCompletedPoints = (existing?.completedPoints ?? 0) + completedPoints;
96
+ domainRows.set(domainTag.id, {
97
+ tagId: domainTag.id,
98
+ label: domainTag.name,
99
+ color: domainTag.color,
100
+ goalCount: nextGoalCount,
101
+ activeTaskCount: nextActiveCount,
102
+ completedPoints: nextCompletedPoints,
103
+ momentumLabel: nextCompletedPoints >= 120 ? "Hot" : nextActiveCount >= 3 ? "Loaded" : nextCompletedPoints > 0 ? "Alive" : "Cold"
104
+ });
105
+ }
106
+ return [...domainRows.values()].sort((left, right) => right.completedPoints - left.completedPoints);
107
+ }
108
+ export function getOverviewContext(now = new Date()) {
109
+ const dashboard = getDashboard();
110
+ const focusTasks = dashboard.tasks.filter((task) => task.status === "focus" || task.status === "in_progress").length;
111
+ const overdueTasks = dashboard.tasks.filter((task) => task.status !== "done" && task.dueDate !== null && task.dueDate < now.toISOString().slice(0, 10)).length;
112
+ return overviewContextSchema.parse({
113
+ generatedAt: now.toISOString(),
114
+ strategicHeader: {
115
+ streakDays: dashboard.gamification.streakDays,
116
+ level: dashboard.gamification.level,
117
+ totalXp: dashboard.gamification.totalXp,
118
+ currentLevelXp: dashboard.gamification.currentLevelXp,
119
+ nextLevelXp: dashboard.gamification.nextLevelXp,
120
+ momentumScore: dashboard.gamification.momentumScore,
121
+ focusTasks,
122
+ overdueTasks
123
+ },
124
+ projects: dashboard.projects.slice(0, 5),
125
+ activeGoals: dashboard.goals.filter((goal) => goal.status === "active").slice(0, 6),
126
+ topTasks: sortStrategicTasks(dashboard.tasks.filter((task) => task.status !== "done")).slice(0, 6),
127
+ recentEvidence: listActivityEvents({ limit: 12 }),
128
+ achievements: buildAchievementSignals(listGoals(), listTasks(), now),
129
+ domainBalance: buildDomainBalance(listGoals(), listTasks()),
130
+ neglectedGoals: buildNeglectedGoals(listGoals(), listTasks(), now)
131
+ });
132
+ }
133
+ export function getTodayContext(now = new Date()) {
134
+ const goals = listGoals();
135
+ const tasks = listTasks();
136
+ const gamification = buildGamificationProfile(goals, tasks, now);
137
+ const inProgressTasks = sortStrategicTasks(tasks.filter((task) => task.status === "in_progress")).slice(0, 4);
138
+ const readyTasks = sortStrategicTasks(tasks.filter((task) => task.status === "focus" || task.status === "backlog")).slice(0, 4);
139
+ const deferredTasks = sortStrategicTasks(tasks.filter((task) => task.status === "blocked")).slice(0, 4);
140
+ const completedTasks = [...tasks]
141
+ .filter((task) => task.status === "done" && task.completedAt !== null)
142
+ .sort((left, right) => Date.parse(right.completedAt ?? "") - Date.parse(left.completedAt ?? ""))
143
+ .slice(0, 4);
144
+ const directiveTask = inProgressTasks[0] ?? readyTasks[0] ?? null;
145
+ const goalTitle = directiveTask?.goalId ? goals.find((goal) => goal.id === directiveTask.goalId)?.title ?? null : null;
146
+ const overdueCount = tasks.filter((task) => task.status !== "done" && task.dueDate !== null && task.dueDate < now.toISOString().slice(0, 10)).length;
147
+ const completedToday = completedTasks.filter((task) => task.completedAt?.slice(0, 10) === now.toISOString().slice(0, 10)).length;
148
+ return todayContextSchema.parse({
149
+ generatedAt: now.toISOString(),
150
+ directive: {
151
+ task: directiveTask,
152
+ goalTitle,
153
+ rewardXp: directiveTask?.points ?? 0,
154
+ sessionLabel: directiveTask ? `${directiveTask.effort} effort · ${directiveTask.energy} energy` : "No active directive selected"
155
+ },
156
+ timeline: [
157
+ { id: "in_progress", label: "In progress", tasks: inProgressTasks },
158
+ { id: "ready", label: "Ready to start", tasks: readyTasks },
159
+ { id: "blocked", label: "Blocked", tasks: deferredTasks },
160
+ { id: "done", label: "Done", tasks: completedTasks }
161
+ ],
162
+ dailyQuests: [
163
+ {
164
+ id: "quest-major-3",
165
+ title: "Complete 3 meaningful tasks",
166
+ summary: "Push enough mass today that the day feels consequential.",
167
+ rewardXp: 90,
168
+ progressLabel: `${completedToday}/3 complete`,
169
+ completed: completedToday >= 3
170
+ },
171
+ {
172
+ id: "quest-focus-lane",
173
+ title: "Keep one focus lane alive",
174
+ summary: "Protect at least one in-progress or focus task from stalling.",
175
+ rewardXp: 60,
176
+ progressLabel: `${inProgressTasks.length > 0 || readyTasks.length > 0 ? 1 : 0}/1 active tasks`,
177
+ completed: inProgressTasks.length > 0 || readyTasks.length > 0
178
+ },
179
+ {
180
+ id: "quest-recovery",
181
+ title: "Run one recovery action",
182
+ summary: "Touch a neglected or blocked arc before it drifts further.",
183
+ rewardXp: 75,
184
+ progressLabel: `${deferredTasks.length > 0 ? 1 : 0}/1 rescue opportunities found`,
185
+ completed: false
186
+ }
187
+ ],
188
+ milestoneRewards: buildMilestoneRewards(goals, tasks, now),
189
+ momentum: {
190
+ streakDays: gamification.streakDays,
191
+ momentumScore: gamification.momentumScore,
192
+ recoveryHint: overdueCount > 0
193
+ ? `Clear ${overdueCount} overdue task${overdueCount === 1 ? "" : "s"} to keep momentum from decaying.`
194
+ : "No overdue drag right now. Preserve the rhythm with one decisive completion."
195
+ }
196
+ });
197
+ }
198
+ export function getRiskContext(now = new Date()) {
199
+ const tasks = listTasks();
200
+ const goals = listGoals();
201
+ const overdueTasks = sortStrategicTasks(tasks.filter((task) => task.status !== "done" && task.dueDate !== null && task.dueDate < now.toISOString().slice(0, 10))).slice(0, 8);
202
+ const blockedTasks = sortStrategicTasks(tasks.filter((task) => task.status === "blocked")).slice(0, 8);
203
+ const neglectedGoals = buildNeglectedGoals(goals, tasks, now);
204
+ const summary = overdueTasks.length === 0 && blockedTasks.length === 0
205
+ ? "No acute risk signals are spiking. The main job is maintaining momentum."
206
+ : `${overdueTasks.length} overdue and ${blockedTasks.length} blocked tasks are the main drag vectors right now.`;
207
+ return riskContextSchema.parse({
208
+ generatedAt: now.toISOString(),
209
+ overdueTasks,
210
+ blockedTasks,
211
+ neglectedGoals,
212
+ summary
213
+ });
214
+ }