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.
Files changed (42) hide show
  1. package/README.md +8 -5
  2. package/dist/assets/{board-C_m78kvK.js → board-2KevHCI0.js} +2 -2
  3. package/dist/assets/{board-C_m78kvK.js.map → board-2KevHCI0.js.map} +1 -1
  4. package/dist/assets/index-CDYW4WDH.js +36 -0
  5. package/dist/assets/index-CDYW4WDH.js.map +1 -0
  6. package/dist/assets/index-yroQr6YZ.css +1 -0
  7. package/dist/assets/{motion-CpZvZumD.js → motion-q19HPmWs.js} +2 -2
  8. package/dist/assets/{motion-CpZvZumD.js.map → motion-q19HPmWs.js.map} +1 -1
  9. package/dist/assets/{table-DtyXTw03.js → table-BDMHBY4a.js} +2 -2
  10. package/dist/assets/{table-DtyXTw03.js.map → table-BDMHBY4a.js.map} +1 -1
  11. package/dist/assets/{ui-BXbpiKyS.js → ui-CQ_AsFs8.js} +2 -2
  12. package/dist/assets/{ui-BXbpiKyS.js.map → ui-CQ_AsFs8.js.map} +1 -1
  13. package/dist/assets/{vendor-QBH6qVEe.js → vendor-5HifrnRK.js} +90 -75
  14. package/dist/assets/{vendor-QBH6qVEe.js.map → vendor-5HifrnRK.js.map} +1 -1
  15. package/dist/assets/{viz-w-IMeueL.js → viz-CQzkRnTu.js} +2 -2
  16. package/dist/assets/{viz-w-IMeueL.js.map → viz-CQzkRnTu.js.map} +1 -1
  17. package/dist/index.html +8 -8
  18. package/dist/openclaw/api-client.d.ts +1 -0
  19. package/dist/openclaw/local-runtime.js +243 -15
  20. package/dist/openclaw/plugin-entry-shared.js +45 -4
  21. package/dist/openclaw/tools.js +15 -0
  22. package/dist/server/app.js +129 -11
  23. package/dist/server/openapi.js +181 -4
  24. package/dist/server/repositories/habits.js +358 -0
  25. package/dist/server/repositories/rewards.js +62 -0
  26. package/dist/server/services/context.js +16 -6
  27. package/dist/server/services/dashboard.js +6 -3
  28. package/dist/server/services/entity-crud.js +23 -1
  29. package/dist/server/services/gamification.js +66 -18
  30. package/dist/server/services/insights.js +2 -1
  31. package/dist/server/services/reviews.js +2 -1
  32. package/dist/server/types.js +140 -1
  33. package/openclaw.plugin.json +1 -1
  34. package/package.json +1 -1
  35. package/server/migrations/003_habits.sql +30 -0
  36. package/server/migrations/004_habit_links.sql +8 -0
  37. package/server/migrations/005_habit_psyche_links.sql +24 -0
  38. package/skills/forge-openclaw/SKILL.md +16 -2
  39. package/skills/forge-openclaw/cron_jobs.md +395 -0
  40. package/dist/assets/index-BWtLtXwb.js +0 -36
  41. package/dist/assets/index-BWtLtXwb.js.map +0 -1
  42. 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 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) => {
@@ -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());
@@ -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: nonEmptyTrimmedString,
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({
@@ -2,7 +2,7 @@
2
2
  "id": "forge-openclaw-plugin",
3
3
  "name": "Forge",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
- "version": "0.2.13",
5
+ "version": "0.2.18",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-openclaw-plugin",
3
- "version": "0.2.13",
3
+ "version": "0.2.18",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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,8 @@
1
+ ALTER TABLE habits
2
+ ADD COLUMN linked_goal_ids_json TEXT NOT NULL DEFAULT '[]';
3
+
4
+ ALTER TABLE habits
5
+ ADD COLUMN linked_project_ids_json TEXT NOT NULL DEFAULT '[]';
6
+
7
+ ALTER TABLE habits
8
+ ADD COLUMN linked_task_ids_json TEXT NOT NULL DEFAULT '[]';
@@ -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`