forge-openclaw-plugin 0.2.4 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +113 -5
  2. package/dist/assets/board-CzgvdLO8.js +6 -0
  3. package/dist/assets/board-CzgvdLO8.js.map +1 -0
  4. package/dist/assets/favicon-BCHm9dUV.ico +0 -0
  5. package/dist/assets/index-8d_oM8fL.js +27 -0
  6. package/dist/assets/index-8d_oM8fL.js.map +1 -0
  7. package/dist/assets/index-D4A_bq8m.css +1 -0
  8. package/dist/assets/motion-STUd1O46.js +10 -0
  9. package/dist/assets/motion-STUd1O46.js.map +1 -0
  10. package/dist/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
  11. package/dist/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
  12. package/dist/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
  13. package/dist/assets/sora-latin-ext-wght-normal-CawQDOvP.woff2 +0 -0
  14. package/dist/assets/sora-latin-wght-normal-DdqRvwsR.woff2 +0 -0
  15. package/dist/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
  16. package/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
  17. package/dist/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
  18. package/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
  19. package/dist/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
  20. package/dist/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
  21. package/dist/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
  22. package/dist/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
  23. package/dist/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
  24. package/dist/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
  25. package/dist/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
  26. package/dist/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
  27. package/dist/assets/table-CtNlETLc.js +23 -0
  28. package/dist/assets/table-CtNlETLc.js.map +1 -0
  29. package/dist/assets/ui-ThzkR_oW.js +46 -0
  30. package/dist/assets/ui-ThzkR_oW.js.map +1 -0
  31. package/dist/assets/vendor-CRS-psbw.css +1 -0
  32. package/dist/assets/vendor-DyHAI6nk.js +423 -0
  33. package/dist/assets/vendor-DyHAI6nk.js.map +1 -0
  34. package/dist/assets/viz-BJuBCz_G.js +34 -0
  35. package/dist/assets/viz-BJuBCz_G.js.map +1 -0
  36. package/dist/favicon.ico +0 -0
  37. package/dist/favicon.png +0 -0
  38. package/dist/index.html +29 -0
  39. package/dist/openclaw/api-client.d.ts +8 -0
  40. package/dist/openclaw/api-client.js +31 -4
  41. package/dist/openclaw/local-runtime.d.ts +3 -0
  42. package/dist/openclaw/local-runtime.js +135 -0
  43. package/dist/openclaw/parity.d.ts +4 -4
  44. package/dist/openclaw/parity.js +23 -33
  45. package/dist/openclaw/plugin-entry-shared.d.ts +4 -2
  46. package/dist/openclaw/plugin-entry-shared.js +51 -9
  47. package/dist/openclaw/routes.d.ts +12 -3
  48. package/dist/openclaw/routes.js +156 -924
  49. package/dist/openclaw/tools.js +242 -1100
  50. package/dist/server/app.js +2450 -0
  51. package/dist/server/db.js +313 -0
  52. package/dist/server/e2e-server.js +20 -0
  53. package/dist/server/errors.js +15 -0
  54. package/dist/server/index.js +16 -0
  55. package/dist/server/managers/base.js +17 -0
  56. package/dist/server/managers/contracts.js +47 -0
  57. package/dist/server/managers/platform/api-gateway-manager.js +11 -0
  58. package/dist/server/managers/platform/audit-manager.js +15 -0
  59. package/dist/server/managers/platform/authentication-manager.js +56 -0
  60. package/dist/server/managers/platform/authorization-manager.js +56 -0
  61. package/dist/server/managers/platform/background-job-manager.js +10 -0
  62. package/dist/server/managers/platform/configuration-manager.js +33 -0
  63. package/dist/server/managers/platform/database-manager.js +14 -0
  64. package/dist/server/managers/platform/event-bus-manager.js +7 -0
  65. package/dist/server/managers/platform/external-service-manager.js +11 -0
  66. package/dist/server/managers/platform/health-manager.js +7 -0
  67. package/dist/server/managers/platform/migration-manager.js +8 -0
  68. package/dist/server/managers/platform/search-index-manager.js +4 -0
  69. package/dist/server/managers/platform/secrets-manager.js +19 -0
  70. package/dist/server/managers/platform/session-manager.js +121 -0
  71. package/dist/server/managers/platform/storage-manager.js +16 -0
  72. package/dist/server/managers/platform/token-manager.js +37 -0
  73. package/dist/server/managers/platform/transaction-manager.js +8 -0
  74. package/dist/server/managers/platform/trusted-network.js +39 -0
  75. package/dist/server/managers/runtime.js +56 -0
  76. package/dist/server/managers/type-guards.js +4 -0
  77. package/dist/server/openapi.js +3512 -0
  78. package/dist/server/psyche-types.js +395 -0
  79. package/dist/server/repositories/activity-events.js +157 -0
  80. package/dist/server/repositories/collaboration.js +497 -0
  81. package/dist/server/repositories/comments.js +176 -0
  82. package/dist/server/repositories/deleted-entities.js +192 -0
  83. package/dist/server/repositories/domains.js +30 -0
  84. package/dist/server/repositories/event-log.js +64 -0
  85. package/dist/server/repositories/goals.js +159 -0
  86. package/dist/server/repositories/projects.js +214 -0
  87. package/dist/server/repositories/psyche.js +1356 -0
  88. package/dist/server/repositories/rewards.js +675 -0
  89. package/dist/server/repositories/settings.js +399 -0
  90. package/dist/server/repositories/tags.js +160 -0
  91. package/dist/server/repositories/task-runs.js +488 -0
  92. package/dist/server/repositories/tasks.js +413 -0
  93. package/dist/server/services/context.js +214 -0
  94. package/dist/server/services/dashboard.js +170 -0
  95. package/dist/server/services/entity-crud.js +576 -0
  96. package/dist/server/services/gamification.js +215 -0
  97. package/dist/server/services/insights.js +91 -0
  98. package/dist/server/services/projects.js +75 -0
  99. package/dist/server/services/psyche.js +63 -0
  100. package/dist/server/services/relations.js +28 -0
  101. package/dist/server/services/reviews.js +88 -0
  102. package/dist/server/services/run-recovery.js +13 -0
  103. package/dist/server/services/tagging.js +49 -0
  104. package/dist/server/services/task-run-watchdog.js +92 -0
  105. package/dist/server/services/work-time.js +176 -0
  106. package/dist/server/types.js +999 -0
  107. package/dist/server/web.js +91 -0
  108. package/openclaw.plugin.json +21 -9
  109. package/package.json +17 -4
  110. package/server/migrations/001_core.sql +333 -0
  111. package/server/migrations/002_psyche.sql +241 -0
  112. package/server/migrations/003_timer_execution.sql +18 -0
  113. package/server/migrations/004_psyche_linked_entities.sql +5 -0
  114. package/server/migrations/005_adaptive_schemas.sql +157 -0
  115. package/server/migrations/006_psyche_auth_setting.sql +4 -0
  116. package/server/migrations/007_deleted_entities.sql +16 -0
  117. package/skills/forge-openclaw/SKILL.md +189 -275
@@ -0,0 +1,215 @@
1
+ import { achievementSignalSchema, gamificationOverviewSchema, gamificationProfileSchema, milestoneRewardSchema } from "../types.js";
2
+ import { getTotalXp, getWeeklyXp } from "../repositories/rewards.js";
3
+ const XP_PER_LEVEL = 120;
4
+ function startOfWeek(date) {
5
+ const clone = new Date(date);
6
+ const day = clone.getDay();
7
+ const delta = day === 0 ? -6 : 1 - day;
8
+ clone.setDate(clone.getDate() + delta);
9
+ clone.setHours(0, 0, 0, 0);
10
+ return clone;
11
+ }
12
+ function dayKey(isoDate) {
13
+ return isoDate.slice(0, 10);
14
+ }
15
+ function calculateStreak(tasks, now) {
16
+ const completedDays = new Set(tasks
17
+ .flatMap((task) => (task.status === "done" && task.completedAt !== null ? [dayKey(task.completedAt)] : [])));
18
+ if (completedDays.size === 0) {
19
+ return 0;
20
+ }
21
+ let streak = 0;
22
+ const cursor = new Date(now);
23
+ cursor.setHours(0, 0, 0, 0);
24
+ while (completedDays.has(cursor.toISOString().slice(0, 10))) {
25
+ streak += 1;
26
+ cursor.setDate(cursor.getDate() - 1);
27
+ }
28
+ return streak;
29
+ }
30
+ function calculateLevel(totalXp) {
31
+ const level = Math.floor(totalXp / XP_PER_LEVEL) + 1;
32
+ const currentLevelFloor = (level - 1) * XP_PER_LEVEL;
33
+ return {
34
+ level,
35
+ currentLevelXp: totalXp - currentLevelFloor,
36
+ nextLevelXp: XP_PER_LEVEL
37
+ };
38
+ }
39
+ function latestCompletionForTasks(tasks) {
40
+ return tasks
41
+ .flatMap((task) => (task.completedAt ? [task.completedAt] : []))
42
+ .sort((left, right) => Date.parse(right) - Date.parse(left))[0] ?? null;
43
+ }
44
+ export function buildGamificationProfile(goals, tasks, now = new Date()) {
45
+ const weekStart = startOfWeek(now).toISOString();
46
+ const doneTasks = tasks.filter((task) => task.status === "done");
47
+ const totalXp = getTotalXp();
48
+ const weeklyXp = getWeeklyXp(weekStart);
49
+ const focusTasks = tasks.filter((task) => task.status === "focus" || task.status === "in_progress").length;
50
+ const overdueTasks = tasks.filter((task) => task.status !== "done" && task.dueDate !== null && task.dueDate < now.toISOString().slice(0, 10)).length;
51
+ const alignedDonePoints = doneTasks
52
+ .filter((task) => task.goalId !== null && task.tagIds.length > 0)
53
+ .reduce((sum, task) => sum + task.points, 0);
54
+ const streakDays = calculateStreak(tasks, now);
55
+ const levelState = calculateLevel(totalXp);
56
+ const goalScores = goals
57
+ .map((goal) => ({
58
+ goalId: goal.id,
59
+ goalTitle: goal.title,
60
+ earnedXp: doneTasks.filter((task) => task.goalId === goal.id).reduce((sum, task) => sum + task.points, 0)
61
+ }))
62
+ .sort((left, right) => right.earnedXp - left.earnedXp);
63
+ const topGoal = goalScores.find((goal) => goal.earnedXp > 0) ?? null;
64
+ return gamificationProfileSchema.parse({
65
+ totalXp,
66
+ level: levelState.level,
67
+ currentLevelXp: levelState.currentLevelXp,
68
+ nextLevelXp: levelState.nextLevelXp,
69
+ weeklyXp,
70
+ streakDays,
71
+ 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))),
73
+ topGoalId: topGoal?.goalId ?? null,
74
+ topGoalTitle: topGoal?.goalTitle ?? null
75
+ });
76
+ }
77
+ export function buildAchievementSignals(goals, tasks, now = new Date()) {
78
+ const profile = buildGamificationProfile(goals, tasks, now);
79
+ const doneTasks = tasks.filter((task) => task.status === "done");
80
+ const alignedDoneTasks = doneTasks.filter((task) => task.goalId !== null && task.tagIds.length > 0);
81
+ const focusTasks = tasks.filter((task) => task.status === "focus" || task.status === "in_progress");
82
+ const highValueGoals = goals.filter((goal) => doneTasks.some((task) => task.goalId === goal.id));
83
+ return [
84
+ {
85
+ id: "streak-operator",
86
+ title: "Streak Operator",
87
+ summary: "Maintain consecutive days of meaningful completions.",
88
+ tier: profile.streakDays >= 7 ? "gold" : "silver",
89
+ progressLabel: `${Math.min(profile.streakDays, 7)}/7 days`,
90
+ unlocked: profile.streakDays >= 7,
91
+ unlockedAt: profile.streakDays >= 7 ? latestCompletionForTasks(doneTasks) : null
92
+ },
93
+ {
94
+ id: "aligned-maker",
95
+ title: "Aligned Maker",
96
+ summary: "Complete work that is explicitly tied to a goal and tagged context.",
97
+ tier: alignedDoneTasks.length >= 5 ? "gold" : "bronze",
98
+ progressLabel: `${Math.min(alignedDoneTasks.length, 5)}/5 aligned completions`,
99
+ unlocked: alignedDoneTasks.length >= 5,
100
+ unlockedAt: alignedDoneTasks.length >= 5 ? latestCompletionForTasks(alignedDoneTasks) : null
101
+ },
102
+ {
103
+ id: "momentum-engine",
104
+ title: "Momentum Engine",
105
+ summary: "Push weekly XP high enough that momentum becomes visible.",
106
+ tier: profile.weeklyXp >= 240 ? "gold" : profile.weeklyXp >= 120 ? "silver" : "bronze",
107
+ progressLabel: `${Math.min(profile.weeklyXp, 240)}/240 weekly xp`,
108
+ unlocked: profile.weeklyXp >= 240,
109
+ unlockedAt: profile.weeklyXp >= 240 ? latestCompletionForTasks(doneTasks) : null
110
+ },
111
+ {
112
+ id: "path-keeper",
113
+ title: "Path Keeper",
114
+ summary: "Keep multiple life arcs alive instead of overfitting one lane.",
115
+ tier: highValueGoals.length >= 3 ? "platinum" : "silver",
116
+ progressLabel: `${Math.min(highValueGoals.length, 3)}/3 active arcs with wins`,
117
+ unlocked: highValueGoals.length >= 3,
118
+ unlockedAt: highValueGoals.length >= 3 ? latestCompletionForTasks(doneTasks) : null
119
+ },
120
+ {
121
+ id: "focus-lane",
122
+ title: "Focus Lane Live",
123
+ summary: "Sustain a protected execution lane instead of browsing a backlog.",
124
+ tier: focusTasks.length > 0 ? "silver" : "bronze",
125
+ progressLabel: `${Math.min(focusTasks.length, 1)}/1 live directives`,
126
+ unlocked: focusTasks.length > 0,
127
+ unlockedAt: focusTasks.length > 0 ? now.toISOString() : null
128
+ }
129
+ ].map((achievement) => achievementSignalSchema.parse(achievement));
130
+ }
131
+ export function buildMilestoneRewards(goals, tasks, now = new Date()) {
132
+ const profile = buildGamificationProfile(goals, tasks, now);
133
+ const doneTasks = tasks.filter((task) => task.status === "done");
134
+ const topGoal = profile.topGoalId ? goals.find((goal) => goal.id === profile.topGoalId) ?? null : null;
135
+ const topGoalXp = topGoal ? doneTasks.filter((task) => task.goalId === topGoal.id).reduce((sum, task) => sum + task.points, 0) : 0;
136
+ const completedToday = doneTasks.filter((task) => task.completedAt?.slice(0, 10) === now.toISOString().slice(0, 10)).length;
137
+ return [
138
+ {
139
+ id: "next-level",
140
+ title: "Next level threshold",
141
+ summary: "Keep pushing until the next level unlocks a stronger sense of ascent.",
142
+ rewardLabel: `Level ${profile.level + 1}`,
143
+ progressLabel: `${profile.currentLevelXp}/${profile.nextLevelXp} xp`,
144
+ current: profile.currentLevelXp,
145
+ target: profile.nextLevelXp,
146
+ completed: profile.currentLevelXp >= profile.nextLevelXp
147
+ },
148
+ {
149
+ id: "weekly-sprint",
150
+ title: "Weekly sprint heat",
151
+ summary: "Cross the weekly XP line that keeps the system feeling alive.",
152
+ rewardLabel: "Momentum bonus",
153
+ progressLabel: `${Math.min(profile.weeklyXp, 240)}/240 weekly xp`,
154
+ current: profile.weeklyXp,
155
+ target: 240,
156
+ completed: profile.weeklyXp >= 240
157
+ },
158
+ {
159
+ id: "daily-mass",
160
+ title: "Daily mass threshold",
161
+ summary: "Make the day feel consequential with multiple completed tasks.",
162
+ rewardLabel: "Quest chest +90 xp",
163
+ progressLabel: `${Math.min(completedToday, 3)}/3 completions today`,
164
+ current: completedToday,
165
+ target: 3,
166
+ completed: completedToday >= 3
167
+ },
168
+ {
169
+ id: "goal-project",
170
+ title: "Project reward track",
171
+ summary: topGoal ? `Keep advancing the leading life goal through a concrete project path.` : "No leading life goal is established yet.",
172
+ rewardLabel: topGoal ? `${topGoal.title} milestone` : "Establish a lead goal",
173
+ progressLabel: topGoal ? `${Math.min(topGoalXp, topGoal.targetPoints)}/${topGoal.targetPoints} goal xp` : "0/1 lead arcs",
174
+ current: topGoal ? topGoalXp : 0,
175
+ target: topGoal ? topGoal.targetPoints : 1,
176
+ completed: topGoal ? topGoalXp >= topGoal.targetPoints : false
177
+ }
178
+ ].map((reward) => milestoneRewardSchema.parse(reward));
179
+ }
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);
184
+ const nextMilestone = milestoneRewards.find((reward) => !reward.completed) ?? milestoneRewards[0] ?? null;
185
+ const unlockedAchievements = achievements.filter((achievement) => achievement.unlocked).length;
186
+ const status = profile.momentumScore >= 80 ? "surging" : profile.momentumScore >= 60 ? "steady" : "recovering";
187
+ const headline = status === "surging"
188
+ ? `${profile.streakDays}-day streak online. Forge is compounding.`
189
+ : status === "steady"
190
+ ? `Momentum is stable. One sharp push keeps the engine hot.`
191
+ : `Recovery window open. A small real win will restart the climb.`;
192
+ const detail = nextMilestone !== null
193
+ ? `${nextMilestone.title} is the clean next unlock. ${nextMilestone.progressLabel}.`
194
+ : `Level ${profile.level} is active with ${profile.weeklyXp} weekly XP already recorded.`;
195
+ const celebrationLabel = unlockedAchievements > 0
196
+ ? `${unlockedAchievements} achievement${unlockedAchievements === 1 ? "" : "s"} unlocked`
197
+ : profile.weeklyXp >= 120
198
+ ? `Weekly sprint heat is building`
199
+ : `Next celebration comes from a real completion or repair`;
200
+ return {
201
+ status,
202
+ headline,
203
+ detail,
204
+ celebrationLabel,
205
+ nextMilestoneId: nextMilestone?.id ?? null,
206
+ nextMilestoneLabel: nextMilestone?.rewardLabel ?? "Keep building visible momentum"
207
+ };
208
+ }
209
+ export function buildGamificationOverview(goals, tasks, now = new Date()) {
210
+ return gamificationOverviewSchema.parse({
211
+ profile: buildGamificationProfile(goals, tasks, now),
212
+ achievements: buildAchievementSignals(goals, tasks, now),
213
+ milestoneRewards: buildMilestoneRewards(goals, tasks, now)
214
+ });
215
+ }
@@ -0,0 +1,91 @@
1
+ import { listActivityEvents } from "../repositories/activity-events.js";
2
+ import { listInsights } from "../repositories/collaboration.js";
3
+ import { listGoals } from "../repositories/goals.js";
4
+ import { listTasks } from "../repositories/tasks.js";
5
+ import { getOverviewContext } from "./context.js";
6
+ import { buildGamificationProfile } from "./gamification.js";
7
+ import { insightsPayloadSchema } from "../types.js";
8
+ function dayKey(date) {
9
+ return date.toISOString().slice(0, 10);
10
+ }
11
+ function addDays(date, days) {
12
+ const clone = new Date(date);
13
+ clone.setDate(clone.getDate() + days);
14
+ return clone;
15
+ }
16
+ function buildHeatmap(tasks, now) {
17
+ const cells = [];
18
+ for (let index = 29; index >= 0; index -= 1) {
19
+ const current = addDays(now, -index);
20
+ const currentKey = dayKey(current);
21
+ const completed = tasks.filter((task) => task.completedAt?.slice(0, 10) === currentKey).length;
22
+ const focus = tasks.filter((task) => task.updatedAt.slice(0, 10) === currentKey && (task.status === "focus" || task.status === "in_progress")).length;
23
+ cells.push({
24
+ id: currentKey,
25
+ label: current.toLocaleDateString("en-US", { month: "short", day: "numeric" }),
26
+ completed,
27
+ focus,
28
+ intensity: Math.min(4, completed + focus)
29
+ });
30
+ }
31
+ return cells;
32
+ }
33
+ export function getInsightsPayload(now = new Date()) {
34
+ const goals = listGoals();
35
+ const tasks = listTasks();
36
+ const gamification = buildGamificationProfile(goals, tasks, now);
37
+ const overview = getOverviewContext(now);
38
+ const activity = listActivityEvents({ limit: 60 });
39
+ const trends = Array.from({ length: 6 }, (_, offset) => {
40
+ const bucketStart = addDays(now, -(5 - offset) * 5);
41
+ const bucketEnd = addDays(bucketStart, 4);
42
+ const completedTasks = tasks.filter((task) => {
43
+ const completedAt = task.completedAt;
44
+ return completedAt !== null && completedAt >= bucketStart.toISOString() && completedAt <= bucketEnd.toISOString();
45
+ });
46
+ const updatedTasks = tasks.filter((task) => task.updatedAt >= bucketStart.toISOString() && task.updatedAt <= bucketEnd.toISOString());
47
+ return {
48
+ label: bucketStart.toLocaleDateString("en-US", { month: "short", day: "numeric" }),
49
+ xp: completedTasks.reduce((sum, task) => sum + task.points, 0),
50
+ focusScore: Math.min(100, updatedTasks.length * 12 + completedTasks.length * 20)
51
+ };
52
+ });
53
+ const domainBalance = overview.domainBalance.map((domain) => ({
54
+ label: domain.label,
55
+ value: Math.min(100, domain.completedPoints + domain.activeTaskCount * 8),
56
+ color: domain.color,
57
+ note: domain.momentumLabel
58
+ }));
59
+ const hottestGoal = overview.activeGoals[0] ?? null;
60
+ const blockedTasks = tasks.filter((task) => task.status === "blocked").length;
61
+ const overdueTasks = tasks.filter((task) => task.status !== "done" && task.dueDate !== null && task.dueDate < dayKey(now)).length;
62
+ const feed = listInsights({ limit: 8 });
63
+ return insightsPayloadSchema.parse({
64
+ generatedAt: now.toISOString(),
65
+ status: {
66
+ systemStatus: gamification.momentumScore >= 80 ? "Optimal Flow" : gamification.momentumScore >= 60 ? "Stable Build" : "Needs Recovery",
67
+ streakDays: gamification.streakDays,
68
+ momentumScore: gamification.momentumScore
69
+ },
70
+ momentumHeatmap: buildHeatmap(tasks, now),
71
+ executionTrends: trends,
72
+ domainBalance,
73
+ coaching: {
74
+ title: hottestGoal ? `Protect ${hottestGoal.title}` : "Rebuild momentum",
75
+ summary: blockedTasks > 0
76
+ ? `${blockedTasks} blocked task${blockedTasks === 1 ? "" : "s"} are leaking velocity out of the system.`
77
+ : overdueTasks > 0
78
+ ? `${overdueTasks} overdue task${overdueTasks === 1 ? "" : "s"} are the main drag vector right now.`
79
+ : "Recent evidence shows enough movement to push the next arc more aggressively.",
80
+ recommendation: blockedTasks > 0
81
+ ? "Clear one blocked task before adding more new work."
82
+ : hottestGoal
83
+ ? `Queue the next decisive move for ${hottestGoal.title} and keep a deep-work lane active.`
84
+ : "Pick one life goal, one project, and one task to stabilize the next 24 hours.",
85
+ ctaLabel: "Trigger coaching insight"
86
+ },
87
+ evidenceDigest: activity.slice(0, 5),
88
+ feed,
89
+ openCount: feed.filter((insight) => insight.status === "open").length
90
+ });
91
+ }
@@ -0,0 +1,75 @@
1
+ import { listActivityEvents } from "../repositories/activity-events.js";
2
+ import { getGoalById, listGoals } from "../repositories/goals.js";
3
+ import { listProjects } from "../repositories/projects.js";
4
+ import { listTasks } from "../repositories/tasks.js";
5
+ import { emptyTaskTimeSummary } from "./work-time.js";
6
+ import { projectBoardPayloadSchema, projectSummarySchema } from "../types.js";
7
+ function projectTaskSummary(tasks) {
8
+ const completedTasks = tasks.filter((task) => task.status === "done");
9
+ const activeTasks = tasks.filter((task) => task.status !== "done");
10
+ const totalTasks = tasks.length;
11
+ const earnedPoints = completedTasks.reduce((sum, task) => sum + task.points, 0);
12
+ const progress = totalTasks === 0 ? 0 : Math.min(100, Math.round((completedTasks.length / totalTasks) * 100));
13
+ const nextTask = activeTasks.find((task) => task.status === "focus" || task.status === "in_progress") ??
14
+ activeTasks.find((task) => task.status === "backlog") ??
15
+ activeTasks[0] ??
16
+ null;
17
+ const momentumLabel = completedTasks.length === 0
18
+ ? activeTasks.length > 0
19
+ ? "Building momentum"
20
+ : "Waiting for the first task"
21
+ : activeTasks.some((task) => task.status === "blocked")
22
+ ? "Needs intervention"
23
+ : progress >= 70
24
+ ? "Closing strong"
25
+ : "Making steady progress";
26
+ return {
27
+ activeTaskCount: activeTasks.length,
28
+ completedTaskCount: completedTasks.length,
29
+ totalTasks,
30
+ earnedPoints,
31
+ progress,
32
+ nextTaskId: nextTask?.id ?? null,
33
+ nextTaskTitle: nextTask?.title ?? null,
34
+ momentumLabel,
35
+ time: tasks.reduce((summary, task) => ({
36
+ totalTrackedSeconds: summary.totalTrackedSeconds + task.time.totalTrackedSeconds,
37
+ totalCreditedSeconds: Math.round((summary.totalCreditedSeconds + task.time.totalCreditedSeconds) * 100) / 100,
38
+ activeRunCount: summary.activeRunCount + task.time.activeRunCount,
39
+ hasCurrentRun: summary.hasCurrentRun || task.time.hasCurrentRun,
40
+ currentRunId: summary.currentRunId ?? task.time.currentRunId
41
+ }), emptyTaskTimeSummary())
42
+ };
43
+ }
44
+ export function listProjectSummaries(filters = {}) {
45
+ const goals = new Map(listGoals().map((goal) => [goal.id, goal]));
46
+ const tasks = listTasks();
47
+ return listProjects(filters).map((project) => {
48
+ const goal = goals.get(project.goalId);
49
+ const projectTasks = tasks.filter((task) => task.projectId === project.id);
50
+ return projectSummarySchema.parse({
51
+ ...project,
52
+ goalTitle: goal?.title ?? "Unknown life goal",
53
+ ...projectTaskSummary(projectTasks)
54
+ });
55
+ });
56
+ }
57
+ export function getProjectSummary(projectId) {
58
+ return listProjectSummaries().find((project) => project.id === projectId);
59
+ }
60
+ export function getProjectBoard(projectId) {
61
+ const project = getProjectSummary(projectId);
62
+ if (!project) {
63
+ return undefined;
64
+ }
65
+ const goal = getGoalById(project.goalId);
66
+ if (!goal) {
67
+ return undefined;
68
+ }
69
+ return projectBoardPayloadSchema.parse({
70
+ project,
71
+ goal,
72
+ tasks: listTasks({ projectId }),
73
+ activity: listActivityEvents({ entityType: "project", entityId: projectId, limit: 20 }).concat(listActivityEvents({ entityType: "task", limit: 100 }).filter((event) => listTasks({ projectId }).some((task) => task.id === event.entityId)).slice(0, 20))
74
+ });
75
+ }
@@ -0,0 +1,63 @@
1
+ import { getDomainBySlug } from "../repositories/domains.js";
2
+ import { listComments } from "../repositories/comments.js";
3
+ import { listInsights } from "../repositories/collaboration.js";
4
+ import { listBehaviorPatterns, listBehaviors, listBeliefEntries, listModeProfiles, listPsycheValues, listSchemaCatalog, listTriggerReports } from "../repositories/psyche.js";
5
+ import { psycheOverviewPayloadSchema } from "../psyche-types.js";
6
+ const PSYCHE_ENTITY_TYPE_SET = new Set([
7
+ "psyche_value",
8
+ "behavior_pattern",
9
+ "behavior",
10
+ "belief_entry",
11
+ "mode_profile",
12
+ "trigger_report"
13
+ ]);
14
+ export function getPsycheOverview() {
15
+ const domain = getDomainBySlug("psyche");
16
+ if (!domain) {
17
+ throw new Error("Psyche domain is not available");
18
+ }
19
+ const values = listPsycheValues();
20
+ const patterns = listBehaviorPatterns();
21
+ const behaviors = listBehaviors();
22
+ const beliefs = listBeliefEntries();
23
+ const modes = listModeProfiles();
24
+ const reports = listTriggerReports(5);
25
+ const schemaCatalog = listSchemaCatalog();
26
+ const comments = listComments({ limit: 200 });
27
+ const openInsights = listInsights({ limit: 100 }).filter((insight) => insight.entityType && PSYCHE_ENTITY_TYPE_SET.has(insight.entityType)).length;
28
+ const unresolvedComments = comments.filter((comment) => PSYCHE_ENTITY_TYPE_SET.has(comment.entityType)).length;
29
+ const committedActions = [
30
+ ...values.flatMap((value) => value.committedActions),
31
+ ...behaviors.filter((behavior) => behavior.kind === "committed").map((behavior) => behavior.title),
32
+ ...reports.flatMap((report) => report.nextMoves)
33
+ ];
34
+ const schemaPressure = schemaCatalog
35
+ .filter((schema) => schema.schemaType === "maladaptive")
36
+ .map((schema) => {
37
+ const activationCount = beliefs.filter((belief) => belief.schemaId === schema.id).length +
38
+ behaviors.filter((behavior) => behavior.linkedSchemaIds.includes(schema.id)).length +
39
+ reports.filter((report) => report.schemaLinks.includes(schema.id) || report.schemaLinks.includes(schema.slug)).length;
40
+ return {
41
+ schemaId: schema.id,
42
+ title: schema.title,
43
+ activationCount
44
+ };
45
+ })
46
+ .filter((entry) => entry.activationCount > 0)
47
+ .sort((left, right) => right.activationCount - left.activationCount)
48
+ .slice(0, 6);
49
+ return psycheOverviewPayloadSchema.parse({
50
+ generatedAt: new Date().toISOString(),
51
+ domain,
52
+ values,
53
+ patterns,
54
+ behaviors,
55
+ beliefs,
56
+ modes,
57
+ reports,
58
+ schemaPressure,
59
+ openInsights,
60
+ unresolvedComments,
61
+ committedActions
62
+ });
63
+ }
@@ -0,0 +1,28 @@
1
+ import { getGoalById } from "../repositories/goals.js";
2
+ import { listTagsByIds } from "../repositories/tags.js";
3
+ import { HttpError } from "../errors.js";
4
+ function assertKnownTags(tagIds) {
5
+ if (tagIds.length === 0) {
6
+ return;
7
+ }
8
+ const knownTagIds = new Set(listTagsByIds(tagIds).map((tag) => tag.id));
9
+ const missingTagIds = [...new Set(tagIds)].filter((tagId) => !knownTagIds.has(tagId));
10
+ if (missingTagIds.length > 0) {
11
+ throw new HttpError(404, "tag_not_found", `Unknown tag ids: ${missingTagIds.join(", ")}`);
12
+ }
13
+ }
14
+ export function assertGoalExists(goalId) {
15
+ if (!goalId) {
16
+ return;
17
+ }
18
+ if (!getGoalById(goalId)) {
19
+ throw new HttpError(404, "goal_not_found", `Goal ${goalId} does not exist`);
20
+ }
21
+ }
22
+ export function assertTaskRelations(input) {
23
+ assertGoalExists(input.goalId);
24
+ assertKnownTags(input.tagIds ?? []);
25
+ }
26
+ export function assertGoalRelations(input) {
27
+ assertKnownTags(input.tagIds ?? []);
28
+ }
@@ -0,0 +1,88 @@
1
+ import { listActivityEvents } from "../repositories/activity-events.js";
2
+ import { listGoals } from "../repositories/goals.js";
3
+ import { listTasks } from "../repositories/tasks.js";
4
+ import { buildGamificationProfile } from "./gamification.js";
5
+ import { weeklyReviewPayloadSchema } from "../types.js";
6
+ function startOfWeek(date) {
7
+ const clone = new Date(date);
8
+ const day = clone.getDay();
9
+ const delta = day === 0 ? -6 : 1 - day;
10
+ clone.setDate(clone.getDate() + delta);
11
+ clone.setHours(0, 0, 0, 0);
12
+ return clone;
13
+ }
14
+ function addDays(date, days) {
15
+ const clone = new Date(date);
16
+ clone.setDate(clone.getDate() + days);
17
+ return clone;
18
+ }
19
+ function formatRange(start, end) {
20
+ return `${start.toLocaleDateString("en-US", { month: "short", day: "numeric" })} - ${end.toLocaleDateString("en-US", { month: "short", day: "numeric" })}`;
21
+ }
22
+ function dailyBuckets(tasks, start) {
23
+ return Array.from({ length: 7 }, (_, index) => {
24
+ const current = addDays(start, index);
25
+ const dayLabel = current.toLocaleDateString("en-US", { weekday: "short" }).toUpperCase();
26
+ const dayIso = current.toISOString().slice(0, 10);
27
+ const completed = tasks.filter((task) => task.completedAt?.slice(0, 10) === dayIso);
28
+ const totalXp = completed.reduce((sum, task) => sum + task.points, 0);
29
+ return {
30
+ label: dayLabel,
31
+ xp: totalXp,
32
+ focusHours: completed.length * 2 + completed.filter((task) => task.effort !== "light").length
33
+ };
34
+ });
35
+ }
36
+ export function getWeeklyReviewPayload(now = new Date()) {
37
+ const goals = listGoals();
38
+ const tasks = listTasks();
39
+ const gamification = buildGamificationProfile(goals, tasks, now);
40
+ const weekStart = startOfWeek(now);
41
+ const weekEnd = addDays(weekStart, 6);
42
+ const weekTasks = tasks.filter((task) => task.updatedAt >= weekStart.toISOString() && task.updatedAt <= addDays(weekEnd, 1).toISOString());
43
+ const completedTasks = weekTasks.filter((task) => task.completedAt !== null);
44
+ const buckets = dailyBuckets(tasks, weekStart);
45
+ const totalXp = completedTasks.reduce((sum, task) => sum + task.points, 0);
46
+ const peakBucket = [...buckets].sort((left, right) => right.xp - left.xp)[0] ?? buckets[0];
47
+ const activity = listActivityEvents({ limit: 20 }).slice(0, 4);
48
+ const wins = activity.length > 0
49
+ ? activity.map((event) => ({
50
+ id: event.id,
51
+ title: event.title,
52
+ summary: event.description || "Structured proof of movement.",
53
+ rewardXp: typeof event.metadata.points === "number" ? event.metadata.points : 40
54
+ }))
55
+ : completedTasks.slice(0, 3).map((task) => ({
56
+ id: task.id,
57
+ title: task.title,
58
+ summary: task.description || "Completed work converted into evidence.",
59
+ rewardXp: task.points
60
+ }));
61
+ return weeklyReviewPayloadSchema.parse({
62
+ generatedAt: now.toISOString(),
63
+ windowLabel: formatRange(weekStart, weekEnd),
64
+ momentumSummary: {
65
+ totalXp,
66
+ focusHours: buckets.reduce((sum, bucket) => sum + bucket.focusHours, 0),
67
+ efficiencyScore: Math.min(100, gamification.momentumScore + completedTasks.length * 3),
68
+ peakWindow: peakBucket.label
69
+ },
70
+ chart: buckets,
71
+ wins,
72
+ calibration: goals.slice(0, 3).map((goal, index) => ({
73
+ id: goal.id,
74
+ title: goal.title,
75
+ mode: index === 0 ? "accelerate" : index === 1 ? "maintain" : "recover",
76
+ note: index === 0
77
+ ? "This arc has enough evidence to push harder next cycle."
78
+ : index === 1
79
+ ? "Keep the current load and prevent drift."
80
+ : "Reduce friction and re-sequence the next steps."
81
+ })),
82
+ reward: {
83
+ title: "Review Completion Bonus",
84
+ summary: "Finalizing the review locks the current cycle into evidence.",
85
+ rewardXp: 250
86
+ }
87
+ });
88
+ }
@@ -0,0 +1,13 @@
1
+ import { recoverTimedOutTaskRuns } from "../repositories/task-runs.js";
2
+ export function reconcileExpiredTaskRuns(options = {}) {
3
+ const now = options.now ?? new Date();
4
+ const recoveredRuns = recoverTimedOutTaskRuns({ now, limit: options.limit });
5
+ return {
6
+ recoveredAt: now.toISOString(),
7
+ recoveredCount: recoveredRuns.length,
8
+ recoveredRunIds: recoveredRuns.map((run) => run.id)
9
+ };
10
+ }
11
+ export function recoverExpiredTaskRunsOnStartup(options = {}) {
12
+ return reconcileExpiredTaskRuns(options);
13
+ }
@@ -0,0 +1,49 @@
1
+ import { getGoalById } from "../repositories/goals.js";
2
+ import { listTags, listTagsByIds } from "../repositories/tags.js";
3
+ const keywordHints = {
4
+ health: ["Health", "Vitality"],
5
+ train: ["Health", "Vitality"],
6
+ workout: ["Health", "Vitality"],
7
+ review: ["Reflection", "Craft"],
8
+ plan: ["Momentum", "Admin"],
9
+ admin: ["Admin"],
10
+ write: ["Craft", "Deep Work"],
11
+ draft: ["Craft", "Deep Work"],
12
+ relationship: ["Relationships"],
13
+ date: ["Relationships"],
14
+ focus: ["Deep Work"]
15
+ };
16
+ function uniqueTags(tags) {
17
+ const seen = new Set();
18
+ return tags.filter((tag) => {
19
+ if (seen.has(tag.id)) {
20
+ return false;
21
+ }
22
+ seen.add(tag.id);
23
+ return true;
24
+ });
25
+ }
26
+ export function suggestTags(input) {
27
+ const tagCatalog = listTags();
28
+ const selected = new Set(input.selectedTagIds);
29
+ const terms = `${input.title} ${input.description}`.toLowerCase();
30
+ const suggestions = [];
31
+ for (const [keyword, tagNames] of Object.entries(keywordHints)) {
32
+ if (!terms.includes(keyword)) {
33
+ continue;
34
+ }
35
+ for (const tagName of tagNames) {
36
+ const tag = tagCatalog.find((entry) => entry.name === tagName);
37
+ if (tag && !selected.has(tag.id)) {
38
+ suggestions.push(tag);
39
+ }
40
+ }
41
+ }
42
+ if (input.goalId) {
43
+ const goal = getGoalById(input.goalId);
44
+ if (goal) {
45
+ suggestions.push(...listTagsByIds(goal.tagIds).filter((tag) => !selected.has(tag.id)));
46
+ }
47
+ }
48
+ return uniqueTags(suggestions).slice(0, 6);
49
+ }