forge-openclaw-plugin 0.2.4 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/README.md +186 -6
  2. package/dist/assets/board-C_m78kvK.js +6 -0
  3. package/dist/assets/board-C_m78kvK.js.map +1 -0
  4. package/dist/assets/favicon-BCHm9dUV.ico +0 -0
  5. package/dist/assets/index-BWtLtXwb.js +36 -0
  6. package/dist/assets/index-BWtLtXwb.js.map +1 -0
  7. package/dist/assets/index-Dp5GXY_z.css +1 -0
  8. package/dist/assets/motion-CpZvZumD.js +10 -0
  9. package/dist/assets/motion-CpZvZumD.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-DtyXTw03.js +23 -0
  28. package/dist/assets/table-DtyXTw03.js.map +1 -0
  29. package/dist/assets/ui-BXbpiKyS.js +46 -0
  30. package/dist/assets/ui-BXbpiKyS.js.map +1 -0
  31. package/dist/assets/vendor-CRS-psbw.css +1 -0
  32. package/dist/assets/vendor-QBH6qVEe.js +433 -0
  33. package/dist/assets/vendor-QBH6qVEe.js.map +1 -0
  34. package/dist/assets/viz-w-IMeueL.js +34 -0
  35. package/dist/assets/viz-w-IMeueL.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 +9 -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 +136 -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 +63 -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 +2487 -0
  51. package/dist/server/db.js +313 -0
  52. package/dist/server/demo-data.js +49 -0
  53. package/dist/server/e2e-server.js +20 -0
  54. package/dist/server/errors.js +15 -0
  55. package/dist/server/index.js +16 -0
  56. package/dist/server/managers/base.js +17 -0
  57. package/dist/server/managers/contracts.js +47 -0
  58. package/dist/server/managers/platform/api-gateway-manager.js +11 -0
  59. package/dist/server/managers/platform/audit-manager.js +15 -0
  60. package/dist/server/managers/platform/authentication-manager.js +56 -0
  61. package/dist/server/managers/platform/authorization-manager.js +56 -0
  62. package/dist/server/managers/platform/background-job-manager.js +10 -0
  63. package/dist/server/managers/platform/configuration-manager.js +33 -0
  64. package/dist/server/managers/platform/database-manager.js +14 -0
  65. package/dist/server/managers/platform/event-bus-manager.js +7 -0
  66. package/dist/server/managers/platform/external-service-manager.js +11 -0
  67. package/dist/server/managers/platform/health-manager.js +7 -0
  68. package/dist/server/managers/platform/migration-manager.js +8 -0
  69. package/dist/server/managers/platform/search-index-manager.js +4 -0
  70. package/dist/server/managers/platform/secrets-manager.js +19 -0
  71. package/dist/server/managers/platform/session-manager.js +121 -0
  72. package/dist/server/managers/platform/storage-manager.js +16 -0
  73. package/dist/server/managers/platform/token-manager.js +37 -0
  74. package/dist/server/managers/platform/transaction-manager.js +8 -0
  75. package/dist/server/managers/platform/trusted-network.js +39 -0
  76. package/dist/server/managers/runtime.js +56 -0
  77. package/dist/server/managers/type-guards.js +4 -0
  78. package/dist/server/openapi.js +3553 -0
  79. package/dist/server/psyche-types.js +366 -0
  80. package/dist/server/repositories/activity-events.js +157 -0
  81. package/dist/server/repositories/collaboration.js +497 -0
  82. package/dist/server/repositories/deleted-entities.js +226 -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 +156 -0
  86. package/dist/server/repositories/notes.js +359 -0
  87. package/dist/server/repositories/projects.js +211 -0
  88. package/dist/server/repositories/psyche.js +1353 -0
  89. package/dist/server/repositories/rewards.js +675 -0
  90. package/dist/server/repositories/settings.js +399 -0
  91. package/dist/server/repositories/tags.js +160 -0
  92. package/dist/server/repositories/task-runs.js +490 -0
  93. package/dist/server/repositories/tasks.js +424 -0
  94. package/dist/server/seed-demo.js +11 -0
  95. package/dist/server/services/context.js +214 -0
  96. package/dist/server/services/dashboard.js +173 -0
  97. package/dist/server/services/entity-crud.js +573 -0
  98. package/dist/server/services/gamification.js +215 -0
  99. package/dist/server/services/insights.js +91 -0
  100. package/dist/server/services/projects.js +77 -0
  101. package/dist/server/services/psyche.js +63 -0
  102. package/dist/server/services/relations.js +28 -0
  103. package/dist/server/services/reviews.js +88 -0
  104. package/dist/server/services/run-recovery.js +13 -0
  105. package/dist/server/services/tagging.js +49 -0
  106. package/dist/server/services/task-run-watchdog.js +92 -0
  107. package/dist/server/services/work-time.js +176 -0
  108. package/dist/server/types.js +1058 -0
  109. package/dist/server/web.js +91 -0
  110. package/openclaw.plugin.json +32 -9
  111. package/package.json +17 -4
  112. package/server/migrations/001_core.sql +411 -0
  113. package/server/migrations/002_psyche.sql +392 -0
  114. package/skills/forge-openclaw/SKILL.md +197 -271
@@ -0,0 +1,424 @@
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 { getGoalById } from "./goals.js";
8
+ import { createLinkedNotes } from "./notes.js";
9
+ import { ensureDefaultProjectForGoal, getProjectById } from "./projects.js";
10
+ import { pruneLinkedEntityReferences } from "./psyche.js";
11
+ import { awardTaskCompletionReward, reverseLatestTaskCompletionReward } from "./rewards.js";
12
+ import { assertTaskRelations } from "../services/relations.js";
13
+ import { computeWorkTime, emptyTaskTimeSummary } from "../services/work-time.js";
14
+ import { taskSchema } from "../types.js";
15
+ function readTaskTagIds(taskId) {
16
+ const rows = getDatabase()
17
+ .prepare(`SELECT tag_id FROM task_tags WHERE task_id = ? ORDER BY tag_id`)
18
+ .all(taskId);
19
+ return filterDeletedIds("tag", rows.map((row) => row.tag_id));
20
+ }
21
+ function mapTask(row, time = emptyTaskTimeSummary()) {
22
+ return taskSchema.parse({
23
+ id: row.id,
24
+ title: row.title,
25
+ description: row.description,
26
+ status: row.status,
27
+ priority: row.priority,
28
+ owner: row.owner,
29
+ goalId: row.goal_id,
30
+ projectId: row.project_id,
31
+ dueDate: row.due_date,
32
+ effort: row.effort,
33
+ energy: row.energy,
34
+ points: row.points,
35
+ sortOrder: row.sort_order,
36
+ completedAt: row.completed_at,
37
+ createdAt: row.created_at,
38
+ updatedAt: row.updated_at,
39
+ tagIds: readTaskTagIds(row.id),
40
+ time
41
+ });
42
+ }
43
+ function replaceTaskTags(taskId, tagIds) {
44
+ const database = getDatabase();
45
+ database.prepare(`DELETE FROM task_tags WHERE task_id = ?`).run(taskId);
46
+ const insert = database.prepare(`INSERT INTO task_tags (task_id, tag_id) VALUES (?, ?)`);
47
+ for (const tagId of tagIds) {
48
+ insert.run(taskId, tagId);
49
+ }
50
+ }
51
+ function nextSortOrder(status) {
52
+ const row = getDatabase()
53
+ .prepare(`SELECT COALESCE(MAX(sort_order), -1) AS max_sort FROM tasks WHERE status = ?`)
54
+ .get(status);
55
+ return row.max_sort + 1;
56
+ }
57
+ function normalizeCompletedAt(status, existingCompletedAt) {
58
+ if (status === "done") {
59
+ return existingCompletedAt ?? new Date().toISOString();
60
+ }
61
+ return null;
62
+ }
63
+ function resolveProjectAndGoalIds(input, current) {
64
+ const currentGoalId = current?.goalId && getGoalById(current.goalId) ? current.goalId : null;
65
+ const currentProject = current?.projectId ? getProjectById(current.projectId) ?? null : null;
66
+ const currentProjectGoalId = currentProject?.goalId && getGoalById(currentProject.goalId) ? currentProject.goalId : null;
67
+ const currentProjectId = currentProject?.id ?? null;
68
+ const requestedGoalId = input.goalId === undefined ? currentGoalId : input.goalId;
69
+ const goalChangedWithoutProjectOverride = current !== undefined && input.goalId !== undefined && input.goalId !== current.goalId && input.projectId === undefined;
70
+ const requestedProjectId = input.projectId === undefined ? (goalChangedWithoutProjectOverride ? null : currentProjectId) : input.projectId;
71
+ if (requestedProjectId) {
72
+ const project = getProjectById(requestedProjectId);
73
+ if (!project) {
74
+ throw new HttpError(404, "project_not_found", `Project ${requestedProjectId} does not exist`);
75
+ }
76
+ const projectGoalId = getGoalById(project.goalId) ? project.goalId : null;
77
+ if (requestedGoalId && projectGoalId && project.goalId !== requestedGoalId) {
78
+ throw new HttpError(409, "project_goal_mismatch", `Project ${requestedProjectId} does not belong to goal ${requestedGoalId}`);
79
+ }
80
+ return {
81
+ goalId: projectGoalId,
82
+ projectId: project.id
83
+ };
84
+ }
85
+ if (requestedGoalId) {
86
+ const defaultProject = ensureDefaultProjectForGoal(requestedGoalId);
87
+ return {
88
+ goalId: requestedGoalId,
89
+ projectId: defaultProject.id
90
+ };
91
+ }
92
+ return {
93
+ goalId: null,
94
+ projectId: null
95
+ };
96
+ }
97
+ function inferReopenStatus(taskId) {
98
+ const row = getDatabase()
99
+ .prepare(`SELECT metadata_json
100
+ FROM activity_events
101
+ WHERE entity_type = 'task'
102
+ AND entity_id = ?
103
+ AND event_type = 'task_completed'
104
+ AND NOT EXISTS (
105
+ SELECT 1
106
+ FROM activity_event_corrections
107
+ WHERE activity_event_corrections.corrected_event_id = activity_events.id
108
+ )
109
+ ORDER BY created_at DESC
110
+ LIMIT 1`)
111
+ .get(taskId);
112
+ if (!row) {
113
+ return "focus";
114
+ }
115
+ const metadata = JSON.parse(row.metadata_json);
116
+ return metadata.previousStatus && metadata.previousStatus !== "done" ? metadata.previousStatus : "focus";
117
+ }
118
+ function updateTaskRecord(current, input, activity) {
119
+ const relationState = resolveProjectAndGoalIds(input, current);
120
+ const nextGoalId = relationState.goalId;
121
+ const nextProjectId = relationState.projectId;
122
+ const nextTagIds = input.tagIds ?? current.tagIds;
123
+ assertTaskRelations({ goalId: nextGoalId, tagIds: nextTagIds });
124
+ const nextStatus = input.status ?? current.status;
125
+ const movedColumns = nextStatus !== current.status;
126
+ const nextSort = input.sortOrder ?? (movedColumns ? nextSortOrder(nextStatus) : current.sortOrder);
127
+ const completedAt = normalizeCompletedAt(nextStatus, current.completedAt);
128
+ const updatedAt = new Date().toISOString();
129
+ getDatabase()
130
+ .prepare(`UPDATE tasks
131
+ SET title = ?, description = ?, status = ?, priority = ?, owner = ?, goal_id = ?, due_date = ?, effort = ?,
132
+ energy = ?, points = ?, sort_order = ?, completed_at = ?, updated_at = ?, project_id = ?
133
+ WHERE id = ?`)
134
+ .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);
135
+ replaceTaskTags(current.id, nextTagIds);
136
+ const updated = getTaskById(current.id);
137
+ if (updated && activity) {
138
+ const statusChanged = current.status !== updated.status;
139
+ const ownerChanged = current.owner !== updated.owner;
140
+ const goalChanged = current.goalId !== updated.goalId;
141
+ const projectChanged = current.projectId !== updated.projectId;
142
+ const pointsChanged = current.points !== updated.points;
143
+ const eventType = statusChanged && updated.status === "done"
144
+ ? "task_completed"
145
+ : statusChanged && current.status === "done"
146
+ ? "task_uncompleted"
147
+ : statusChanged
148
+ ? "task_status_changed"
149
+ : "task_updated";
150
+ const title = eventType === "task_completed"
151
+ ? `Task completed: ${updated.title}`
152
+ : eventType === "task_uncompleted"
153
+ ? `Task reopened: ${updated.title}`
154
+ : eventType === "task_status_changed"
155
+ ? `Task moved to ${updated.status.replaceAll("_", " ")}: ${updated.title}`
156
+ : `Task updated: ${updated.title}`;
157
+ recordActivityEvent({
158
+ entityType: "task",
159
+ entityId: updated.id,
160
+ eventType,
161
+ title,
162
+ description: goalChanged
163
+ ? `Goal link updated${updated.goalId ? ` to ${updated.goalId}` : ""}.`
164
+ : projectChanged
165
+ ? `Project link updated${updated.projectId ? ` to ${updated.projectId}` : ""}.`
166
+ : ownerChanged
167
+ ? `Ownership changed to ${updated.owner}.`
168
+ : statusChanged
169
+ ? `Status changed from ${current.status} to ${updated.status}.`
170
+ : "Task details were edited.",
171
+ actor: activity.actor ?? null,
172
+ source: activity.source,
173
+ metadata: {
174
+ previousStatus: current.status,
175
+ status: updated.status,
176
+ owner: updated.owner,
177
+ previousOwner: current.owner,
178
+ goalId: updated.goalId,
179
+ previousGoalId: current.goalId,
180
+ projectId: updated.projectId,
181
+ previousProjectId: current.projectId,
182
+ points: updated.points,
183
+ previousPoints: current.points,
184
+ pointsChanged
185
+ }
186
+ });
187
+ if (current.status !== "done" && updated.status === "done") {
188
+ awardTaskCompletionReward(updated, activity);
189
+ }
190
+ else if (current.status === "done" && updated.status !== "done") {
191
+ reverseLatestTaskCompletionReward(updated, activity);
192
+ }
193
+ }
194
+ if (updated) {
195
+ createLinkedNotes(input.notes, { entityType: "task", entityId: updated.id, anchorKey: null }, activity ?? { source: "ui", actor: null });
196
+ }
197
+ return updated;
198
+ }
199
+ function fingerprintTaskCreate(input) {
200
+ return createHash("sha256")
201
+ .update(JSON.stringify({
202
+ title: input.title,
203
+ description: input.description,
204
+ status: input.status,
205
+ priority: input.priority,
206
+ owner: input.owner,
207
+ goalId: input.goalId,
208
+ projectId: input.projectId,
209
+ dueDate: input.dueDate,
210
+ effort: input.effort,
211
+ energy: input.energy,
212
+ points: input.points,
213
+ sortOrder: input.sortOrder ?? null,
214
+ tagIds: input.tagIds,
215
+ notes: input.notes.map((note) => ({
216
+ contentMarkdown: note.contentMarkdown,
217
+ author: note.author,
218
+ links: note.links
219
+ }))
220
+ }))
221
+ .digest("hex");
222
+ }
223
+ function insertTaskRecord(input, activity) {
224
+ const relationState = resolveProjectAndGoalIds(input);
225
+ assertTaskRelations({ goalId: relationState.goalId, tagIds: input.tagIds });
226
+ if (!relationState.projectId) {
227
+ throw new HttpError(400, "project_required", "Tasks must belong to a project");
228
+ }
229
+ const now = new Date().toISOString();
230
+ const id = `task_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
231
+ const sortOrder = input.sortOrder ?? nextSortOrder(input.status);
232
+ const completedAt = normalizeCompletedAt(input.status, null);
233
+ getDatabase()
234
+ .prepare(`INSERT INTO tasks (
235
+ id, title, description, status, priority, owner, goal_id, project_id, due_date, effort, energy, points, sort_order,
236
+ completed_at, created_at, updated_at
237
+ )
238
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
239
+ .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);
240
+ replaceTaskTags(id, input.tagIds);
241
+ const task = getTaskById(id);
242
+ if (activity) {
243
+ recordActivityEvent({
244
+ entityType: "task",
245
+ entityId: task.id,
246
+ eventType: "task_created",
247
+ title: `Task created: ${task.title}`,
248
+ description: task.goalId ? `Linked to ${task.goalId} and assigned to ${task.owner}.` : `Assigned to ${task.owner}.`,
249
+ actor: activity.actor ?? null,
250
+ source: activity.source,
251
+ metadata: {
252
+ status: task.status,
253
+ owner: task.owner,
254
+ goalId: task.goalId,
255
+ projectId: task.projectId,
256
+ points: task.points
257
+ }
258
+ });
259
+ if (task.status === "done") {
260
+ awardTaskCompletionReward(task, activity);
261
+ }
262
+ }
263
+ createLinkedNotes(input.notes, { entityType: "task", entityId: task.id, anchorKey: null }, activity ?? { source: "ui", actor: null });
264
+ return task;
265
+ }
266
+ export function listTasks(filters = {}) {
267
+ const whereClauses = [];
268
+ const params = [];
269
+ const todayIso = new Date().toISOString().slice(0, 10);
270
+ const weekIso = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
271
+ if (filters.status) {
272
+ whereClauses.push("status = ?");
273
+ params.push(filters.status);
274
+ }
275
+ if (filters.owner) {
276
+ whereClauses.push("owner = ?");
277
+ params.push(filters.owner);
278
+ }
279
+ if (filters.goalId) {
280
+ whereClauses.push("goal_id = ?");
281
+ params.push(filters.goalId);
282
+ }
283
+ if (filters.projectId) {
284
+ whereClauses.push("project_id = ?");
285
+ params.push(filters.projectId);
286
+ }
287
+ if (filters.tagId) {
288
+ whereClauses.push("EXISTS (SELECT 1 FROM task_tags WHERE task_tags.task_id = tasks.id AND task_tags.tag_id = ?)");
289
+ params.push(filters.tagId);
290
+ }
291
+ if (filters.due === "overdue") {
292
+ whereClauses.push("status != 'done' AND due_date IS NOT NULL AND due_date < ?");
293
+ params.push(todayIso);
294
+ }
295
+ if (filters.due === "today") {
296
+ whereClauses.push("status != 'done' AND due_date = ?");
297
+ params.push(todayIso);
298
+ }
299
+ if (filters.due === "week") {
300
+ whereClauses.push("status != 'done' AND due_date IS NOT NULL AND due_date >= ? AND due_date <= ?");
301
+ params.push(todayIso, weekIso);
302
+ }
303
+ const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
304
+ const limitSql = filters.limit ? "LIMIT ?" : "";
305
+ if (filters.limit) {
306
+ params.push(filters.limit);
307
+ }
308
+ const rows = getDatabase()
309
+ .prepare(`SELECT id, title, description, status, priority, owner, goal_id, project_id, due_date, effort, energy, points, sort_order,
310
+ completed_at, created_at, updated_at
311
+ FROM tasks
312
+ ${whereSql}
313
+ ORDER BY
314
+ CASE status
315
+ WHEN 'backlog' THEN 0
316
+ WHEN 'focus' THEN 1
317
+ WHEN 'in_progress' THEN 2
318
+ WHEN 'blocked' THEN 3
319
+ ELSE 4
320
+ END,
321
+ sort_order,
322
+ created_at
323
+ ${limitSql}`)
324
+ .all(...params);
325
+ const workTime = computeWorkTime();
326
+ return filterDeletedEntities("task", rows.map((row) => mapTask(row, workTime.taskSummaries.get(row.id) ?? emptyTaskTimeSummary())));
327
+ }
328
+ export function getTaskById(taskId) {
329
+ if (isEntityDeleted("task", taskId)) {
330
+ return undefined;
331
+ }
332
+ const row = getDatabase()
333
+ .prepare(`SELECT id, title, description, status, priority, owner, goal_id, project_id, due_date, effort, energy, points, sort_order,
334
+ completed_at, created_at, updated_at
335
+ FROM tasks
336
+ WHERE id = ?`)
337
+ .get(taskId);
338
+ const workTime = computeWorkTime();
339
+ return row ? mapTask(row, workTime.taskSummaries.get(row.id) ?? emptyTaskTimeSummary()) : undefined;
340
+ }
341
+ export function createTask(input, activity) {
342
+ return runInTransaction(() => insertTaskRecord(input, activity));
343
+ }
344
+ export function createTaskWithIdempotency(input, idempotencyKey, activity) {
345
+ return runInTransaction(() => {
346
+ const fingerprint = fingerprintTaskCreate(input);
347
+ const existing = getDatabase()
348
+ .prepare(`SELECT task_id, request_fingerprint
349
+ FROM task_create_idempotency
350
+ WHERE idempotency_key = ?`)
351
+ .get(idempotencyKey);
352
+ if (existing) {
353
+ if (existing.request_fingerprint !== fingerprint) {
354
+ throw new HttpError(409, "idempotency_conflict", "Idempotency key was already used for a different task creation payload");
355
+ }
356
+ const task = getTaskById(existing.task_id);
357
+ if (!task) {
358
+ throw new HttpError(500, "idempotency_corruption", `Stored task ${existing.task_id} for idempotency key is missing`);
359
+ }
360
+ return { task, replayed: true };
361
+ }
362
+ const task = insertTaskRecord(input, activity);
363
+ getDatabase()
364
+ .prepare(`INSERT INTO task_create_idempotency (idempotency_key, request_fingerprint, task_id, created_at)
365
+ VALUES (?, ?, ?, ?)`)
366
+ .run(idempotencyKey, fingerprint, task.id, new Date().toISOString());
367
+ return { task, replayed: false };
368
+ });
369
+ }
370
+ export function updateTask(taskId, input, activity) {
371
+ const current = getTaskById(taskId);
372
+ if (!current) {
373
+ return undefined;
374
+ }
375
+ return runInTransaction(() => updateTaskRecord(current, input, activity));
376
+ }
377
+ export function updateTaskInTransaction(taskId, input, activity) {
378
+ const current = getTaskById(taskId);
379
+ if (!current) {
380
+ return undefined;
381
+ }
382
+ return updateTaskRecord(current, input, activity);
383
+ }
384
+ export function uncompleteTask(taskId, activity) {
385
+ const current = getTaskById(taskId);
386
+ if (!current) {
387
+ return undefined;
388
+ }
389
+ if (current.status !== "done") {
390
+ return current;
391
+ }
392
+ return runInTransaction(() => updateTaskRecord(current, { status: inferReopenStatus(taskId) }, activity));
393
+ }
394
+ export function deleteTask(taskId, activity) {
395
+ const current = getTaskById(taskId);
396
+ if (!current) {
397
+ return undefined;
398
+ }
399
+ return runInTransaction(() => {
400
+ pruneLinkedEntityReferences("task", taskId);
401
+ getDatabase()
402
+ .prepare(`DELETE FROM tasks WHERE id = ?`)
403
+ .run(taskId);
404
+ if (activity) {
405
+ recordActivityEvent({
406
+ entityType: "task",
407
+ entityId: current.id,
408
+ eventType: "task_deleted",
409
+ title: `Task deleted: ${current.title}`,
410
+ description: "Task removed from the system.",
411
+ actor: activity.actor ?? null,
412
+ source: activity.source,
413
+ metadata: {
414
+ status: current.status,
415
+ owner: current.owner,
416
+ goalId: current.goalId,
417
+ projectId: current.projectId,
418
+ points: current.points
419
+ }
420
+ });
421
+ }
422
+ return current;
423
+ });
424
+ }
@@ -0,0 +1,11 @@
1
+ import { seedDemoDataIntoRuntime } from "./demo-data.js";
2
+ const explicitDataRoot = process.argv[2];
3
+ try {
4
+ const summary = await seedDemoDataIntoRuntime(explicitDataRoot);
5
+ console.log(`Seeded Forge demo data into ${summary.databasePath}`);
6
+ console.log(`Counts: goals=${summary.counts.goals}, projects=${summary.counts.projects}, tasks=${summary.counts.tasks}, task_runs=${summary.counts.task_runs}`);
7
+ }
8
+ catch (error) {
9
+ console.error(error instanceof Error ? error.message : String(error));
10
+ process.exitCode = 1;
11
+ }
@@ -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
+ }