forge-openclaw-plugin 0.2.18 → 0.2.19

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 (56) hide show
  1. package/README.md +36 -4
  2. package/dist/assets/{board-2KevHCI0.js → board-8L3uX7_O.js} +2 -2
  3. package/dist/assets/{board-2KevHCI0.js.map → board-8L3uX7_O.js.map} +1 -1
  4. package/dist/assets/index-Cj1IBH_w.js +36 -0
  5. package/dist/assets/index-Cj1IBH_w.js.map +1 -0
  6. package/dist/assets/index-DQT6EbuS.css +1 -0
  7. package/dist/assets/{motion-q19HPmWs.js → motion-1GAqqi8M.js} +2 -2
  8. package/dist/assets/{motion-q19HPmWs.js.map → motion-1GAqqi8M.js.map} +1 -1
  9. package/dist/assets/{table-BDMHBY4a.js → table-DBGlgRjk.js} +2 -2
  10. package/dist/assets/{table-BDMHBY4a.js.map → table-DBGlgRjk.js.map} +1 -1
  11. package/dist/assets/{ui-CQ_AsFs8.js → ui-iTluWjC4.js} +2 -2
  12. package/dist/assets/{ui-CQ_AsFs8.js.map → ui-iTluWjC4.js.map} +1 -1
  13. package/dist/assets/{vendor-5HifrnRK.js → vendor-BvM2F9Dp.js} +139 -84
  14. package/dist/assets/vendor-BvM2F9Dp.js.map +1 -0
  15. package/dist/assets/{viz-CQzkRnTu.js → viz-CNeunkfu.js} +2 -2
  16. package/dist/assets/{viz-CQzkRnTu.js.map → viz-CNeunkfu.js.map} +1 -1
  17. package/dist/index.html +8 -8
  18. package/dist/openclaw/parity.js +1 -0
  19. package/dist/openclaw/routes.js +7 -0
  20. package/dist/openclaw/tools.js +183 -16
  21. package/dist/server/app.js +2509 -263
  22. package/dist/server/managers/platform/secrets-manager.js +44 -1
  23. package/dist/server/managers/runtime.js +3 -1
  24. package/dist/server/openapi.js +2037 -172
  25. package/dist/server/repositories/calendar.js +1101 -0
  26. package/dist/server/repositories/deleted-entities.js +10 -2
  27. package/dist/server/repositories/notes.js +161 -28
  28. package/dist/server/repositories/projects.js +45 -13
  29. package/dist/server/repositories/rewards.js +114 -6
  30. package/dist/server/repositories/settings.js +47 -5
  31. package/dist/server/repositories/task-runs.js +46 -10
  32. package/dist/server/repositories/tasks.js +25 -9
  33. package/dist/server/repositories/weekly-reviews.js +109 -0
  34. package/dist/server/repositories/work-adjustments.js +105 -0
  35. package/dist/server/services/calendar-runtime.js +1301 -0
  36. package/dist/server/services/entity-crud.js +94 -3
  37. package/dist/server/services/projects.js +32 -8
  38. package/dist/server/services/reviews.js +15 -1
  39. package/dist/server/services/work-time.js +27 -0
  40. package/dist/server/types.js +934 -49
  41. package/openclaw.plugin.json +1 -1
  42. package/package.json +1 -1
  43. package/server/migrations/006_work_adjustments.sql +14 -0
  44. package/server/migrations/007_weekly_review_closures.sql +17 -0
  45. package/server/migrations/008_calendar_execution.sql +147 -0
  46. package/server/migrations/009_true_calendar_events.sql +195 -0
  47. package/server/migrations/010_calendar_selection_state.sql +6 -0
  48. package/server/migrations/011_calendar_timezone_backfill.sql +11 -0
  49. package/server/migrations/012_work_block_ranges.sql +7 -0
  50. package/server/migrations/013_microsoft_local_auth_settings.sql +8 -0
  51. package/server/migrations/014_note_tags_and_ephemeral.sql +8 -0
  52. package/skills/forge-openclaw/SKILL.md +117 -11
  53. package/dist/assets/index-CDYW4WDH.js +0 -36
  54. package/dist/assets/index-CDYW4WDH.js.map +0 -1
  55. package/dist/assets/index-yroQr6YZ.css +0 -1
  56. package/dist/assets/vendor-5HifrnRK.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import { getDatabase, runInTransaction } from "../db.js";
2
2
  import { createInsight, deleteInsight, getInsightById, listInsights, updateInsight } from "../repositories/collaboration.js";
3
+ import { createCalendarEvent, createTaskTimebox, createWorkBlockTemplate, deleteCalendarEvent, deleteTaskTimebox, deleteWorkBlockTemplate, getCalendarEventById, getTaskTimeboxById, getWorkBlockTemplateById, listCalendarEvents, listTaskTimeboxes, listWorkBlockTemplates, updateCalendarEvent, updateTaskTimebox, updateWorkBlockTemplate } from "../repositories/calendar.js";
3
4
  import { createNote, deleteNote, getNoteById, listNotes, unlinkNotesForEntity, updateNote } from "../repositories/notes.js";
4
5
  import { createBehaviorPatternSchema, createBehaviorSchema, createBeliefEntrySchema, createEmotionDefinitionSchema, createEventTypeSchema, createModeGuideSessionSchema, createModeProfileSchema, createPsycheValueSchema, createTriggerReportSchema, updateBehaviorPatternSchema, updateBehaviorSchema, updateBeliefEntrySchema, updateEmotionDefinitionSchema, updateEventTypeSchema, updateModeGuideSessionSchema, updateModeProfileSchema, updatePsycheValueSchema, updateTriggerReportSchema } from "../psyche-types.js";
5
6
  import { buildSettingsBinPayload, cascadeSoftDeleteAnchoredCollaboration, clearDeletedEntityRecord, getDeletedEntityRecord, listDeletedEntities, restoreAnchoredCollaboration, restoreDeletedEntityRecord, upsertDeletedEntityRecord } from "../repositories/deleted-entities.js";
@@ -9,7 +10,11 @@ import { createBehavior, createBehaviorPattern, createBeliefEntry, createEmotion
9
10
  import { createProject, deleteProject, getProjectById, listProjects, updateProject } from "../repositories/projects.js";
10
11
  import { createTag, deleteTag, getTagById, listTags, updateTag } from "../repositories/tags.js";
11
12
  import { createTask, deleteTask, getTaskById, listTasks, updateTask } from "../repositories/tasks.js";
12
- import { createGoalSchema, createHabitSchema, createInsightSchema, createNoteSchema, createProjectSchema, createTagSchema, createTaskSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateNoteSchema, updateProjectSchema, updateTagSchema, updateTaskSchema } from "../types.js";
13
+ import { createCalendarEventSchema, createGoalSchema, createHabitSchema, createInsightSchema, createNoteSchema, createProjectSchema, createTaskTimeboxSchema, createTagSchema, createTaskSchema, createWorkBlockTemplateSchema, updateCalendarEventSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateNoteSchema, updateProjectSchema, updateTaskTimeboxSchema, updateTagSchema, updateTaskSchema, updateWorkBlockTemplateSchema } from "../types.js";
14
+ const ENTITY_CALENDAR_LIST_RANGE = {
15
+ from: "1970-01-01T00:00:00.000Z",
16
+ to: "2100-01-01T00:00:00.000Z"
17
+ };
13
18
  class AtomicBatchRollback extends Error {
14
19
  index;
15
20
  code;
@@ -26,6 +31,8 @@ const CRUD_ENTITY_CAPABILITIES = {
26
31
  goal: {
27
32
  entityType: "goal",
28
33
  routeBase: "/api/v1/goals",
34
+ deleteMode: "soft_default",
35
+ inBin: true,
29
36
  list: () => listGoals(),
30
37
  get: (id) => getGoalById(id),
31
38
  create: (data, context) => createGoal(data, context),
@@ -35,6 +42,8 @@ const CRUD_ENTITY_CAPABILITIES = {
35
42
  project: {
36
43
  entityType: "project",
37
44
  routeBase: "/api/v1/projects",
45
+ deleteMode: "soft_default",
46
+ inBin: true,
38
47
  list: () => listProjects(),
39
48
  get: (id) => getProjectById(id),
40
49
  create: (data, context) => createProject(data, context),
@@ -44,6 +53,8 @@ const CRUD_ENTITY_CAPABILITIES = {
44
53
  task: {
45
54
  entityType: "task",
46
55
  routeBase: "/api/v1/tasks",
56
+ deleteMode: "soft_default",
57
+ inBin: true,
47
58
  list: () => listTasks(),
48
59
  get: (id) => getTaskById(id),
49
60
  create: (data, context) => createTask(data, context),
@@ -53,6 +64,8 @@ const CRUD_ENTITY_CAPABILITIES = {
53
64
  habit: {
54
65
  entityType: "habit",
55
66
  routeBase: "/api/v1/habits",
67
+ deleteMode: "soft_default",
68
+ inBin: true,
56
69
  list: () => listHabits(),
57
70
  get: (id) => getHabitById(id),
58
71
  create: (data, context) => createHabit(data, context),
@@ -62,6 +75,8 @@ const CRUD_ENTITY_CAPABILITIES = {
62
75
  tag: {
63
76
  entityType: "tag",
64
77
  routeBase: "/api/v1/tags",
78
+ deleteMode: "soft_default",
79
+ inBin: true,
65
80
  list: () => listTags(),
66
81
  get: (id) => getTagById(id),
67
82
  create: (data, context) => createTag(data, context),
@@ -71,6 +86,8 @@ const CRUD_ENTITY_CAPABILITIES = {
71
86
  note: {
72
87
  entityType: "note",
73
88
  routeBase: "/api/v1/notes",
89
+ deleteMode: "soft_default",
90
+ inBin: true,
74
91
  list: () => listNotes(),
75
92
  get: (id) => getNoteById(id),
76
93
  create: (data, context) => createNote(data, context),
@@ -80,15 +97,52 @@ const CRUD_ENTITY_CAPABILITIES = {
80
97
  insight: {
81
98
  entityType: "insight",
82
99
  routeBase: "/api/v1/insights",
100
+ deleteMode: "soft_default",
101
+ inBin: true,
83
102
  list: () => listInsights(),
84
103
  get: (id) => getInsightById(id),
85
104
  create: (data, context) => createInsight(data, context),
86
105
  update: (id, patch, context) => updateInsight(id, patch, context),
87
106
  hardDelete: (id, context) => deleteInsight(id, context)
88
107
  },
108
+ calendar_event: {
109
+ entityType: "calendar_event",
110
+ routeBase: "/api/v1/calendar/events",
111
+ deleteMode: "immediate",
112
+ inBin: false,
113
+ list: () => listCalendarEvents(ENTITY_CALENDAR_LIST_RANGE),
114
+ get: (id) => getCalendarEventById(id),
115
+ create: (data) => createCalendarEvent(data),
116
+ update: (id, patch) => updateCalendarEvent(id, patch),
117
+ hardDelete: (id) => deleteCalendarEvent(id)
118
+ },
119
+ work_block_template: {
120
+ entityType: "work_block_template",
121
+ routeBase: "/api/v1/calendar/work-block-templates",
122
+ deleteMode: "immediate",
123
+ inBin: false,
124
+ list: () => listWorkBlockTemplates(),
125
+ get: (id) => getWorkBlockTemplateById(id),
126
+ create: (data) => createWorkBlockTemplate(data),
127
+ update: (id, patch) => updateWorkBlockTemplate(id, patch),
128
+ hardDelete: (id) => deleteWorkBlockTemplate(id)
129
+ },
130
+ task_timebox: {
131
+ entityType: "task_timebox",
132
+ routeBase: "/api/v1/calendar/timeboxes",
133
+ deleteMode: "immediate",
134
+ inBin: false,
135
+ list: () => listTaskTimeboxes(ENTITY_CALENDAR_LIST_RANGE),
136
+ get: (id) => getTaskTimeboxById(id),
137
+ create: (data) => createTaskTimebox(data),
138
+ update: (id, patch) => updateTaskTimebox(id, patch),
139
+ hardDelete: (id) => deleteTaskTimebox(id)
140
+ },
89
141
  psyche_value: {
90
142
  entityType: "psyche_value",
91
143
  routeBase: "/api/v1/psyche/values",
144
+ deleteMode: "soft_default",
145
+ inBin: true,
92
146
  list: () => listPsycheValues(),
93
147
  get: (id) => getPsycheValueById(id),
94
148
  create: (data, context) => createPsycheValue(data, context),
@@ -98,6 +152,8 @@ const CRUD_ENTITY_CAPABILITIES = {
98
152
  behavior_pattern: {
99
153
  entityType: "behavior_pattern",
100
154
  routeBase: "/api/v1/psyche/patterns",
155
+ deleteMode: "soft_default",
156
+ inBin: true,
101
157
  list: () => listBehaviorPatterns(),
102
158
  get: (id) => getBehaviorPatternById(id),
103
159
  create: (data, context) => createBehaviorPattern(data, context),
@@ -107,6 +163,8 @@ const CRUD_ENTITY_CAPABILITIES = {
107
163
  behavior: {
108
164
  entityType: "behavior",
109
165
  routeBase: "/api/v1/psyche/behaviors",
166
+ deleteMode: "soft_default",
167
+ inBin: true,
110
168
  list: () => listBehaviors(),
111
169
  get: (id) => getBehaviorById(id),
112
170
  create: (data, context) => createBehavior(data, context),
@@ -116,6 +174,8 @@ const CRUD_ENTITY_CAPABILITIES = {
116
174
  belief_entry: {
117
175
  entityType: "belief_entry",
118
176
  routeBase: "/api/v1/psyche/beliefs",
177
+ deleteMode: "soft_default",
178
+ inBin: true,
119
179
  list: () => listBeliefEntries(),
120
180
  get: (id) => getBeliefEntryById(id),
121
181
  create: (data, context) => createBeliefEntry(data, context),
@@ -125,6 +185,8 @@ const CRUD_ENTITY_CAPABILITIES = {
125
185
  mode_profile: {
126
186
  entityType: "mode_profile",
127
187
  routeBase: "/api/v1/psyche/modes",
188
+ deleteMode: "soft_default",
189
+ inBin: true,
128
190
  list: () => listModeProfiles(),
129
191
  get: (id) => getModeProfileById(id),
130
192
  create: (data, context) => createModeProfile(data, context),
@@ -134,6 +196,8 @@ const CRUD_ENTITY_CAPABILITIES = {
134
196
  mode_guide_session: {
135
197
  entityType: "mode_guide_session",
136
198
  routeBase: "/api/v1/psyche/mode-guides",
199
+ deleteMode: "soft_default",
200
+ inBin: true,
137
201
  list: () => listModeGuideSessions(200),
138
202
  get: (id) => getModeGuideSessionById(id),
139
203
  create: (data, context) => createModeGuideSession(data, context),
@@ -143,6 +207,8 @@ const CRUD_ENTITY_CAPABILITIES = {
143
207
  event_type: {
144
208
  entityType: "event_type",
145
209
  routeBase: "/api/v1/psyche/event-types",
210
+ deleteMode: "soft_default",
211
+ inBin: true,
146
212
  list: () => listEventTypes(),
147
213
  get: (id) => getEventTypeById(id),
148
214
  create: (data, context) => createEventType(data, context),
@@ -152,6 +218,8 @@ const CRUD_ENTITY_CAPABILITIES = {
152
218
  emotion_definition: {
153
219
  entityType: "emotion_definition",
154
220
  routeBase: "/api/v1/psyche/emotions",
221
+ deleteMode: "soft_default",
222
+ inBin: true,
155
223
  list: () => listEmotionDefinitions(),
156
224
  get: (id) => getEmotionDefinitionById(id),
157
225
  create: (data, context) => createEmotionDefinition(data, context),
@@ -161,6 +229,8 @@ const CRUD_ENTITY_CAPABILITIES = {
161
229
  trigger_report: {
162
230
  entityType: "trigger_report",
163
231
  routeBase: "/api/v1/psyche/reports",
232
+ deleteMode: "soft_default",
233
+ inBin: true,
164
234
  list: () => listTriggerReports(200),
165
235
  get: (id) => getTriggerReportById(id),
166
236
  create: (data, context) => createTriggerReport(data, context),
@@ -173,8 +243,8 @@ export function getCrudEntityCapabilityMatrix() {
173
243
  entityType: capability.entityType,
174
244
  routeBase: capability.routeBase,
175
245
  pluginExposed: true,
176
- deleteMode: "soft_default",
177
- inBin: true
246
+ deleteMode: capability.deleteMode,
247
+ inBin: capability.inBin
178
248
  }));
179
249
  }
180
250
  function getCapability(entityType) {
@@ -188,6 +258,9 @@ const CREATE_ENTITY_SCHEMAS = {
188
258
  tag: createTagSchema,
189
259
  note: createNoteSchema,
190
260
  insight: createInsightSchema,
261
+ calendar_event: createCalendarEventSchema,
262
+ work_block_template: createWorkBlockTemplateSchema,
263
+ task_timebox: createTaskTimeboxSchema,
191
264
  psyche_value: createPsycheValueSchema,
192
265
  behavior_pattern: createBehaviorPatternSchema,
193
266
  behavior: createBehaviorSchema,
@@ -206,6 +279,9 @@ const UPDATE_ENTITY_SCHEMAS = {
206
279
  tag: updateTagSchema,
207
280
  note: updateNoteSchema,
208
281
  insight: updateInsightSchema,
282
+ calendar_event: updateCalendarEventSchema,
283
+ work_block_template: updateWorkBlockTemplateSchema,
284
+ task_timebox: updateTaskTimeboxSchema,
209
285
  psyche_value: updatePsycheValueSchema,
210
286
  behavior_pattern: updateBehaviorPatternSchema,
211
287
  behavior: updateBehaviorSchema,
@@ -344,6 +420,17 @@ function matchesLinkedTo(entityType, entity, linkedTo) {
344
420
  link.entityId === linkedTo.id));
345
421
  case "insight":
346
422
  return entity.entityType === linkedTo.entityType && entity.entityId === linkedTo.id;
423
+ case "calendar_event":
424
+ return (Array.isArray(entity.links) &&
425
+ entity.links.some((link) => typeof link === "object" &&
426
+ link !== null &&
427
+ "entityType" in link &&
428
+ "entityId" in link &&
429
+ link.entityType === linkedTo.entityType &&
430
+ link.entityId === linkedTo.id));
431
+ case "task_timebox":
432
+ return ((linkedTo.entityType === "task" && entity.taskId === linkedTo.id) ||
433
+ (linkedTo.entityType === "project" && entity.projectId === linkedTo.id));
347
434
  case "psyche_value":
348
435
  return ((linkedTo.entityType === "goal" && Array.isArray(entity.linkedGoalIds) && entity.linkedGoalIds.includes(linkedTo.id)) ||
349
436
  (linkedTo.entityType === "project" && Array.isArray(entity.linkedProjectIds) && entity.linkedProjectIds.includes(linkedTo.id)) ||
@@ -412,6 +499,10 @@ function purgeAnchoredCollaboration(entityType, entityId) {
412
499
  export function deleteEntity(entityType, id, options, context) {
413
500
  const capability = getCapability(entityType);
414
501
  const mode = options.mode ?? "soft";
502
+ if (capability.deleteMode === "immediate") {
503
+ clearDeletedEntityRecord(entityType, id);
504
+ return capability.hardDelete(id, context);
505
+ }
415
506
  const existing = capability.get(id);
416
507
  if (!existing) {
417
508
  const deleted = getDeletedEntityRecord(entityType, id);
@@ -3,6 +3,7 @@ import { getGoalById, listGoals } from "../repositories/goals.js";
3
3
  import { buildNotesSummaryByEntity } from "../repositories/notes.js";
4
4
  import { listProjects } from "../repositories/projects.js";
5
5
  import { listTasks } from "../repositories/tasks.js";
6
+ import { listProjectWorkAdjustmentSecondsMap } from "../repositories/work-adjustments.js";
6
7
  import { emptyTaskTimeSummary } from "./work-time.js";
7
8
  import { projectBoardPayloadSchema, projectSummarySchema } from "../types.js";
8
9
  function projectTaskSummary(tasks) {
@@ -33,25 +34,48 @@ function projectTaskSummary(tasks) {
33
34
  nextTaskId: nextTask?.id ?? null,
34
35
  nextTaskTitle: nextTask?.title ?? null,
35
36
  momentumLabel,
36
- time: tasks.reduce((summary, task) => ({
37
- totalTrackedSeconds: summary.totalTrackedSeconds + task.time.totalTrackedSeconds,
38
- totalCreditedSeconds: Math.round((summary.totalCreditedSeconds + task.time.totalCreditedSeconds) * 100) / 100,
39
- activeRunCount: summary.activeRunCount + task.time.activeRunCount,
40
- hasCurrentRun: summary.hasCurrentRun || task.time.hasCurrentRun,
41
- currentRunId: summary.currentRunId ?? task.time.currentRunId
42
- }), emptyTaskTimeSummary())
37
+ time: tasks.reduce((summary, task) => {
38
+ const liveTrackedSeconds = task.time.liveTrackedSeconds ?? 0;
39
+ const liveCreditedSeconds = task.time.liveCreditedSeconds ?? 0;
40
+ const manualAdjustedSeconds = task.time.manualAdjustedSeconds ?? 0;
41
+ return {
42
+ totalTrackedSeconds: summary.totalTrackedSeconds + task.time.totalTrackedSeconds,
43
+ totalCreditedSeconds: Math.round((summary.totalCreditedSeconds + task.time.totalCreditedSeconds) * 100) / 100,
44
+ liveTrackedSeconds: summary.liveTrackedSeconds + liveTrackedSeconds,
45
+ liveCreditedSeconds: Math.round((summary.liveCreditedSeconds + liveCreditedSeconds) * 100) / 100,
46
+ manualAdjustedSeconds: summary.manualAdjustedSeconds + manualAdjustedSeconds,
47
+ activeRunCount: summary.activeRunCount + task.time.activeRunCount,
48
+ hasCurrentRun: summary.hasCurrentRun || task.time.hasCurrentRun,
49
+ currentRunId: summary.currentRunId ?? task.time.currentRunId
50
+ };
51
+ }, emptyTaskTimeSummary())
43
52
  };
44
53
  }
45
54
  export function listProjectSummaries(filters = {}) {
46
55
  const goals = new Map(listGoals().map((goal) => [goal.id, goal]));
47
56
  const tasks = listTasks();
57
+ const projectAdjustmentSeconds = listProjectWorkAdjustmentSecondsMap();
48
58
  return listProjects(filters).map((project) => {
49
59
  const goal = goals.get(project.goalId);
50
60
  const projectTasks = tasks.filter((task) => task.projectId === project.id);
61
+ const taskSummary = projectTaskSummary(projectTasks);
62
+ const projectAdjustmentSecondsTotal = projectAdjustmentSeconds.get(project.id) ?? 0;
63
+ const manualAdjustedSeconds = (taskSummary.time.manualAdjustedSeconds ?? 0) + projectAdjustmentSecondsTotal;
64
+ const time = {
65
+ totalTrackedSeconds: Math.max(0, taskSummary.time.totalTrackedSeconds + projectAdjustmentSecondsTotal),
66
+ totalCreditedSeconds: Math.round(Math.max(0, taskSummary.time.totalCreditedSeconds + projectAdjustmentSecondsTotal) * 100) / 100,
67
+ liveTrackedSeconds: taskSummary.time.liveTrackedSeconds ?? 0,
68
+ liveCreditedSeconds: taskSummary.time.liveCreditedSeconds ?? 0,
69
+ manualAdjustedSeconds,
70
+ activeRunCount: taskSummary.time.activeRunCount,
71
+ hasCurrentRun: taskSummary.time.hasCurrentRun,
72
+ currentRunId: taskSummary.time.currentRunId
73
+ };
51
74
  return projectSummarySchema.parse({
52
75
  ...project,
53
76
  goalTitle: goal?.title ?? "Unknown life goal",
54
- ...projectTaskSummary(projectTasks)
77
+ ...taskSummary,
78
+ time
55
79
  });
56
80
  });
57
81
  }
@@ -2,9 +2,10 @@ import { listActivityEvents } from "../repositories/activity-events.js";
2
2
  import { listGoals } from "../repositories/goals.js";
3
3
  import { listHabits } from "../repositories/habits.js";
4
4
  import { listTasks } from "../repositories/tasks.js";
5
+ import { getWeeklyReviewClosure } from "../repositories/weekly-reviews.js";
5
6
  import { buildGamificationProfile } from "./gamification.js";
6
7
  import { weeklyReviewPayloadSchema } from "../types.js";
7
- function startOfWeek(date) {
8
+ export function startOfWeek(date) {
8
9
  const clone = new Date(date);
9
10
  const day = clone.getDay();
10
11
  const delta = day === 0 ? -6 : 1 - day;
@@ -20,6 +21,9 @@ function addDays(date, days) {
20
21
  function formatRange(start, end) {
21
22
  return `${start.toLocaleDateString("en-US", { month: "short", day: "numeric" })} - ${end.toLocaleDateString("en-US", { month: "short", day: "numeric" })}`;
22
23
  }
24
+ function toDateOnly(date) {
25
+ return date.toISOString().slice(0, 10);
26
+ }
23
27
  function dailyBuckets(tasks, start) {
24
28
  return Array.from({ length: 7 }, (_, index) => {
25
29
  const current = addDays(start, index);
@@ -40,6 +44,8 @@ export function getWeeklyReviewPayload(now = new Date()) {
40
44
  const gamification = buildGamificationProfile(goals, tasks, listHabits(), now);
41
45
  const weekStart = startOfWeek(now);
42
46
  const weekEnd = addDays(weekStart, 6);
47
+ const weekKey = toDateOnly(weekStart);
48
+ const closure = getWeeklyReviewClosure(weekKey);
43
49
  const weekTasks = tasks.filter((task) => task.updatedAt >= weekStart.toISOString() && task.updatedAt <= addDays(weekEnd, 1).toISOString());
44
50
  const completedTasks = weekTasks.filter((task) => task.completedAt !== null);
45
51
  const buckets = dailyBuckets(tasks, weekStart);
@@ -62,6 +68,9 @@ export function getWeeklyReviewPayload(now = new Date()) {
62
68
  return weeklyReviewPayloadSchema.parse({
63
69
  generatedAt: now.toISOString(),
64
70
  windowLabel: formatRange(weekStart, weekEnd),
71
+ weekKey,
72
+ weekStartDate: weekKey,
73
+ weekEndDate: toDateOnly(weekEnd),
65
74
  momentumSummary: {
66
75
  totalXp,
67
76
  focusHours: buckets.reduce((sum, bucket) => sum + bucket.focusHours, 0),
@@ -84,6 +93,11 @@ export function getWeeklyReviewPayload(now = new Date()) {
84
93
  title: "Review Completion Bonus",
85
94
  summary: "Finalizing the review locks the current cycle into evidence.",
86
95
  rewardXp: 250
96
+ },
97
+ completion: {
98
+ finalized: closure !== null,
99
+ finalizedAt: closure?.createdAt ?? null,
100
+ finalizedBy: closure?.actor ?? null
87
101
  }
88
102
  });
89
103
  }
@@ -1,4 +1,5 @@
1
1
  import { getDatabase } from "../db.js";
2
+ import { listTaskWorkAdjustmentSecondsMap } from "../repositories/work-adjustments.js";
2
3
  function readTimeAccountingMode() {
3
4
  try {
4
5
  const row = getDatabase()
@@ -132,6 +133,9 @@ export function computeWorkTime(now = new Date()) {
132
133
  const existing = taskSummaries.get(timing.row.task_id) ?? {
133
134
  totalTrackedSeconds: 0,
134
135
  totalCreditedSeconds: 0,
136
+ liveTrackedSeconds: 0,
137
+ liveCreditedSeconds: 0,
138
+ manualAdjustedSeconds: 0,
135
139
  activeRunCount: 0,
136
140
  hasCurrentRun: false,
137
141
  currentRunId: null
@@ -139,11 +143,28 @@ export function computeWorkTime(now = new Date()) {
139
143
  taskSummaries.set(timing.row.task_id, {
140
144
  totalTrackedSeconds: existing.totalTrackedSeconds + elapsedWallSeconds,
141
145
  totalCreditedSeconds: roundCreditedSeconds(existing.totalCreditedSeconds + creditedSeconds),
146
+ liveTrackedSeconds: existing.liveTrackedSeconds + elapsedWallSeconds,
147
+ liveCreditedSeconds: roundCreditedSeconds(existing.liveCreditedSeconds + creditedSeconds),
148
+ manualAdjustedSeconds: existing.manualAdjustedSeconds,
142
149
  activeRunCount: existing.activeRunCount + (timing.row.status === "active" ? 1 : 0),
143
150
  hasCurrentRun: existing.hasCurrentRun || isCurrent,
144
151
  currentRunId: isCurrent ? timing.row.id : existing.currentRunId
145
152
  });
146
153
  }
154
+ const adjustmentSecondsByTaskId = listTaskWorkAdjustmentSecondsMap();
155
+ for (const [taskId, adjustmentSeconds] of adjustmentSecondsByTaskId.entries()) {
156
+ const existing = taskSummaries.get(taskId) ?? emptyTaskTimeSummary();
157
+ taskSummaries.set(taskId, {
158
+ totalTrackedSeconds: Math.max(0, existing.totalTrackedSeconds + adjustmentSeconds),
159
+ totalCreditedSeconds: roundCreditedSeconds(Math.max(0, existing.totalCreditedSeconds + adjustmentSeconds)),
160
+ liveTrackedSeconds: existing.liveTrackedSeconds,
161
+ liveCreditedSeconds: existing.liveCreditedSeconds,
162
+ manualAdjustedSeconds: existing.manualAdjustedSeconds + adjustmentSeconds,
163
+ activeRunCount: existing.activeRunCount,
164
+ hasCurrentRun: existing.hasCurrentRun,
165
+ currentRunId: existing.currentRunId
166
+ });
167
+ }
147
168
  return {
148
169
  mode,
149
170
  runMetrics,
@@ -154,6 +175,9 @@ export function emptyTaskTimeSummary() {
154
175
  return {
155
176
  totalTrackedSeconds: 0,
156
177
  totalCreditedSeconds: 0,
178
+ liveTrackedSeconds: 0,
179
+ liveCreditedSeconds: 0,
180
+ manualAdjustedSeconds: 0,
157
181
  activeRunCount: 0,
158
182
  hasCurrentRun: false,
159
183
  currentRunId: null
@@ -168,6 +192,9 @@ export function sumTaskTimeSummaries(taskIds, summaries) {
168
192
  return {
169
193
  totalTrackedSeconds: accumulator.totalTrackedSeconds + summary.totalTrackedSeconds,
170
194
  totalCreditedSeconds: roundCreditedSeconds(accumulator.totalCreditedSeconds + summary.totalCreditedSeconds),
195
+ liveTrackedSeconds: accumulator.liveTrackedSeconds + summary.liveTrackedSeconds,
196
+ liveCreditedSeconds: roundCreditedSeconds(accumulator.liveCreditedSeconds + summary.liveCreditedSeconds),
197
+ manualAdjustedSeconds: accumulator.manualAdjustedSeconds + summary.manualAdjustedSeconds,
171
198
  activeRunCount: accumulator.activeRunCount + summary.activeRunCount,
172
199
  hasCurrentRun: accumulator.hasCurrentRun || summary.hasCurrentRun,
173
200
  currentRunId: accumulator.currentRunId ?? summary.currentRunId