forge-openclaw-plugin 0.2.13 → 0.2.18
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.
- package/README.md +8 -5
- package/dist/assets/{board-C_m78kvK.js → board-2KevHCI0.js} +2 -2
- package/dist/assets/{board-C_m78kvK.js.map → board-2KevHCI0.js.map} +1 -1
- package/dist/assets/index-CDYW4WDH.js +36 -0
- package/dist/assets/index-CDYW4WDH.js.map +1 -0
- package/dist/assets/index-yroQr6YZ.css +1 -0
- package/dist/assets/{motion-CpZvZumD.js → motion-q19HPmWs.js} +2 -2
- package/dist/assets/{motion-CpZvZumD.js.map → motion-q19HPmWs.js.map} +1 -1
- package/dist/assets/{table-DtyXTw03.js → table-BDMHBY4a.js} +2 -2
- package/dist/assets/{table-DtyXTw03.js.map → table-BDMHBY4a.js.map} +1 -1
- package/dist/assets/{ui-BXbpiKyS.js → ui-CQ_AsFs8.js} +2 -2
- package/dist/assets/{ui-BXbpiKyS.js.map → ui-CQ_AsFs8.js.map} +1 -1
- package/dist/assets/{vendor-QBH6qVEe.js → vendor-5HifrnRK.js} +90 -75
- package/dist/assets/{vendor-QBH6qVEe.js.map → vendor-5HifrnRK.js.map} +1 -1
- package/dist/assets/{viz-w-IMeueL.js → viz-CQzkRnTu.js} +2 -2
- package/dist/assets/{viz-w-IMeueL.js.map → viz-CQzkRnTu.js.map} +1 -1
- package/dist/index.html +8 -8
- package/dist/openclaw/api-client.d.ts +1 -0
- package/dist/openclaw/local-runtime.js +243 -15
- package/dist/openclaw/plugin-entry-shared.js +45 -4
- package/dist/openclaw/tools.js +15 -0
- package/dist/server/app.js +129 -11
- package/dist/server/openapi.js +181 -4
- package/dist/server/repositories/habits.js +358 -0
- package/dist/server/repositories/rewards.js +62 -0
- package/dist/server/services/context.js +16 -6
- package/dist/server/services/dashboard.js +6 -3
- package/dist/server/services/entity-crud.js +23 -1
- package/dist/server/services/gamification.js +66 -18
- package/dist/server/services/insights.js +2 -1
- package/dist/server/services/reviews.js +2 -1
- package/dist/server/types.js +140 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/003_habits.sql +30 -0
- package/server/migrations/004_habit_links.sql +8 -0
- package/server/migrations/005_habit_psyche_links.sql +24 -0
- package/skills/forge-openclaw/SKILL.md +16 -2
- package/skills/forge-openclaw/cron_jobs.md +395 -0
- package/dist/assets/index-BWtLtXwb.js +0 -36
- package/dist/assets/index-BWtLtXwb.js.map +0 -1
- package/dist/assets/index-Dp5GXY_z.css +0 -1
|
@@ -12,9 +12,15 @@ function startOfWeek(date) {
|
|
|
12
12
|
function dayKey(isoDate) {
|
|
13
13
|
return isoDate.slice(0, 10);
|
|
14
14
|
}
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { listActivityEvents } from "../repositories/activity-events.js";
|
|
2
2
|
import { listGoals } from "../repositories/goals.js";
|
|
3
|
+
import { listHabits } from "../repositories/habits.js";
|
|
3
4
|
import { listTasks } from "../repositories/tasks.js";
|
|
4
5
|
import { buildGamificationProfile } from "./gamification.js";
|
|
5
6
|
import { weeklyReviewPayloadSchema } from "../types.js";
|
|
@@ -36,7 +37,7 @@ function dailyBuckets(tasks, start) {
|
|
|
36
37
|
export function getWeeklyReviewPayload(now = new Date()) {
|
|
37
38
|
const goals = listGoals();
|
|
38
39
|
const tasks = listTasks();
|
|
39
|
-
const gamification = buildGamificationProfile(goals, tasks, now);
|
|
40
|
+
const gamification = buildGamificationProfile(goals, tasks, listHabits(), now);
|
|
40
41
|
const weekStart = startOfWeek(now);
|
|
41
42
|
const weekEnd = addDays(weekStart, 6);
|
|
42
43
|
const weekTasks = tasks.filter((task) => task.updatedAt >= weekStart.toISOString() && task.updatedAt <= addDays(weekEnd, 1).toISOString());
|
package/dist/server/types.js
CHANGED
|
@@ -11,8 +11,13 @@ export const taskDueFilterSchema = z.enum(["overdue", "today", "week"]);
|
|
|
11
11
|
export const taskRunStatusSchema = z.enum(["active", "completed", "released", "timed_out"]);
|
|
12
12
|
export const taskTimerModeSchema = z.enum(["planned", "unlimited"]);
|
|
13
13
|
export const timeAccountingModeSchema = z.enum(["split", "parallel", "primary_only"]);
|
|
14
|
+
export const habitFrequencySchema = z.enum(["daily", "weekly"]);
|
|
15
|
+
export const habitPolaritySchema = z.enum(["positive", "negative"]);
|
|
16
|
+
export const habitStatusSchema = z.enum(["active", "paused", "archived"]);
|
|
17
|
+
export const habitCheckInStatusSchema = z.enum(["done", "missed"]);
|
|
14
18
|
export const activityEntityTypeSchema = z.enum([
|
|
15
19
|
"task",
|
|
20
|
+
"habit",
|
|
16
21
|
"goal",
|
|
17
22
|
"project",
|
|
18
23
|
"domain",
|
|
@@ -50,6 +55,7 @@ export const crudEntityTypeSchema = z.enum([
|
|
|
50
55
|
"goal",
|
|
51
56
|
"project",
|
|
52
57
|
"task",
|
|
58
|
+
"habit",
|
|
53
59
|
"tag",
|
|
54
60
|
"note",
|
|
55
61
|
"insight",
|
|
@@ -63,6 +69,22 @@ export const crudEntityTypeSchema = z.enum([
|
|
|
63
69
|
"emotion_definition",
|
|
64
70
|
"trigger_report"
|
|
65
71
|
]);
|
|
72
|
+
export const rewardableEntityTypeSchema = z.enum([
|
|
73
|
+
"system",
|
|
74
|
+
"goal",
|
|
75
|
+
"project",
|
|
76
|
+
"task",
|
|
77
|
+
"habit",
|
|
78
|
+
"tag",
|
|
79
|
+
"note",
|
|
80
|
+
"insight",
|
|
81
|
+
"psyche_value",
|
|
82
|
+
"behavior_pattern",
|
|
83
|
+
"behavior",
|
|
84
|
+
"belief_entry",
|
|
85
|
+
"mode_profile",
|
|
86
|
+
"trigger_report"
|
|
87
|
+
]);
|
|
66
88
|
export const deleteModeSchema = z.enum(["soft", "hard"]);
|
|
67
89
|
export const rewardRuleFamilySchema = z.enum([
|
|
68
90
|
"completion",
|
|
@@ -215,6 +237,48 @@ export const taskRunSchema = z.object({
|
|
|
215
237
|
timedOutAt: z.string().nullable(),
|
|
216
238
|
updatedAt: z.string()
|
|
217
239
|
});
|
|
240
|
+
export const habitCheckInSchema = z.object({
|
|
241
|
+
id: z.string(),
|
|
242
|
+
habitId: z.string(),
|
|
243
|
+
dateKey: z.string(),
|
|
244
|
+
status: habitCheckInStatusSchema,
|
|
245
|
+
note: z.string(),
|
|
246
|
+
deltaXp: z.number().int(),
|
|
247
|
+
createdAt: z.string(),
|
|
248
|
+
updatedAt: z.string()
|
|
249
|
+
});
|
|
250
|
+
export const habitSchema = z.object({
|
|
251
|
+
id: z.string(),
|
|
252
|
+
title: nonEmptyTrimmedString,
|
|
253
|
+
description: trimmedString,
|
|
254
|
+
status: habitStatusSchema,
|
|
255
|
+
polarity: habitPolaritySchema,
|
|
256
|
+
frequency: habitFrequencySchema,
|
|
257
|
+
targetCount: z.number().int().positive(),
|
|
258
|
+
weekDays: z.array(z.number().int().min(0).max(6)).default([]),
|
|
259
|
+
linkedGoalIds: uniqueStringArraySchema.default([]),
|
|
260
|
+
linkedProjectIds: uniqueStringArraySchema.default([]),
|
|
261
|
+
linkedTaskIds: uniqueStringArraySchema.default([]),
|
|
262
|
+
linkedValueIds: uniqueStringArraySchema.default([]),
|
|
263
|
+
linkedPatternIds: uniqueStringArraySchema.default([]),
|
|
264
|
+
linkedBehaviorIds: uniqueStringArraySchema.default([]),
|
|
265
|
+
linkedBeliefIds: uniqueStringArraySchema.default([]),
|
|
266
|
+
linkedModeIds: uniqueStringArraySchema.default([]),
|
|
267
|
+
linkedReportIds: uniqueStringArraySchema.default([]),
|
|
268
|
+
linkedBehaviorId: z.string().nullable(),
|
|
269
|
+
linkedBehaviorTitle: z.string().nullable(),
|
|
270
|
+
linkedBehaviorTitles: z.array(z.string()).default([]),
|
|
271
|
+
rewardXp: z.number().int().positive(),
|
|
272
|
+
penaltyXp: z.number().int().positive(),
|
|
273
|
+
createdAt: z.string(),
|
|
274
|
+
updatedAt: z.string(),
|
|
275
|
+
lastCheckInAt: z.string().nullable(),
|
|
276
|
+
lastCheckInStatus: habitCheckInStatusSchema.nullable(),
|
|
277
|
+
streakCount: z.number().int().nonnegative(),
|
|
278
|
+
completionRate: z.number().min(0).max(100),
|
|
279
|
+
dueToday: z.boolean(),
|
|
280
|
+
checkIns: z.array(habitCheckInSchema).default([])
|
|
281
|
+
});
|
|
218
282
|
export const activityEventSchema = z.object({
|
|
219
283
|
id: z.string(),
|
|
220
284
|
entityType: activityEntityTypeSchema,
|
|
@@ -314,6 +378,7 @@ export const dashboardPayloadSchema = z.object({
|
|
|
314
378
|
goals: z.array(dashboardGoalSchema),
|
|
315
379
|
projects: z.array(projectSummarySchema),
|
|
316
380
|
tasks: z.array(taskSchema),
|
|
381
|
+
habits: z.array(habitSchema),
|
|
317
382
|
tags: z.array(tagSchema),
|
|
318
383
|
suggestedTags: z.array(tagSchema),
|
|
319
384
|
owners: z.array(z.string()),
|
|
@@ -354,6 +419,7 @@ export const overviewContextSchema = z.object({
|
|
|
354
419
|
projects: z.array(projectSummarySchema),
|
|
355
420
|
activeGoals: z.array(dashboardGoalSchema),
|
|
356
421
|
topTasks: z.array(taskSchema),
|
|
422
|
+
dueHabits: z.array(habitSchema),
|
|
357
423
|
recentEvidence: z.array(activityEventSchema),
|
|
358
424
|
achievements: z.array(achievementSignalSchema),
|
|
359
425
|
domainBalance: z.array(contextDomainBalanceSchema),
|
|
@@ -381,8 +447,10 @@ export const todayContextSchema = z.object({
|
|
|
381
447
|
sessionLabel: z.string()
|
|
382
448
|
}),
|
|
383
449
|
timeline: z.array(todayTimelineBucketSchema),
|
|
450
|
+
dueHabits: z.array(habitSchema),
|
|
384
451
|
dailyQuests: z.array(todayQuestSchema),
|
|
385
452
|
milestoneRewards: z.array(milestoneRewardSchema),
|
|
453
|
+
recentHabitRewards: z.array(z.lazy(() => rewardLedgerEventSchema)).default([]),
|
|
386
454
|
momentum: z.object({
|
|
387
455
|
streakDays: z.number().int().nonnegative(),
|
|
388
456
|
momentumScore: z.number().int().min(0).max(100),
|
|
@@ -656,6 +724,7 @@ export const operatorContextPayloadSchema = z.object({
|
|
|
656
724
|
generatedAt: z.string(),
|
|
657
725
|
activeProjects: z.array(projectSummarySchema),
|
|
658
726
|
focusTasks: z.array(taskSchema),
|
|
727
|
+
dueHabits: z.array(habitSchema),
|
|
659
728
|
currentBoard: z.object({
|
|
660
729
|
backlog: z.array(taskSchema),
|
|
661
730
|
focus: z.array(taskSchema),
|
|
@@ -675,7 +744,7 @@ export const updateRewardRuleSchema = z.object({
|
|
|
675
744
|
config: z.record(z.string(), rewardConfigValueSchema).optional()
|
|
676
745
|
});
|
|
677
746
|
export const createManualRewardGrantSchema = z.object({
|
|
678
|
-
entityType:
|
|
747
|
+
entityType: rewardableEntityTypeSchema,
|
|
679
748
|
entityId: nonEmptyTrimmedString,
|
|
680
749
|
deltaXp: z.number().int().refine((value) => value !== 0, {
|
|
681
750
|
message: "deltaXp must not be zero"
|
|
@@ -777,6 +846,12 @@ export const projectListQuerySchema = z.object({
|
|
|
777
846
|
status: projectStatusSchema.optional(),
|
|
778
847
|
limit: z.coerce.number().int().positive().max(100).optional()
|
|
779
848
|
});
|
|
849
|
+
export const habitListQuerySchema = z.object({
|
|
850
|
+
status: habitStatusSchema.optional(),
|
|
851
|
+
polarity: habitPolaritySchema.optional(),
|
|
852
|
+
dueToday: z.coerce.boolean().optional(),
|
|
853
|
+
limit: z.coerce.number().int().positive().max(100).optional()
|
|
854
|
+
});
|
|
780
855
|
export const createGoalSchema = z.object({
|
|
781
856
|
title: nonEmptyTrimmedString,
|
|
782
857
|
description: trimmedString.default(""),
|
|
@@ -822,6 +897,65 @@ export const taskMutationShape = {
|
|
|
822
897
|
notes: z.array(nestedCreateNoteSchema).default([])
|
|
823
898
|
};
|
|
824
899
|
export const createTaskSchema = z.object(taskMutationShape);
|
|
900
|
+
const habitMutationShape = {
|
|
901
|
+
title: nonEmptyTrimmedString,
|
|
902
|
+
description: trimmedString.default(""),
|
|
903
|
+
status: habitStatusSchema.default("active"),
|
|
904
|
+
polarity: habitPolaritySchema.default("positive"),
|
|
905
|
+
frequency: habitFrequencySchema.default("daily"),
|
|
906
|
+
targetCount: z.number().int().min(1).max(14).default(1),
|
|
907
|
+
weekDays: z.array(z.number().int().min(0).max(6)).max(7).default([]),
|
|
908
|
+
linkedGoalIds: uniqueStringArraySchema.default([]),
|
|
909
|
+
linkedProjectIds: uniqueStringArraySchema.default([]),
|
|
910
|
+
linkedTaskIds: uniqueStringArraySchema.default([]),
|
|
911
|
+
linkedValueIds: uniqueStringArraySchema.default([]),
|
|
912
|
+
linkedPatternIds: uniqueStringArraySchema.default([]),
|
|
913
|
+
linkedBehaviorIds: uniqueStringArraySchema.default([]),
|
|
914
|
+
linkedBeliefIds: uniqueStringArraySchema.default([]),
|
|
915
|
+
linkedModeIds: uniqueStringArraySchema.default([]),
|
|
916
|
+
linkedReportIds: uniqueStringArraySchema.default([]),
|
|
917
|
+
linkedBehaviorId: nonEmptyTrimmedString.nullable().default(null),
|
|
918
|
+
rewardXp: z.number().int().min(1).max(100).default(12),
|
|
919
|
+
penaltyXp: z.number().int().min(1).max(100).default(8)
|
|
920
|
+
};
|
|
921
|
+
export const createHabitSchema = z.object(habitMutationShape).superRefine((value, context) => {
|
|
922
|
+
if (value.frequency === "weekly" && value.weekDays.length === 0) {
|
|
923
|
+
context.addIssue({
|
|
924
|
+
code: z.ZodIssueCode.custom,
|
|
925
|
+
path: ["weekDays"],
|
|
926
|
+
message: "Select at least one weekday for weekly habits"
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
export const updateHabitSchema = z.object({
|
|
931
|
+
title: nonEmptyTrimmedString.optional(),
|
|
932
|
+
description: trimmedString.optional(),
|
|
933
|
+
status: habitStatusSchema.optional(),
|
|
934
|
+
polarity: habitPolaritySchema.optional(),
|
|
935
|
+
frequency: habitFrequencySchema.optional(),
|
|
936
|
+
targetCount: z.number().int().min(1).max(14).optional(),
|
|
937
|
+
weekDays: z.array(z.number().int().min(0).max(6)).max(7).optional(),
|
|
938
|
+
linkedGoalIds: uniqueStringArraySchema.optional(),
|
|
939
|
+
linkedProjectIds: uniqueStringArraySchema.optional(),
|
|
940
|
+
linkedTaskIds: uniqueStringArraySchema.optional(),
|
|
941
|
+
linkedValueIds: uniqueStringArraySchema.optional(),
|
|
942
|
+
linkedPatternIds: uniqueStringArraySchema.optional(),
|
|
943
|
+
linkedBehaviorIds: uniqueStringArraySchema.optional(),
|
|
944
|
+
linkedBeliefIds: uniqueStringArraySchema.optional(),
|
|
945
|
+
linkedModeIds: uniqueStringArraySchema.optional(),
|
|
946
|
+
linkedReportIds: uniqueStringArraySchema.optional(),
|
|
947
|
+
linkedBehaviorId: nonEmptyTrimmedString.nullable().optional(),
|
|
948
|
+
rewardXp: z.number().int().min(1).max(100).optional(),
|
|
949
|
+
penaltyXp: z.number().int().min(1).max(100).optional()
|
|
950
|
+
}).superRefine((value, context) => {
|
|
951
|
+
if (value.frequency === "weekly" && value.weekDays !== undefined && value.weekDays.length === 0) {
|
|
952
|
+
context.addIssue({
|
|
953
|
+
code: z.ZodIssueCode.custom,
|
|
954
|
+
path: ["weekDays"],
|
|
955
|
+
message: "Select at least one weekday for weekly habits"
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
});
|
|
825
959
|
export const updateTaskSchema = z.object({
|
|
826
960
|
title: nonEmptyTrimmedString.optional(),
|
|
827
961
|
description: trimmedString.optional(),
|
|
@@ -880,6 +1014,11 @@ export const taskRunFinishSchema = z.object({
|
|
|
880
1014
|
export const taskRunFocusSchema = z.object({
|
|
881
1015
|
actor: nonEmptyTrimmedString.optional()
|
|
882
1016
|
});
|
|
1017
|
+
export const createHabitCheckInSchema = z.object({
|
|
1018
|
+
dateKey: dateOnlySchema.default(new Date().toISOString().slice(0, 10)),
|
|
1019
|
+
status: habitCheckInStatusSchema,
|
|
1020
|
+
note: trimmedString.default("")
|
|
1021
|
+
});
|
|
883
1022
|
export const updateSettingsSchema = z.object({
|
|
884
1023
|
profile: z
|
|
885
1024
|
.object({
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
CREATE TABLE IF NOT EXISTS habits (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
title TEXT NOT NULL,
|
|
4
|
+
description TEXT NOT NULL DEFAULT '',
|
|
5
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
6
|
+
polarity TEXT NOT NULL DEFAULT 'positive',
|
|
7
|
+
frequency TEXT NOT NULL DEFAULT 'daily',
|
|
8
|
+
target_count INTEGER NOT NULL DEFAULT 1,
|
|
9
|
+
week_days_json TEXT NOT NULL DEFAULT '[]',
|
|
10
|
+
linked_behavior_id TEXT REFERENCES psyche_behaviors(id) ON DELETE SET NULL,
|
|
11
|
+
reward_xp INTEGER NOT NULL DEFAULT 12,
|
|
12
|
+
penalty_xp INTEGER NOT NULL DEFAULT 8,
|
|
13
|
+
created_at TEXT NOT NULL,
|
|
14
|
+
updated_at TEXT NOT NULL
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE TABLE IF NOT EXISTS habit_check_ins (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
habit_id TEXT NOT NULL REFERENCES habits(id) ON DELETE CASCADE,
|
|
20
|
+
date_key TEXT NOT NULL,
|
|
21
|
+
status TEXT NOT NULL,
|
|
22
|
+
note TEXT NOT NULL DEFAULT '',
|
|
23
|
+
delta_xp INTEGER NOT NULL DEFAULT 0,
|
|
24
|
+
created_at TEXT NOT NULL,
|
|
25
|
+
updated_at TEXT NOT NULL,
|
|
26
|
+
UNIQUE (habit_id, date_key)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_habits_status ON habits(status, updated_at DESC);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_habit_check_ins_habit_date ON habit_check_ins(habit_id, date_key DESC);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
ALTER TABLE habits
|
|
2
|
+
ADD COLUMN linked_value_ids_json TEXT NOT NULL DEFAULT '[]';
|
|
3
|
+
|
|
4
|
+
ALTER TABLE habits
|
|
5
|
+
ADD COLUMN linked_pattern_ids_json TEXT NOT NULL DEFAULT '[]';
|
|
6
|
+
|
|
7
|
+
ALTER TABLE habits
|
|
8
|
+
ADD COLUMN linked_behavior_ids_json TEXT NOT NULL DEFAULT '[]';
|
|
9
|
+
|
|
10
|
+
ALTER TABLE habits
|
|
11
|
+
ADD COLUMN linked_belief_ids_json TEXT NOT NULL DEFAULT '[]';
|
|
12
|
+
|
|
13
|
+
ALTER TABLE habits
|
|
14
|
+
ADD COLUMN linked_mode_ids_json TEXT NOT NULL DEFAULT '[]';
|
|
15
|
+
|
|
16
|
+
ALTER TABLE habits
|
|
17
|
+
ADD COLUMN linked_report_ids_json TEXT NOT NULL DEFAULT '[]';
|
|
18
|
+
|
|
19
|
+
UPDATE habits
|
|
20
|
+
SET linked_behavior_ids_json = CASE
|
|
21
|
+
WHEN linked_behavior_id IS NULL OR trim(linked_behavior_id) = '' THEN '[]'
|
|
22
|
+
ELSE json_array(linked_behavior_id)
|
|
23
|
+
END
|
|
24
|
+
WHERE linked_behavior_ids_json = '[]';
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: forge-openclaw
|
|
3
|
-
description: use when the user wants to save, search, update, review, start, stop, or explain work or psyche records inside forge, or when the conversation is clearly about a forge entity such as a goal, project, task, task_run, insight, psyche_value, behavior_pattern, behavior, belief_entry, mode_profile, mode_guide_session, trigger_report, event_type, or emotion_definition. identify the exact forge entity, keep the main conversation natural, offer saving once when helpful, ask only for missing fields, and use the correct forge tool and payload shape.
|
|
3
|
+
description: use when the user wants to save, search, update, review, start, stop, reward, or explain work or psyche records inside forge, or when the conversation is clearly about a forge entity such as a goal, project, task, habit, task_run, insight, psyche_value, behavior_pattern, behavior, belief_entry, mode_profile, mode_guide_session, trigger_report, event_type, or emotion_definition. identify the exact forge entity, keep the main conversation natural, offer saving once when helpful, ask only for missing fields, and use the correct forge tool and payload shape.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Forge is the user's structured system for planning work, doing work, reflecting on patterns, and keeping a truthful record of what is happening. Use it when the user is clearly working inside that system, or when they are describing something that naturally belongs there and would benefit from being stored, updated, reviewed, or acted on in Forge. Keep the conversation natural first. Do not turn every message into intake. When a real Forge entity is clearly present, name the exact entity type plainly, help with the substance of the conversation, and then offer Forge once, lightly, if storing it would genuinely help.
|
|
7
7
|
|
|
8
8
|
Forge has two major domains. The planning side covers goals, projects, tasks, notes, live work sessions, and agent-authored insights. The Psyche side covers values, patterns, behaviors, beliefs, modes, guided mode sessions, trigger reports, event types, and reusable emotion definitions. The model should use the real entity names, not vague substitutes. Say `project`, not “initiative”. Say `behavior_pattern`, not “theme”. Say `trigger_report`, not “incident note”.
|
|
9
|
+
Habits are a first-class recurring entity in the planning side. They can link directly to goals, projects, tasks, values, patterns, behaviors, beliefs, modes, and trigger reports, and they participate in the same searchable noteable graph as the rest of Forge.
|
|
9
10
|
|
|
10
11
|
Write to Forge only with clear user consent. If the user is just thinking aloud, helping first is usually better than writing immediately. After helping, you may offer one short Forge prompt if the match is strong. If the user agrees, ask only for the missing fields and only one to three focused questions at a time. Do not offer Forge again after a decline unless the user reopens it.
|
|
11
12
|
|
|
13
|
+
Optional recurring automation templates live in `cron_jobs.md` next to this skill. Use that file only when the user explicitly asks for recurring Forge automations, cron jobs, scheduled check-ins, or a recurring synthesis workflow. Those entries are rich examples, not defaults: adapt personal details such as names, recipients, phone numbers, or project titles to the current user, but preserve the intended tone, operational logic, and any example naming conventions when the user chooses to adopt that pattern.
|
|
14
|
+
|
|
12
15
|
Forge data location rule:
|
|
13
16
|
|
|
14
17
|
- by default, Forge stores data under the active runtime root at `data/forge.sqlite`
|
|
@@ -80,6 +83,15 @@ Ask:
|
|
|
80
83
|
2. Should it live under an existing goal or project?
|
|
81
84
|
3. Does it need a due date, priority, or owner?
|
|
82
85
|
|
|
86
|
+
`habit`
|
|
87
|
+
Use for a recurring commitment or recurring slip with explicit cadence and XP consequences.
|
|
88
|
+
Minimum field: `title`
|
|
89
|
+
Usually useful: `polarity`, `frequency`, `linkedGoalIds`, `linkedProjectIds`, `linkedTaskIds`, `linkedValueIds`, `linkedPatternIds`, `linkedBehaviorIds`, `linkedBeliefIds`, `linkedModeIds`, `linkedReportIds`
|
|
90
|
+
Ask:
|
|
91
|
+
1. What is the recurring behavior in one concrete sentence?
|
|
92
|
+
2. Is doing it good (`positive`) or a slip (`negative`)?
|
|
93
|
+
3. What should it link back to in Forge or Psyche?
|
|
94
|
+
|
|
83
95
|
`task_run`
|
|
84
96
|
Use for live work happening now.
|
|
85
97
|
Required fields to start: `taskId`, `actor`
|
|
@@ -171,12 +183,13 @@ Use the batch entity tools for stored records:
|
|
|
171
183
|
`forge_search_entities`, `forge_create_entities`, `forge_update_entities`, `forge_delete_entities`, `forge_restore_entities`
|
|
172
184
|
|
|
173
185
|
These tools operate on:
|
|
174
|
-
`goal`, `project`, `task`, `note`, `psyche_value`, `behavior_pattern`, `behavior`, `belief_entry`, `mode_profile`, `mode_guide_session`, `trigger_report`, `event_type`, `emotion_definition`
|
|
186
|
+
`goal`, `project`, `task`, `habit`, `note`, `psyche_value`, `behavior_pattern`, `behavior`, `belief_entry`, `mode_profile`, `mode_guide_session`, `trigger_report`, `event_type`, `emotion_definition`
|
|
175
187
|
|
|
176
188
|
Use live work tools for `task_run`:
|
|
177
189
|
`forge_log_work`, `forge_start_task_run`, `forge_heartbeat_task_run`, `forge_focus_task_run`, `forge_complete_task_run`, `forge_release_task_run`
|
|
178
190
|
|
|
179
191
|
Use `forge_post_insight` for `insight`.
|
|
192
|
+
Use `forge_grant_reward_bonus` only for explicit manual XP bonuses or penalties that should be auditable and cannot be expressed through the normal task-run or habit check-in routes.
|
|
180
193
|
|
|
181
194
|
Do not say you lack a creation path when these tools cover the request. Do not open the Forge UI or a browser for normal creation or updates that the tools already support. Use `forge_get_ui_entrypoint` only when visual review, Kanban movement, graph exploration, or complex multi-record editing would genuinely be easier there.
|
|
182
195
|
|
|
@@ -247,6 +260,7 @@ When the user asks which Forge tools are available, list exactly these tools:
|
|
|
247
260
|
`forge_update_entities`
|
|
248
261
|
`forge_delete_entities`
|
|
249
262
|
`forge_restore_entities`
|
|
263
|
+
`forge_grant_reward_bonus`
|
|
250
264
|
`forge_log_work`
|
|
251
265
|
`forge_start_task_run`
|
|
252
266
|
`forge_heartbeat_task_run`
|