forge-openclaw-plugin 0.2.15 → 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 (67) hide show
  1. package/README.md +39 -4
  2. package/dist/assets/{board-C_m78kvK.js → board-8L3uX7_O.js} +2 -2
  3. package/dist/assets/{board-C_m78kvK.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-CpZvZumD.js → motion-1GAqqi8M.js} +2 -2
  8. package/dist/assets/{motion-CpZvZumD.js.map → motion-1GAqqi8M.js.map} +1 -1
  9. package/dist/assets/{table-DtyXTw03.js → table-DBGlgRjk.js} +2 -2
  10. package/dist/assets/{table-DtyXTw03.js.map → table-DBGlgRjk.js.map} +1 -1
  11. package/dist/assets/{ui-BXbpiKyS.js → ui-iTluWjC4.js} +2 -2
  12. package/dist/assets/{ui-BXbpiKyS.js.map → ui-iTluWjC4.js.map} +1 -1
  13. package/dist/assets/{vendor-QBH6qVEe.js → vendor-BvM2F9Dp.js} +151 -81
  14. package/dist/assets/vendor-BvM2F9Dp.js.map +1 -0
  15. package/dist/assets/{viz-w-IMeueL.js → viz-CNeunkfu.js} +2 -2
  16. package/dist/assets/{viz-w-IMeueL.js.map → viz-CNeunkfu.js.map} +1 -1
  17. package/dist/index.html +8 -8
  18. package/dist/openclaw/local-runtime.js +142 -9
  19. package/dist/openclaw/parity.js +1 -0
  20. package/dist/openclaw/plugin-entry-shared.js +7 -1
  21. package/dist/openclaw/routes.js +7 -0
  22. package/dist/openclaw/tools.js +198 -16
  23. package/dist/server/app.js +2615 -251
  24. package/dist/server/managers/platform/secrets-manager.js +44 -1
  25. package/dist/server/managers/runtime.js +3 -1
  26. package/dist/server/openapi.js +2212 -170
  27. package/dist/server/repositories/calendar.js +1101 -0
  28. package/dist/server/repositories/deleted-entities.js +10 -2
  29. package/dist/server/repositories/habits.js +358 -0
  30. package/dist/server/repositories/notes.js +161 -28
  31. package/dist/server/repositories/projects.js +45 -13
  32. package/dist/server/repositories/rewards.js +176 -6
  33. package/dist/server/repositories/settings.js +47 -5
  34. package/dist/server/repositories/task-runs.js +46 -10
  35. package/dist/server/repositories/tasks.js +25 -9
  36. package/dist/server/repositories/weekly-reviews.js +109 -0
  37. package/dist/server/repositories/work-adjustments.js +105 -0
  38. package/dist/server/services/calendar-runtime.js +1301 -0
  39. package/dist/server/services/context.js +16 -6
  40. package/dist/server/services/dashboard.js +6 -3
  41. package/dist/server/services/entity-crud.js +116 -3
  42. package/dist/server/services/gamification.js +66 -18
  43. package/dist/server/services/insights.js +2 -1
  44. package/dist/server/services/projects.js +32 -8
  45. package/dist/server/services/reviews.js +17 -2
  46. package/dist/server/services/work-time.js +27 -0
  47. package/dist/server/types.js +1069 -45
  48. package/openclaw.plugin.json +1 -1
  49. package/package.json +1 -1
  50. package/server/migrations/003_habits.sql +30 -0
  51. package/server/migrations/004_habit_links.sql +8 -0
  52. package/server/migrations/005_habit_psyche_links.sql +24 -0
  53. package/server/migrations/006_work_adjustments.sql +14 -0
  54. package/server/migrations/007_weekly_review_closures.sql +17 -0
  55. package/server/migrations/008_calendar_execution.sql +147 -0
  56. package/server/migrations/009_true_calendar_events.sql +195 -0
  57. package/server/migrations/010_calendar_selection_state.sql +6 -0
  58. package/server/migrations/011_calendar_timezone_backfill.sql +11 -0
  59. package/server/migrations/012_work_block_ranges.sql +7 -0
  60. package/server/migrations/013_microsoft_local_auth_settings.sql +8 -0
  61. package/server/migrations/014_note_tags_and_ephemeral.sql +8 -0
  62. package/skills/forge-openclaw/SKILL.md +130 -10
  63. package/skills/forge-openclaw/cron_jobs.md +395 -0
  64. package/dist/assets/index-BWtLtXwb.js +0 -36
  65. package/dist/assets/index-BWtLtXwb.js.map +0 -1
  66. package/dist/assets/index-Dp5GXY_z.css +0 -1
  67. package/dist/assets/vendor-QBH6qVEe.js.map +0 -1
@@ -1,5 +1,7 @@
1
1
  import { listActivityEvents } from "../repositories/activity-events.js";
2
2
  import { listGoals } from "../repositories/goals.js";
3
+ import { listHabits } from "../repositories/habits.js";
4
+ import { listRewardLedger } from "../repositories/rewards.js";
3
5
  import { listTags, listTagsByIds } from "../repositories/tags.js";
4
6
  import { listTasks } from "../repositories/tasks.js";
5
7
  import { getDashboard } from "./dashboard.js";
@@ -109,6 +111,7 @@ export function getOverviewContext(now = new Date()) {
109
111
  const dashboard = getDashboard();
110
112
  const focusTasks = dashboard.tasks.filter((task) => task.status === "focus" || task.status === "in_progress").length;
111
113
  const overdueTasks = dashboard.tasks.filter((task) => task.status !== "done" && task.dueDate !== null && task.dueDate < now.toISOString().slice(0, 10)).length;
114
+ const dueHabits = dashboard.habits.filter((habit) => habit.dueToday).slice(0, 6);
112
115
  return overviewContextSchema.parse({
113
116
  generatedAt: now.toISOString(),
114
117
  strategicHeader: {
@@ -124,8 +127,9 @@ export function getOverviewContext(now = new Date()) {
124
127
  projects: dashboard.projects.slice(0, 5),
125
128
  activeGoals: dashboard.goals.filter((goal) => goal.status === "active").slice(0, 6),
126
129
  topTasks: sortStrategicTasks(dashboard.tasks.filter((task) => task.status !== "done")).slice(0, 6),
130
+ dueHabits,
127
131
  recentEvidence: listActivityEvents({ limit: 12 }),
128
- achievements: buildAchievementSignals(listGoals(), listTasks(), now),
132
+ achievements: buildAchievementSignals(listGoals(), listTasks(), listHabits(), now),
129
133
  domainBalance: buildDomainBalance(listGoals(), listTasks()),
130
134
  neglectedGoals: buildNeglectedGoals(listGoals(), listTasks(), now)
131
135
  });
@@ -133,10 +137,12 @@ export function getOverviewContext(now = new Date()) {
133
137
  export function getTodayContext(now = new Date()) {
134
138
  const goals = listGoals();
135
139
  const tasks = listTasks();
136
- const gamification = buildGamificationProfile(goals, tasks, now);
140
+ const habits = listHabits();
141
+ const gamification = buildGamificationProfile(goals, tasks, habits, now);
137
142
  const inProgressTasks = sortStrategicTasks(tasks.filter((task) => task.status === "in_progress")).slice(0, 4);
138
143
  const readyTasks = sortStrategicTasks(tasks.filter((task) => task.status === "focus" || task.status === "backlog")).slice(0, 4);
139
144
  const deferredTasks = sortStrategicTasks(tasks.filter((task) => task.status === "blocked")).slice(0, 4);
145
+ const dueHabits = habits.filter((habit) => habit.dueToday).slice(0, 6);
140
146
  const completedTasks = [...tasks]
141
147
  .filter((task) => task.status === "done" && task.completedAt !== null)
142
148
  .sort((left, right) => Date.parse(right.completedAt ?? "") - Date.parse(left.completedAt ?? ""))
@@ -185,13 +191,17 @@ export function getTodayContext(now = new Date()) {
185
191
  completed: false
186
192
  }
187
193
  ],
188
- milestoneRewards: buildMilestoneRewards(goals, tasks, now),
194
+ dueHabits,
195
+ milestoneRewards: buildMilestoneRewards(goals, tasks, habits, now),
196
+ recentHabitRewards: listRewardLedger({ entityType: "habit", limit: 8 }),
189
197
  momentum: {
190
198
  streakDays: gamification.streakDays,
191
199
  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."
200
+ recoveryHint: dueHabits.length > 0
201
+ ? `${dueHabits.length} habit${dueHabits.length === 1 ? "" : "s"} still need a check-in today. Closing one will keep momentum honest.`
202
+ : overdueCount > 0
203
+ ? `Clear ${overdueCount} overdue task${overdueCount === 1 ? "" : "s"} to keep momentum from decaying.`
204
+ : "No overdue drag right now. Preserve the rhythm with one decisive completion."
195
205
  }
196
206
  });
197
207
  }
@@ -1,5 +1,6 @@
1
1
  import { listGoals } from "../repositories/goals.js";
2
2
  import { listActivityEvents } from "../repositories/activity-events.js";
3
+ import { listHabits } from "../repositories/habits.js";
3
4
  import { buildNotesSummaryByEntity } from "../repositories/notes.js";
4
5
  import { listTagsByIds, listTags } from "../repositories/tags.js";
5
6
  import { listTasks } from "../repositories/tasks.js";
@@ -116,6 +117,7 @@ function buildGoalSummary(tasks, goalId) {
116
117
  export function getDashboard() {
117
118
  const goals = listGoals();
118
119
  const tasks = listTasks();
120
+ const habits = listHabits();
119
121
  const tags = listTags();
120
122
  const now = new Date();
121
123
  const weekStart = startOfWeek(now).toISOString();
@@ -150,9 +152,9 @@ export function getDashboard() {
150
152
  const suggestedTags = tags.filter((tag) => ["value", "execution"].includes(tag.kind)).slice(0, 6);
151
153
  const owners = [...new Set(tasks.map((task) => task.owner).filter(Boolean))].sort((left, right) => left.localeCompare(right));
152
154
  const executionBuckets = buildExecutionBuckets(tasks, todayIso, weekEndIso);
153
- const gamification = buildGamificationProfile(goals, tasks, now);
154
- const achievements = buildAchievementSignals(goals, tasks, now);
155
- const milestoneRewards = buildMilestoneRewards(goals, tasks, now);
155
+ const gamification = buildGamificationProfile(goals, tasks, habits, now);
156
+ const achievements = buildAchievementSignals(goals, tasks, habits, now);
157
+ const milestoneRewards = buildMilestoneRewards(goals, tasks, habits, now);
156
158
  const recentActivity = listActivityEvents({ limit: 12 });
157
159
  const notesSummaryByEntity = buildNotesSummaryByEntity();
158
160
  return dashboardPayloadSchema.parse({
@@ -160,6 +162,7 @@ export function getDashboard() {
160
162
  goals: goalCards,
161
163
  projects,
162
164
  tasks,
165
+ habits,
163
166
  tags,
164
167
  suggestedTags,
165
168
  owners,
@@ -1,14 +1,20 @@
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";
6
7
  import { createGoal, deleteGoal, getGoalById, listGoals, updateGoal } from "../repositories/goals.js";
8
+ import { createHabit, deleteHabit, getHabitById, listHabits, updateHabit } from "../repositories/habits.js";
7
9
  import { createBehavior, createBehaviorPattern, createBeliefEntry, createEmotionDefinition, createEventType, createModeGuideSession, createModeProfile, createPsycheValue, createTriggerReport, deleteBehavior, deleteBehaviorPattern, deleteBeliefEntry, deleteEmotionDefinition, deleteEventType, deleteModeGuideSession, deleteModeProfile, deletePsycheValue, deleteTriggerReport, getBehaviorById, getBehaviorPatternById, getBeliefEntryById, getEmotionDefinitionById, getEventTypeById, getModeGuideSessionById, getModeProfileById, getPsycheValueById, getTriggerReportById, listBehaviors, listBehaviorPatterns, listBeliefEntries, listEmotionDefinitions, listEventTypes, listModeGuideSessions, listModeProfiles, listPsycheValues, listTriggerReports, updateBehavior, updateBehaviorPattern, updateBeliefEntry, updateEmotionDefinition, updateEventType, updateModeGuideSession, updateModeProfile, updatePsycheValue, updateTriggerReport } from "../repositories/psyche.js";
8
10
  import { createProject, deleteProject, getProjectById, listProjects, updateProject } from "../repositories/projects.js";
9
11
  import { createTag, deleteTag, getTagById, listTags, updateTag } from "../repositories/tags.js";
10
12
  import { createTask, deleteTask, getTaskById, listTasks, updateTask } from "../repositories/tasks.js";
11
- import { createGoalSchema, createInsightSchema, createNoteSchema, createProjectSchema, createTagSchema, createTaskSchema, updateGoalSchema, 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
+ };
12
18
  class AtomicBatchRollback extends Error {
13
19
  index;
14
20
  code;
@@ -25,6 +31,8 @@ const CRUD_ENTITY_CAPABILITIES = {
25
31
  goal: {
26
32
  entityType: "goal",
27
33
  routeBase: "/api/v1/goals",
34
+ deleteMode: "soft_default",
35
+ inBin: true,
28
36
  list: () => listGoals(),
29
37
  get: (id) => getGoalById(id),
30
38
  create: (data, context) => createGoal(data, context),
@@ -34,6 +42,8 @@ const CRUD_ENTITY_CAPABILITIES = {
34
42
  project: {
35
43
  entityType: "project",
36
44
  routeBase: "/api/v1/projects",
45
+ deleteMode: "soft_default",
46
+ inBin: true,
37
47
  list: () => listProjects(),
38
48
  get: (id) => getProjectById(id),
39
49
  create: (data, context) => createProject(data, context),
@@ -43,15 +53,30 @@ const CRUD_ENTITY_CAPABILITIES = {
43
53
  task: {
44
54
  entityType: "task",
45
55
  routeBase: "/api/v1/tasks",
56
+ deleteMode: "soft_default",
57
+ inBin: true,
46
58
  list: () => listTasks(),
47
59
  get: (id) => getTaskById(id),
48
60
  create: (data, context) => createTask(data, context),
49
61
  update: (id, patch, context) => updateTask(id, patch, context),
50
62
  hardDelete: (id, context) => deleteTask(id, context)
51
63
  },
64
+ habit: {
65
+ entityType: "habit",
66
+ routeBase: "/api/v1/habits",
67
+ deleteMode: "soft_default",
68
+ inBin: true,
69
+ list: () => listHabits(),
70
+ get: (id) => getHabitById(id),
71
+ create: (data, context) => createHabit(data, context),
72
+ update: (id, patch, context) => updateHabit(id, patch, context),
73
+ hardDelete: (id, context) => deleteHabit(id, context)
74
+ },
52
75
  tag: {
53
76
  entityType: "tag",
54
77
  routeBase: "/api/v1/tags",
78
+ deleteMode: "soft_default",
79
+ inBin: true,
55
80
  list: () => listTags(),
56
81
  get: (id) => getTagById(id),
57
82
  create: (data, context) => createTag(data, context),
@@ -61,6 +86,8 @@ const CRUD_ENTITY_CAPABILITIES = {
61
86
  note: {
62
87
  entityType: "note",
63
88
  routeBase: "/api/v1/notes",
89
+ deleteMode: "soft_default",
90
+ inBin: true,
64
91
  list: () => listNotes(),
65
92
  get: (id) => getNoteById(id),
66
93
  create: (data, context) => createNote(data, context),
@@ -70,15 +97,52 @@ const CRUD_ENTITY_CAPABILITIES = {
70
97
  insight: {
71
98
  entityType: "insight",
72
99
  routeBase: "/api/v1/insights",
100
+ deleteMode: "soft_default",
101
+ inBin: true,
73
102
  list: () => listInsights(),
74
103
  get: (id) => getInsightById(id),
75
104
  create: (data, context) => createInsight(data, context),
76
105
  update: (id, patch, context) => updateInsight(id, patch, context),
77
106
  hardDelete: (id, context) => deleteInsight(id, context)
78
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
+ },
79
141
  psyche_value: {
80
142
  entityType: "psyche_value",
81
143
  routeBase: "/api/v1/psyche/values",
144
+ deleteMode: "soft_default",
145
+ inBin: true,
82
146
  list: () => listPsycheValues(),
83
147
  get: (id) => getPsycheValueById(id),
84
148
  create: (data, context) => createPsycheValue(data, context),
@@ -88,6 +152,8 @@ const CRUD_ENTITY_CAPABILITIES = {
88
152
  behavior_pattern: {
89
153
  entityType: "behavior_pattern",
90
154
  routeBase: "/api/v1/psyche/patterns",
155
+ deleteMode: "soft_default",
156
+ inBin: true,
91
157
  list: () => listBehaviorPatterns(),
92
158
  get: (id) => getBehaviorPatternById(id),
93
159
  create: (data, context) => createBehaviorPattern(data, context),
@@ -97,6 +163,8 @@ const CRUD_ENTITY_CAPABILITIES = {
97
163
  behavior: {
98
164
  entityType: "behavior",
99
165
  routeBase: "/api/v1/psyche/behaviors",
166
+ deleteMode: "soft_default",
167
+ inBin: true,
100
168
  list: () => listBehaviors(),
101
169
  get: (id) => getBehaviorById(id),
102
170
  create: (data, context) => createBehavior(data, context),
@@ -106,6 +174,8 @@ const CRUD_ENTITY_CAPABILITIES = {
106
174
  belief_entry: {
107
175
  entityType: "belief_entry",
108
176
  routeBase: "/api/v1/psyche/beliefs",
177
+ deleteMode: "soft_default",
178
+ inBin: true,
109
179
  list: () => listBeliefEntries(),
110
180
  get: (id) => getBeliefEntryById(id),
111
181
  create: (data, context) => createBeliefEntry(data, context),
@@ -115,6 +185,8 @@ const CRUD_ENTITY_CAPABILITIES = {
115
185
  mode_profile: {
116
186
  entityType: "mode_profile",
117
187
  routeBase: "/api/v1/psyche/modes",
188
+ deleteMode: "soft_default",
189
+ inBin: true,
118
190
  list: () => listModeProfiles(),
119
191
  get: (id) => getModeProfileById(id),
120
192
  create: (data, context) => createModeProfile(data, context),
@@ -124,6 +196,8 @@ const CRUD_ENTITY_CAPABILITIES = {
124
196
  mode_guide_session: {
125
197
  entityType: "mode_guide_session",
126
198
  routeBase: "/api/v1/psyche/mode-guides",
199
+ deleteMode: "soft_default",
200
+ inBin: true,
127
201
  list: () => listModeGuideSessions(200),
128
202
  get: (id) => getModeGuideSessionById(id),
129
203
  create: (data, context) => createModeGuideSession(data, context),
@@ -133,6 +207,8 @@ const CRUD_ENTITY_CAPABILITIES = {
133
207
  event_type: {
134
208
  entityType: "event_type",
135
209
  routeBase: "/api/v1/psyche/event-types",
210
+ deleteMode: "soft_default",
211
+ inBin: true,
136
212
  list: () => listEventTypes(),
137
213
  get: (id) => getEventTypeById(id),
138
214
  create: (data, context) => createEventType(data, context),
@@ -142,6 +218,8 @@ const CRUD_ENTITY_CAPABILITIES = {
142
218
  emotion_definition: {
143
219
  entityType: "emotion_definition",
144
220
  routeBase: "/api/v1/psyche/emotions",
221
+ deleteMode: "soft_default",
222
+ inBin: true,
145
223
  list: () => listEmotionDefinitions(),
146
224
  get: (id) => getEmotionDefinitionById(id),
147
225
  create: (data, context) => createEmotionDefinition(data, context),
@@ -151,6 +229,8 @@ const CRUD_ENTITY_CAPABILITIES = {
151
229
  trigger_report: {
152
230
  entityType: "trigger_report",
153
231
  routeBase: "/api/v1/psyche/reports",
232
+ deleteMode: "soft_default",
233
+ inBin: true,
154
234
  list: () => listTriggerReports(200),
155
235
  get: (id) => getTriggerReportById(id),
156
236
  create: (data, context) => createTriggerReport(data, context),
@@ -163,8 +243,8 @@ export function getCrudEntityCapabilityMatrix() {
163
243
  entityType: capability.entityType,
164
244
  routeBase: capability.routeBase,
165
245
  pluginExposed: true,
166
- deleteMode: "soft_default",
167
- inBin: true
246
+ deleteMode: capability.deleteMode,
247
+ inBin: capability.inBin
168
248
  }));
169
249
  }
170
250
  function getCapability(entityType) {
@@ -174,9 +254,13 @@ const CREATE_ENTITY_SCHEMAS = {
174
254
  goal: createGoalSchema,
175
255
  project: createProjectSchema,
176
256
  task: createTaskSchema,
257
+ habit: createHabitSchema,
177
258
  tag: createTagSchema,
178
259
  note: createNoteSchema,
179
260
  insight: createInsightSchema,
261
+ calendar_event: createCalendarEventSchema,
262
+ work_block_template: createWorkBlockTemplateSchema,
263
+ task_timebox: createTaskTimeboxSchema,
180
264
  psyche_value: createPsycheValueSchema,
181
265
  behavior_pattern: createBehaviorPatternSchema,
182
266
  behavior: createBehaviorSchema,
@@ -191,9 +275,13 @@ const UPDATE_ENTITY_SCHEMAS = {
191
275
  goal: updateGoalSchema,
192
276
  project: updateProjectSchema,
193
277
  task: updateTaskSchema,
278
+ habit: updateHabitSchema,
194
279
  tag: updateTagSchema,
195
280
  note: updateNoteSchema,
196
281
  insight: updateInsightSchema,
282
+ calendar_event: updateCalendarEventSchema,
283
+ work_block_template: updateWorkBlockTemplateSchema,
284
+ task_timebox: updateTaskTimeboxSchema,
197
285
  psyche_value: updatePsycheValueSchema,
198
286
  behavior_pattern: updateBehaviorPatternSchema,
199
287
  behavior: updateBehaviorSchema,
@@ -312,6 +400,16 @@ function matchesLinkedTo(entityType, entity, linkedTo) {
312
400
  return linkedTo.entityType === "goal" && entity.goalId === linkedTo.id;
313
401
  case "task":
314
402
  return (linkedTo.entityType === "goal" && entity.goalId === linkedTo.id) || (linkedTo.entityType === "project" && entity.projectId === linkedTo.id);
403
+ case "habit":
404
+ return ((linkedTo.entityType === "goal" && Array.isArray(entity.linkedGoalIds) && entity.linkedGoalIds.includes(linkedTo.id)) ||
405
+ (linkedTo.entityType === "project" && Array.isArray(entity.linkedProjectIds) && entity.linkedProjectIds.includes(linkedTo.id)) ||
406
+ (linkedTo.entityType === "task" && Array.isArray(entity.linkedTaskIds) && entity.linkedTaskIds.includes(linkedTo.id)) ||
407
+ (linkedTo.entityType === "psyche_value" && Array.isArray(entity.linkedValueIds) && entity.linkedValueIds.includes(linkedTo.id)) ||
408
+ (linkedTo.entityType === "behavior_pattern" && Array.isArray(entity.linkedPatternIds) && entity.linkedPatternIds.includes(linkedTo.id)) ||
409
+ (linkedTo.entityType === "behavior" && Array.isArray(entity.linkedBehaviorIds) && entity.linkedBehaviorIds.includes(linkedTo.id)) ||
410
+ (linkedTo.entityType === "belief_entry" && Array.isArray(entity.linkedBeliefIds) && entity.linkedBeliefIds.includes(linkedTo.id)) ||
411
+ (linkedTo.entityType === "mode_profile" && Array.isArray(entity.linkedModeIds) && entity.linkedModeIds.includes(linkedTo.id)) ||
412
+ (linkedTo.entityType === "trigger_report" && Array.isArray(entity.linkedReportIds) && entity.linkedReportIds.includes(linkedTo.id)));
315
413
  case "note":
316
414
  return (Array.isArray(entity.links) &&
317
415
  entity.links.some((link) => typeof link === "object" &&
@@ -322,6 +420,17 @@ function matchesLinkedTo(entityType, entity, linkedTo) {
322
420
  link.entityId === linkedTo.id));
323
421
  case "insight":
324
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));
325
434
  case "psyche_value":
326
435
  return ((linkedTo.entityType === "goal" && Array.isArray(entity.linkedGoalIds) && entity.linkedGoalIds.includes(linkedTo.id)) ||
327
436
  (linkedTo.entityType === "project" && Array.isArray(entity.linkedProjectIds) && entity.linkedProjectIds.includes(linkedTo.id)) ||
@@ -390,6 +499,10 @@ function purgeAnchoredCollaboration(entityType, entityId) {
390
499
  export function deleteEntity(entityType, id, options, context) {
391
500
  const capability = getCapability(entityType);
392
501
  const mode = options.mode ?? "soft";
502
+ if (capability.deleteMode === "immediate") {
503
+ clearDeletedEntityRecord(entityType, id);
504
+ return capability.hardDelete(id, context);
505
+ }
393
506
  const existing = capability.get(id);
394
507
  if (!existing) {
395
508
  const deleted = getDeletedEntityRecord(entityType, id);
@@ -12,9 +12,15 @@ function startOfWeek(date) {
12
12
  function dayKey(isoDate) {
13
13
  return isoDate.slice(0, 10);
14
14
  }
15
- function calculateStreak(tasks, now) {
16
- const completedDays = new Set(tasks
17
- .flatMap((task) => (task.status === "done" && task.completedAt !== null ? [dayKey(task.completedAt)] : [])));
15
+ function isAlignedHabitCheckIn(habit, checkIn) {
16
+ return (habit.polarity === "positive" && checkIn.status === "done") || (habit.polarity === "negative" && checkIn.status === "missed");
17
+ }
18
+ function calculateStreak(tasks, habits, now) {
19
+ const completedDays = new Set([
20
+ ...tasks
21
+ .flatMap((task) => (task.status === "done" && task.completedAt !== null ? [dayKey(task.completedAt)] : [])),
22
+ ...habits.flatMap((habit) => habit.checkIns.filter((checkIn) => isAlignedHabitCheckIn(habit, checkIn)).map((checkIn) => checkIn.dateKey))
23
+ ]);
18
24
  if (completedDays.size === 0) {
19
25
  return 0;
20
26
  }
@@ -41,17 +47,27 @@ function latestCompletionForTasks(tasks) {
41
47
  .flatMap((task) => (task.completedAt ? [task.completedAt] : []))
42
48
  .sort((left, right) => Date.parse(right) - Date.parse(left))[0] ?? null;
43
49
  }
44
- export function buildGamificationProfile(goals, tasks, now = new Date()) {
50
+ function latestAlignedHabitAt(habits) {
51
+ return habits
52
+ .flatMap((habit) => habit.checkIns
53
+ .filter((checkIn) => isAlignedHabitCheckIn(habit, checkIn))
54
+ .map((checkIn) => checkIn.createdAt))
55
+ .sort((left, right) => Date.parse(right) - Date.parse(left))[0] ?? null;
56
+ }
57
+ export function buildGamificationProfile(goals, tasks, habits, now = new Date()) {
45
58
  const weekStart = startOfWeek(now).toISOString();
46
59
  const doneTasks = tasks.filter((task) => task.status === "done");
47
60
  const totalXp = getTotalXp();
48
61
  const weeklyXp = getWeeklyXp(weekStart);
49
62
  const focusTasks = tasks.filter((task) => task.status === "focus" || task.status === "in_progress").length;
50
63
  const overdueTasks = tasks.filter((task) => task.status !== "done" && task.dueDate !== null && task.dueDate < now.toISOString().slice(0, 10)).length;
64
+ const dueHabits = habits.filter((habit) => habit.dueToday).length;
65
+ const alignedHabitCheckIns = habits.flatMap((habit) => habit.checkIns.filter((checkIn) => isAlignedHabitCheckIn(habit, checkIn)));
66
+ const habitMomentum = habits.reduce((sum, habit) => sum + habit.streakCount * 3 + (habit.dueToday ? -4 : 2), 0);
51
67
  const alignedDonePoints = doneTasks
52
68
  .filter((task) => task.goalId !== null && task.tagIds.length > 0)
53
69
  .reduce((sum, task) => sum + task.points, 0);
54
- const streakDays = calculateStreak(tasks, now);
70
+ const streakDays = calculateStreak(tasks, habits, now);
55
71
  const levelState = calculateLevel(totalXp);
56
72
  const goalScores = goals
57
73
  .map((goal) => ({
@@ -69,17 +85,20 @@ export function buildGamificationProfile(goals, tasks, now = new Date()) {
69
85
  weeklyXp,
70
86
  streakDays,
71
87
  comboMultiplier: Number((1 + Math.min(0.75, streakDays * 0.05)).toFixed(2)),
72
- momentumScore: Math.max(0, Math.min(100, Math.round(weeklyXp / 6 + alignedDonePoints / 20 + focusTasks * 5 - overdueTasks * 9))),
88
+ momentumScore: Math.max(0, Math.min(100, Math.round(weeklyXp / 6 + alignedDonePoints / 20 + focusTasks * 5 + alignedHabitCheckIns.length * 4 + habitMomentum - overdueTasks * 9 - dueHabits * 3))),
73
89
  topGoalId: topGoal?.goalId ?? null,
74
90
  topGoalTitle: topGoal?.goalTitle ?? null
75
91
  });
76
92
  }
77
- export function buildAchievementSignals(goals, tasks, now = new Date()) {
78
- const profile = buildGamificationProfile(goals, tasks, now);
93
+ export function buildAchievementSignals(goals, tasks, habits, now = new Date()) {
94
+ const profile = buildGamificationProfile(goals, tasks, habits, now);
79
95
  const doneTasks = tasks.filter((task) => task.status === "done");
80
96
  const alignedDoneTasks = doneTasks.filter((task) => task.goalId !== null && task.tagIds.length > 0);
81
97
  const focusTasks = tasks.filter((task) => task.status === "focus" || task.status === "in_progress");
82
98
  const highValueGoals = goals.filter((goal) => doneTasks.some((task) => task.goalId === goal.id));
99
+ const alignedHabitCount = habits.reduce((sum, habit) => sum + habit.checkIns.filter((checkIn) => isAlignedHabitCheckIn(habit, checkIn)).length, 0);
100
+ const topHabitStreak = Math.max(0, ...habits.map((habit) => habit.streakCount));
101
+ const latestHabitWin = latestAlignedHabitAt(habits);
83
102
  return [
84
103
  {
85
104
  id: "streak-operator",
@@ -125,15 +144,34 @@ export function buildAchievementSignals(goals, tasks, now = new Date()) {
125
144
  progressLabel: `${Math.min(focusTasks.length, 1)}/1 live directives`,
126
145
  unlocked: focusTasks.length > 0,
127
146
  unlockedAt: focusTasks.length > 0 ? now.toISOString() : null
147
+ },
148
+ {
149
+ id: "habit-keeper",
150
+ title: "Habit Keeper",
151
+ summary: "Turn recurring behavior into visible operating evidence.",
152
+ tier: alignedHabitCount >= 12 ? "gold" : alignedHabitCount >= 6 ? "silver" : "bronze",
153
+ progressLabel: `${Math.min(alignedHabitCount, 12)}/12 aligned habit wins`,
154
+ unlocked: alignedHabitCount >= 12,
155
+ unlockedAt: alignedHabitCount >= 12 ? latestHabitWin : null
156
+ },
157
+ {
158
+ id: "ritual-pressure",
159
+ title: "Ritual Pressure",
160
+ summary: "Keep one habit alive long enough that it changes the texture of the week.",
161
+ tier: topHabitStreak >= 10 ? "gold" : "silver",
162
+ progressLabel: `${Math.min(topHabitStreak, 10)}/10 habit streak`,
163
+ unlocked: topHabitStreak >= 10,
164
+ unlockedAt: topHabitStreak >= 10 ? latestHabitWin : null
128
165
  }
129
166
  ].map((achievement) => achievementSignalSchema.parse(achievement));
130
167
  }
131
- export function buildMilestoneRewards(goals, tasks, now = new Date()) {
132
- const profile = buildGamificationProfile(goals, tasks, now);
168
+ export function buildMilestoneRewards(goals, tasks, habits, now = new Date()) {
169
+ const profile = buildGamificationProfile(goals, tasks, habits, now);
133
170
  const doneTasks = tasks.filter((task) => task.status === "done");
134
171
  const topGoal = profile.topGoalId ? goals.find((goal) => goal.id === profile.topGoalId) ?? null : null;
135
172
  const topGoalXp = topGoal ? doneTasks.filter((task) => task.goalId === topGoal.id).reduce((sum, task) => sum + task.points, 0) : 0;
136
173
  const completedToday = doneTasks.filter((task) => task.completedAt?.slice(0, 10) === now.toISOString().slice(0, 10)).length;
174
+ const alignedHabitCount = habits.reduce((sum, habit) => sum + habit.checkIns.filter((checkIn) => isAlignedHabitCheckIn(habit, checkIn)).length, 0);
137
175
  return [
138
176
  {
139
177
  id: "next-level",
@@ -174,13 +212,23 @@ export function buildMilestoneRewards(goals, tasks, now = new Date()) {
174
212
  current: topGoal ? topGoalXp : 0,
175
213
  target: topGoal ? topGoal.targetPoints : 1,
176
214
  completed: topGoal ? topGoalXp >= topGoal.targetPoints : false
215
+ },
216
+ {
217
+ id: "habit-mass",
218
+ title: "Habit mass threshold",
219
+ summary: "Make recurring behavior part of the same reward engine as tasks and projects.",
220
+ rewardLabel: "Consistency cache +75 xp",
221
+ progressLabel: `${Math.min(alignedHabitCount, 14)}/14 aligned habit check-ins`,
222
+ current: alignedHabitCount,
223
+ target: 14,
224
+ completed: alignedHabitCount >= 14
177
225
  }
178
226
  ].map((reward) => milestoneRewardSchema.parse(reward));
179
227
  }
180
- export function buildXpMomentumPulse(goals, tasks, now = new Date()) {
181
- const profile = buildGamificationProfile(goals, tasks, now);
182
- const achievements = buildAchievementSignals(goals, tasks, now);
183
- const milestoneRewards = buildMilestoneRewards(goals, tasks, now);
228
+ export function buildXpMomentumPulse(goals, tasks, habits, now = new Date()) {
229
+ const profile = buildGamificationProfile(goals, tasks, habits, now);
230
+ const achievements = buildAchievementSignals(goals, tasks, habits, now);
231
+ const milestoneRewards = buildMilestoneRewards(goals, tasks, habits, now);
184
232
  const nextMilestone = milestoneRewards.find((reward) => !reward.completed) ?? milestoneRewards[0] ?? null;
185
233
  const unlockedAchievements = achievements.filter((achievement) => achievement.unlocked).length;
186
234
  const status = profile.momentumScore >= 80 ? "surging" : profile.momentumScore >= 60 ? "steady" : "recovering";
@@ -206,10 +254,10 @@ export function buildXpMomentumPulse(goals, tasks, now = new Date()) {
206
254
  nextMilestoneLabel: nextMilestone?.rewardLabel ?? "Keep building visible momentum"
207
255
  };
208
256
  }
209
- export function buildGamificationOverview(goals, tasks, now = new Date()) {
257
+ export function buildGamificationOverview(goals, tasks, habits, now = new Date()) {
210
258
  return gamificationOverviewSchema.parse({
211
- profile: buildGamificationProfile(goals, tasks, now),
212
- achievements: buildAchievementSignals(goals, tasks, now),
213
- milestoneRewards: buildMilestoneRewards(goals, tasks, now)
259
+ profile: buildGamificationProfile(goals, tasks, habits, now),
260
+ achievements: buildAchievementSignals(goals, tasks, habits, now),
261
+ milestoneRewards: buildMilestoneRewards(goals, tasks, habits, now)
214
262
  });
215
263
  }
@@ -1,6 +1,7 @@
1
1
  import { listActivityEvents } from "../repositories/activity-events.js";
2
2
  import { listInsights } from "../repositories/collaboration.js";
3
3
  import { listGoals } from "../repositories/goals.js";
4
+ import { listHabits } from "../repositories/habits.js";
4
5
  import { listTasks } from "../repositories/tasks.js";
5
6
  import { getOverviewContext } from "./context.js";
6
7
  import { buildGamificationProfile } from "./gamification.js";
@@ -33,7 +34,7 @@ function buildHeatmap(tasks, now) {
33
34
  export function getInsightsPayload(now = new Date()) {
34
35
  const goals = listGoals();
35
36
  const tasks = listTasks();
36
- const gamification = buildGamificationProfile(goals, tasks, now);
37
+ const gamification = buildGamificationProfile(goals, tasks, listHabits(), now);
37
38
  const overview = getOverviewContext(now);
38
39
  const activity = listActivityEvents({ limit: 60 });
39
40
  const trends = Array.from({ length: 6 }, (_, offset) => {
@@ -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
  }