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
@@ -7,6 +7,7 @@ import { listActivityEvents, listActivityEventsForTask, removeActivityEvent } fr
7
7
  import { approveApprovalRequest, createAgentAction, createInsight, createInsightFeedback, deleteInsight, getInsightById, listAgentActions, listApprovalRequests, listInsights, rejectApprovalRequest, updateInsight } from "./repositories/collaboration.js";
8
8
  import { listEventLog } from "./repositories/event-log.js";
9
9
  import { createGoal, getGoalById, listGoals, updateGoal } from "./repositories/goals.js";
10
+ import { createHabit, createHabitCheckIn, getHabitById, listHabits, updateHabit } from "./repositories/habits.js";
10
11
  import { listDomains } from "./repositories/domains.js";
11
12
  import { buildNotesSummaryByEntity, createNote, getNoteById, listNotes, updateNote } from "./repositories/notes.js";
12
13
  import { createBehavior, createBehaviorPattern, createBeliefEntry, createEmotionDefinition, createEventType, createModeGuideSession, createModeProfile, createPsycheValue, createTriggerReport, getBehaviorById, getBehaviorPatternById, getBeliefEntryById, getEmotionDefinitionById, getEventTypeById, getModeGuideSessionById, getModeProfileById, getPsycheValueById, getTriggerReportById, listBehaviors, listBehaviorPatterns, listBeliefEntries, listEmotionDefinitions, listEventTypes, listModeGuideSessions, listModeProfiles, listPsycheValues, listSchemaCatalog, listTriggerReports, updateBehavior, updateBehaviorPattern, updateBeliefEntry, updateEmotionDefinition, updateEventType, updateModeGuideSession, updateModeProfile, updatePsycheValue, updateTriggerReport } from "./repositories/psyche.js";
@@ -27,7 +28,7 @@ import { getWeeklyReviewPayload } from "./services/reviews.js";
27
28
  import { createTaskRunWatchdog } from "./services/task-run-watchdog.js";
28
29
  import { suggestTags } from "./services/tagging.js";
29
30
  import { PSYCHE_ENTITY_TYPES, createBehaviorSchema, createBeliefEntrySchema, createBehaviorPatternSchema, createEmotionDefinitionSchema, createEventTypeSchema, createModeGuideSessionSchema, createModeProfileSchema, createPsycheValueSchema, createTriggerReportSchema, updateBehaviorSchema, updateBeliefEntrySchema, updateBehaviorPatternSchema, updateEmotionDefinitionSchema, updateEventTypeSchema, updateModeGuideSessionSchema, updateModeProfileSchema, updatePsycheValueSchema, updateTriggerReportSchema } from "./psyche-types.js";
30
- import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createSessionEventSchema, createTagSchema, notesListQuerySchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateInsightSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskSchema } from "./types.js";
31
+ import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createHabitCheckInSchema, createHabitSchema, createSessionEventSchema, createTagSchema, notesListQuerySchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskSchema } from "./types.js";
31
32
  import { buildOpenApiDocument } from "./openapi.js";
32
33
  import { registerWebRoutes } from "./web.js";
33
34
  import { createManagerRuntime } from "./managers/runtime.js";
@@ -186,6 +187,39 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
186
187
  { name: "notes", type: "Array<{ contentMarkdown, author?, links? }>", required: false, description: "Optional nested notes that will auto-link to the new task.", defaultValue: [] }
187
188
  ]
188
189
  },
190
+ {
191
+ entityType: "habit",
192
+ purpose: "A recurring commitment or recurring slip with explicit cadence, graph links, and XP consequences.",
193
+ minimumCreateFields: ["title"],
194
+ relationshipRules: [
195
+ "Habits can link directly to goals, projects, tasks, values, patterns, behaviors, beliefs, modes, and trigger reports.",
196
+ "Habits are recurring records, not task variants, and they participate in search, notes, delete/restore, and XP.",
197
+ "linkedBehaviorId remains a compatibility alias; linkedBehaviorIds is the canonical array form."
198
+ ],
199
+ searchHints: ["Search by title before creating a duplicate habit.", "Use linkedTo when the habit should already be attached to a goal, project, task, or Psyche entity."],
200
+ examples: ['{"title":"Morning training","frequency":"daily","polarity":"positive","linkedGoalIds":["goal_train_body"],"linkedValueIds":["value_steadiness"],"linkedBehaviorIds":["behavior_regulating_walk"]}'],
201
+ fieldGuide: [
202
+ { name: "title", type: "string", required: true, description: "Concrete recurring behavior label." },
203
+ { name: "description", type: "string", required: false, description: "What counts as success or failure for this habit.", defaultValue: "" },
204
+ { name: "status", type: "active|paused|archived", required: false, description: "Lifecycle state.", enumValues: ["active", "paused", "archived"], defaultValue: "active" },
205
+ { name: "polarity", type: "positive|negative", required: false, description: "Whether doing the behavior is aligned or misaligned.", enumValues: ["positive", "negative"], defaultValue: "positive" },
206
+ { name: "frequency", type: "daily|weekly", required: false, description: "Recurrence cadence.", enumValues: ["daily", "weekly"], defaultValue: "daily" },
207
+ { name: "targetCount", type: "integer", required: false, description: "How many repetitions define the cadence window.", defaultValue: 1 },
208
+ { name: "weekDays", type: "integer[]", required: false, description: "Weekday numbers for weekly habits where Monday is 1 and Sunday is 0.", defaultValue: [] },
209
+ { name: "linkedGoalIds", type: "string[]", required: false, description: "Linked goal ids.", defaultValue: [] },
210
+ { name: "linkedProjectIds", type: "string[]", required: false, description: "Linked project ids.", defaultValue: [] },
211
+ { name: "linkedTaskIds", type: "string[]", required: false, description: "Linked task ids.", defaultValue: [] },
212
+ { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
213
+ { name: "linkedPatternIds", type: "string[]", required: false, description: "Linked pattern ids.", defaultValue: [] },
214
+ { name: "linkedBehaviorIds", type: "string[]", required: false, description: "Canonical linked behavior ids.", defaultValue: [] },
215
+ { name: "linkedBehaviorId", type: "string|null", required: false, description: "Compatibility alias for the first linked behavior id.", defaultValue: null, nullable: true },
216
+ { name: "linkedBeliefIds", type: "string[]", required: false, description: "Linked belief ids.", defaultValue: [] },
217
+ { name: "linkedModeIds", type: "string[]", required: false, description: "Linked mode ids.", defaultValue: [] },
218
+ { name: "linkedReportIds", type: "string[]", required: false, description: "Linked trigger report ids.", defaultValue: [] },
219
+ { name: "rewardXp", type: "integer", required: false, description: "XP granted on aligned check-ins.", defaultValue: 12 },
220
+ { name: "penaltyXp", type: "integer", required: false, description: "XP removed on misaligned check-ins.", defaultValue: 8 }
221
+ ]
222
+ },
189
223
  {
190
224
  entityType: "note",
191
225
  purpose: "A Markdown note that can link to one or many Forge entities.",
@@ -594,6 +628,15 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
594
628
  notes: ["Restore only works for soft-deleted entities."],
595
629
  example: '{"operations":[{"entityType":"goal","id":"goal_123","clientRef":"goal-restore-1"}]}'
596
630
  },
631
+ {
632
+ toolName: "forge_grant_reward_bonus",
633
+ summary: "Grant an explicit manual XP bonus or penalty with clear provenance.",
634
+ whenToUse: "Use when the user or operator explicitly wants an auditable reward adjustment beyond the automatic task and habit reward paths.",
635
+ inputShape: "{ entityType: RewardableEntityType, entityId: string, deltaXp: integer, reasonTitle: string, reasonSummary?: string, metadata?: object }",
636
+ requiredFields: ["entityType", "entityId", "deltaXp", "reasonTitle"],
637
+ notes: ["Requires rewards.manage and write scopes.", "Use this for explicit operator judgement, not as a substitute for normal task_run or habit check-in rewards."],
638
+ example: '{"entityType":"habit","entityId":"habit_morning_training","deltaXp":18,"reasonTitle":"Operator bonus","reasonSummary":"Stayed with the habit through unusual travel friction.","metadata":{"manual":true,"source":"agent"}}'
639
+ },
597
640
  {
598
641
  toolName: "forge_post_insight",
599
642
  summary: "Store an agent-authored insight.",
@@ -736,6 +779,7 @@ function buildAgentOnboardingPayload(request) {
736
779
  "Goals are the top-level strategic layer.",
737
780
  "Projects belong to one goal through goalId.",
738
781
  "Tasks can belong to a goal, a project, both, or neither.",
782
+ "Habits are recurring records that can connect directly to goals, projects, tasks, and durable Psyche entities.",
739
783
  "Task runs represent live work sessions on tasks and are separate from task status.",
740
784
  "Notes can link to one or many entities and are the canonical place for Markdown progress context or close-out evidence.",
741
785
  "Psyche values can link to goals, projects, and tasks.",
@@ -771,6 +815,7 @@ function buildAgentOnboardingPayload(request) {
771
815
  "forge_delete_entities",
772
816
  "forge_restore_entities"
773
817
  ],
818
+ rewardWorkflow: ["forge_grant_reward_bonus"],
774
819
  workWorkflow: [
775
820
  "forge_log_work",
776
821
  "forge_start_task_run",
@@ -955,7 +1000,17 @@ function buildHealthPayload(taskRunWatchdog, extras = {}) {
955
1000
  ...extras
956
1001
  };
957
1002
  }
1003
+ function shouldIncludeRuntimeProbe(headers) {
1004
+ const probeHeader = headers["x-forge-runtime-probe"];
1005
+ if (Array.isArray(probeHeader)) {
1006
+ return probeHeader.some((value) => typeof value === "string" && value.trim() === "1");
1007
+ }
1008
+ return typeof probeHeader === "string" && probeHeader.trim() === "1";
1009
+ }
958
1010
  function buildV1Context() {
1011
+ const goals = listGoals();
1012
+ const tasks = listTasks();
1013
+ const habits = listHabits();
959
1014
  return {
960
1015
  meta: {
961
1016
  apiVersion: "v1",
@@ -964,15 +1019,16 @@ function buildV1Context() {
964
1019
  backend: "forge-node-runtime",
965
1020
  mode: "transitional-node"
966
1021
  },
967
- metrics: buildGamificationProfile(listGoals(), listTasks()),
1022
+ metrics: buildGamificationProfile(goals, tasks, habits),
968
1023
  dashboard: getDashboard(),
969
1024
  overview: getOverviewContext(),
970
1025
  today: getTodayContext(),
971
1026
  risk: getRiskContext(),
972
- goals: listGoals(),
1027
+ goals,
973
1028
  projects: listProjectSummaries(),
974
1029
  tags: listTags(),
975
- tasks: listTasks(),
1030
+ tasks,
1031
+ habits,
976
1032
  activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
977
1033
  activity: listActivityEvents({ limit: 25 })
978
1034
  };
@@ -980,8 +1036,9 @@ function buildV1Context() {
980
1036
  function buildXpMetricsPayload() {
981
1037
  const goals = listGoals();
982
1038
  const tasks = listTasks();
1039
+ const habits = listHabits();
983
1040
  const rules = listRewardRules();
984
- const gamificationOverview = buildGamificationOverview(goals, tasks);
1041
+ const gamificationOverview = buildGamificationOverview(goals, tasks, habits);
985
1042
  const dailyAmbientCap = rules
986
1043
  .filter((rule) => rule.family === "ambient")
987
1044
  .reduce((max, rule) => Math.max(max, Number(rule.config.dailyCap ?? 0)), 0) || 12;
@@ -989,7 +1046,7 @@ function buildXpMetricsPayload() {
989
1046
  profile: gamificationOverview.profile,
990
1047
  achievements: gamificationOverview.achievements,
991
1048
  milestoneRewards: gamificationOverview.milestoneRewards,
992
- momentumPulse: buildXpMomentumPulse(goals, tasks),
1049
+ momentumPulse: buildXpMomentumPulse(goals, tasks, habits),
993
1050
  recentLedger: listRewardLedger({ limit: 25 }),
994
1051
  rules,
995
1052
  dailyAmbientXp: getDailyAmbientXp(new Date().toISOString().slice(0, 10)),
@@ -998,6 +1055,7 @@ function buildXpMetricsPayload() {
998
1055
  }
999
1056
  function buildOperatorContext() {
1000
1057
  const tasks = listTasks();
1058
+ const dueHabits = listHabits({ dueToday: true }).slice(0, 12);
1001
1059
  const activeProjects = listProjectSummaries({ status: "active" }).filter((project) => project.activeTaskCount > 0 || project.completedTaskCount > 0);
1002
1060
  const focusTasks = tasks.filter((task) => task.status === "focus" || task.status === "in_progress");
1003
1061
  const recommendedNextTask = focusTasks[0] ??
@@ -1008,6 +1066,7 @@ function buildOperatorContext() {
1008
1066
  generatedAt: new Date().toISOString(),
1009
1067
  activeProjects: activeProjects.slice(0, 8),
1010
1068
  focusTasks: focusTasks.slice(0, 12),
1069
+ dueHabits,
1011
1070
  currentBoard: {
1012
1071
  backlog: tasks.filter((task) => task.status === "backlog").slice(0, 20),
1013
1072
  focus: tasks.filter((task) => task.status === "focus").slice(0, 20),
@@ -1231,9 +1290,18 @@ export async function buildServer(options = {}) {
1231
1290
  return context;
1232
1291
  };
1233
1292
  app.get("/api/health", async () => buildHealthPayload(taskRunWatchdog));
1234
- app.get("/api/v1/health", async () => buildHealthPayload(taskRunWatchdog, {
1293
+ app.get("/api/v1/health", async (request) => buildHealthPayload(taskRunWatchdog, {
1235
1294
  apiVersion: "v1",
1236
- backend: "forge-node-runtime"
1295
+ backend: "forge-node-runtime",
1296
+ ...(shouldIncludeRuntimeProbe(request.headers)
1297
+ ? {
1298
+ runtime: {
1299
+ pid: process.pid,
1300
+ storageRoot: runtimeConfig.dataRoot ?? process.cwd(),
1301
+ basePath: runtimeConfig.basePath
1302
+ }
1303
+ }
1304
+ : {})
1237
1305
  }));
1238
1306
  app.get("/api/v1/auth/operator-session", async (request, reply) => ({
1239
1307
  session: managers.session.ensureLocalOperatorSession(request.headers, reply)
@@ -1721,6 +1789,19 @@ export async function buildServer(options = {}) {
1721
1789
  const query = taskListQuerySchema.parse(request.query ?? {});
1722
1790
  return { tasks: listTasks(query) };
1723
1791
  });
1792
+ app.get("/api/v1/habits", async (request) => {
1793
+ const query = habitListQuerySchema.parse(request.query ?? {});
1794
+ return { habits: listHabits(query) };
1795
+ });
1796
+ app.get("/api/v1/habits/:id", async (request, reply) => {
1797
+ const { id } = request.params;
1798
+ const habit = getHabitById(id);
1799
+ if (!habit) {
1800
+ reply.code(404);
1801
+ return { error: "Habit not found" };
1802
+ }
1803
+ return { habit };
1804
+ });
1724
1805
  app.get("/api/v1/projects/:id", async (request, reply) => {
1725
1806
  const { id } = request.params;
1726
1807
  const project = listProjectSummaries().find((entry) => entry.id === id);
@@ -1764,7 +1845,7 @@ export async function buildServer(options = {}) {
1764
1845
  return { event };
1765
1846
  });
1766
1847
  app.get("/api/v1/metrics", async () => ({
1767
- metrics: buildGamificationOverview(listGoals(), listTasks())
1848
+ metrics: buildGamificationOverview(listGoals(), listTasks(), listHabits())
1768
1849
  }));
1769
1850
  app.get("/api/v1/metrics/xp", async () => ({
1770
1851
  metrics: buildXpMetricsPayload()
@@ -1944,6 +2025,12 @@ export async function buildServer(options = {}) {
1944
2025
  reply.code(201);
1945
2026
  return { project };
1946
2027
  });
2028
+ app.post("/api/v1/habits", async (request, reply) => {
2029
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits" });
2030
+ const habit = createHabit(createHabitSchema.parse(request.body ?? {}), toActivityContext(auth));
2031
+ reply.code(201);
2032
+ return { habit };
2033
+ });
1947
2034
  app.patch("/api/v1/projects/:id", async (request, reply) => {
1948
2035
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/projects/:id" });
1949
2036
  const { id } = request.params;
@@ -1964,6 +2051,36 @@ export async function buildServer(options = {}) {
1964
2051
  }
1965
2052
  return { project };
1966
2053
  });
2054
+ app.patch("/api/v1/habits/:id", async (request, reply) => {
2055
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id" });
2056
+ const { id } = request.params;
2057
+ const habit = updateHabit(id, updateHabitSchema.parse(request.body ?? {}), toActivityContext(auth));
2058
+ if (!habit) {
2059
+ reply.code(404);
2060
+ return { error: "Habit not found" };
2061
+ }
2062
+ return { habit };
2063
+ });
2064
+ app.delete("/api/v1/habits/:id", async (request, reply) => {
2065
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id" });
2066
+ const { id } = request.params;
2067
+ const habit = deleteEntity("habit", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
2068
+ if (!habit) {
2069
+ reply.code(404);
2070
+ return { error: "Habit not found" };
2071
+ }
2072
+ return { habit };
2073
+ });
2074
+ app.post("/api/v1/habits/:id/check-ins", async (request, reply) => {
2075
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id/check-ins" });
2076
+ const { id } = request.params;
2077
+ const habit = createHabitCheckIn(id, createHabitCheckInSchema.parse(request.body ?? {}), toActivityContext(auth));
2078
+ if (!habit) {
2079
+ reply.code(404);
2080
+ return { error: "Habit not found" };
2081
+ }
2082
+ return { habit, metrics: buildXpMetricsPayload() };
2083
+ });
1967
2084
  app.patch("/api/v1/settings", async (request) => {
1968
2085
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/settings" });
1969
2086
  return {
@@ -2072,7 +2189,7 @@ export async function buildServer(options = {}) {
2072
2189
  app.get("/api/metrics", async (_request, reply) => {
2073
2190
  markCompatibilityRoute(reply);
2074
2191
  return {
2075
- metrics: buildGamificationProfile(listGoals(), listTasks())
2192
+ metrics: buildGamificationProfile(listGoals(), listTasks(), listHabits())
2076
2193
  };
2077
2194
  });
2078
2195
  app.get("/api/task-runs", async (request, reply) => {
@@ -2102,7 +2219,7 @@ export async function buildServer(options = {}) {
2102
2219
  markCompatibilityRoute(reply);
2103
2220
  const query = taskListQuerySchema.parse(request.query ?? {});
2104
2221
  return {
2105
- metrics: buildGamificationProfile(listGoals(), listTasks()),
2222
+ metrics: buildGamificationProfile(listGoals(), listTasks(), listHabits()),
2106
2223
  dashboard: getDashboard(),
2107
2224
  overview: getOverviewContext(),
2108
2225
  today: getTodayContext(),
@@ -2111,6 +2228,7 @@ export async function buildServer(options = {}) {
2111
2228
  projects: listProjectSummaries(),
2112
2229
  tags: listTags(),
2113
2230
  tasks: listTasks(query),
2231
+ habits: listHabits(),
2114
2232
  activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
2115
2233
  activity: listActivityEvents({ limit: 25 })
2116
2234
  };
@@ -242,6 +242,89 @@ export function buildOpenApiDocument() {
242
242
  isCurrent: { type: "boolean" }
243
243
  }
244
244
  };
245
+ const habitCheckIn = {
246
+ type: "object",
247
+ additionalProperties: false,
248
+ required: ["id", "habitId", "dateKey", "status", "note", "deltaXp", "createdAt", "updatedAt"],
249
+ properties: {
250
+ id: { type: "string" },
251
+ habitId: { type: "string" },
252
+ dateKey: { type: "string", format: "date" },
253
+ status: { type: "string", enum: ["done", "missed"] },
254
+ note: { type: "string" },
255
+ deltaXp: { type: "integer" },
256
+ createdAt: { type: "string", format: "date-time" },
257
+ updatedAt: { type: "string", format: "date-time" }
258
+ }
259
+ };
260
+ const habit = {
261
+ type: "object",
262
+ additionalProperties: false,
263
+ required: [
264
+ "id",
265
+ "title",
266
+ "description",
267
+ "status",
268
+ "polarity",
269
+ "frequency",
270
+ "targetCount",
271
+ "weekDays",
272
+ "linkedGoalIds",
273
+ "linkedProjectIds",
274
+ "linkedTaskIds",
275
+ "linkedValueIds",
276
+ "linkedPatternIds",
277
+ "linkedBehaviorIds",
278
+ "linkedBeliefIds",
279
+ "linkedModeIds",
280
+ "linkedReportIds",
281
+ "linkedBehaviorId",
282
+ "linkedBehaviorTitle",
283
+ "linkedBehaviorTitles",
284
+ "rewardXp",
285
+ "penaltyXp",
286
+ "createdAt",
287
+ "updatedAt",
288
+ "lastCheckInAt",
289
+ "lastCheckInStatus",
290
+ "streakCount",
291
+ "completionRate",
292
+ "dueToday",
293
+ "checkIns"
294
+ ],
295
+ properties: {
296
+ id: { type: "string" },
297
+ title: { type: "string" },
298
+ description: { type: "string" },
299
+ status: { type: "string", enum: ["active", "paused", "archived"] },
300
+ polarity: { type: "string", enum: ["positive", "negative"] },
301
+ frequency: { type: "string", enum: ["daily", "weekly"] },
302
+ targetCount: { type: "integer" },
303
+ weekDays: arrayOf({ type: "integer" }),
304
+ linkedGoalIds: arrayOf({ type: "string" }),
305
+ linkedProjectIds: arrayOf({ type: "string" }),
306
+ linkedTaskIds: arrayOf({ type: "string" }),
307
+ linkedValueIds: arrayOf({ type: "string" }),
308
+ linkedPatternIds: arrayOf({ type: "string" }),
309
+ linkedBehaviorIds: arrayOf({ type: "string" }),
310
+ linkedBeliefIds: arrayOf({ type: "string" }),
311
+ linkedModeIds: arrayOf({ type: "string" }),
312
+ linkedReportIds: arrayOf({ type: "string" }),
313
+ linkedBehaviorId: nullable({ type: "string" }),
314
+ linkedBehaviorTitle: nullable({ type: "string" }),
315
+ linkedBehaviorTitles: arrayOf({ type: "string" }),
316
+ rewardXp: { type: "integer" },
317
+ penaltyXp: { type: "integer" },
318
+ createdAt: { type: "string", format: "date-time" },
319
+ updatedAt: { type: "string", format: "date-time" },
320
+ lastCheckInAt: nullable({ type: "string", format: "date-time" }),
321
+ lastCheckInStatus: nullable({ type: "string", enum: ["done", "missed"] }),
322
+ streakCount: { type: "integer" },
323
+ completionRate: { type: "number" },
324
+ dueToday: { type: "boolean" },
325
+ checkIns: arrayOf({ $ref: "#/components/schemas/HabitCheckIn" })
326
+ }
327
+ };
245
328
  const activityEvent = {
246
329
  type: "object",
247
330
  additionalProperties: false,
@@ -252,6 +335,7 @@ export function buildOpenApiDocument() {
252
335
  type: "string",
253
336
  enum: [
254
337
  "task",
338
+ "habit",
255
339
  "goal",
256
340
  "project",
257
341
  "domain",
@@ -378,7 +462,7 @@ export function buildOpenApiDocument() {
378
462
  const dashboardPayload = {
379
463
  type: "object",
380
464
  additionalProperties: false,
381
- required: ["stats", "goals", "projects", "tasks", "tags", "suggestedTags", "owners", "executionBuckets", "gamification", "achievements", "milestoneRewards", "recentActivity", "notesSummaryByEntity"],
465
+ required: ["stats", "goals", "projects", "tasks", "habits", "tags", "suggestedTags", "owners", "executionBuckets", "gamification", "achievements", "milestoneRewards", "recentActivity", "notesSummaryByEntity"],
382
466
  properties: {
383
467
  stats: {
384
468
  type: "object",
@@ -397,6 +481,7 @@ export function buildOpenApiDocument() {
397
481
  goals: arrayOf({ $ref: "#/components/schemas/DashboardGoal" }),
398
482
  projects: arrayOf({ $ref: "#/components/schemas/ProjectSummary" }),
399
483
  tasks: arrayOf({ $ref: "#/components/schemas/Task" }),
484
+ habits: arrayOf({ $ref: "#/components/schemas/Habit" }),
400
485
  tags: arrayOf({ $ref: "#/components/schemas/Tag" }),
401
486
  suggestedTags: arrayOf({ $ref: "#/components/schemas/Tag" }),
402
487
  owners: arrayOf({ type: "string" }),
@@ -422,7 +507,7 @@ export function buildOpenApiDocument() {
422
507
  const overviewContext = {
423
508
  type: "object",
424
509
  additionalProperties: false,
425
- required: ["generatedAt", "strategicHeader", "projects", "activeGoals", "topTasks", "recentEvidence", "achievements", "domainBalance", "neglectedGoals"],
510
+ required: ["generatedAt", "strategicHeader", "projects", "activeGoals", "topTasks", "dueHabits", "recentEvidence", "achievements", "domainBalance", "neglectedGoals"],
426
511
  properties: {
427
512
  generatedAt: { type: "string", format: "date-time" },
428
513
  strategicHeader: {
@@ -443,6 +528,7 @@ export function buildOpenApiDocument() {
443
528
  projects: arrayOf({ $ref: "#/components/schemas/ProjectSummary" }),
444
529
  activeGoals: arrayOf({ $ref: "#/components/schemas/DashboardGoal" }),
445
530
  topTasks: arrayOf({ $ref: "#/components/schemas/Task" }),
531
+ dueHabits: arrayOf({ $ref: "#/components/schemas/Habit" }),
446
532
  recentEvidence: arrayOf({ $ref: "#/components/schemas/ActivityEvent" }),
447
533
  achievements: arrayOf({ $ref: "#/components/schemas/AchievementSignal" }),
448
534
  domainBalance: arrayOf({
@@ -475,7 +561,7 @@ export function buildOpenApiDocument() {
475
561
  const todayContext = {
476
562
  type: "object",
477
563
  additionalProperties: false,
478
- required: ["generatedAt", "directive", "timeline", "dailyQuests", "milestoneRewards", "momentum"],
564
+ required: ["generatedAt", "directive", "timeline", "dueHabits", "dailyQuests", "milestoneRewards", "recentHabitRewards", "momentum"],
479
565
  properties: {
480
566
  generatedAt: { type: "string", format: "date-time" },
481
567
  directive: {
@@ -499,6 +585,7 @@ export function buildOpenApiDocument() {
499
585
  tasks: arrayOf({ $ref: "#/components/schemas/Task" })
500
586
  }
501
587
  }),
588
+ dueHabits: arrayOf({ $ref: "#/components/schemas/Habit" }),
502
589
  dailyQuests: arrayOf({
503
590
  type: "object",
504
591
  additionalProperties: false,
@@ -513,6 +600,7 @@ export function buildOpenApiDocument() {
513
600
  }
514
601
  }),
515
602
  milestoneRewards: arrayOf({ $ref: "#/components/schemas/MilestoneReward" }),
603
+ recentHabitRewards: arrayOf({ $ref: "#/components/schemas/RewardLedgerEvent" }),
516
604
  momentum: {
517
605
  type: "object",
518
606
  additionalProperties: false,
@@ -550,7 +638,7 @@ export function buildOpenApiDocument() {
550
638
  const forgeSnapshot = {
551
639
  type: "object",
552
640
  additionalProperties: false,
553
- required: ["meta", "metrics", "dashboard", "overview", "today", "risk", "goals", "projects", "tags", "tasks", "activeTaskRuns", "activity"],
641
+ required: ["meta", "metrics", "dashboard", "overview", "today", "risk", "goals", "projects", "tags", "tasks", "habits", "activeTaskRuns", "activity"],
554
642
  properties: {
555
643
  meta: {
556
644
  type: "object",
@@ -573,6 +661,7 @@ export function buildOpenApiDocument() {
573
661
  projects: arrayOf({ $ref: "#/components/schemas/ProjectSummary" }),
574
662
  tags: arrayOf({ $ref: "#/components/schemas/Tag" }),
575
663
  tasks: arrayOf({ $ref: "#/components/schemas/Task" }),
664
+ habits: arrayOf({ $ref: "#/components/schemas/Habit" }),
576
665
  activeTaskRuns: arrayOf({ $ref: "#/components/schemas/TaskRun" }),
577
666
  activity: arrayOf({ $ref: "#/components/schemas/ActivityEvent" })
578
667
  }
@@ -1041,6 +1130,7 @@ export function buildOpenApiDocument() {
1041
1130
  "generatedAt",
1042
1131
  "activeProjects",
1043
1132
  "focusTasks",
1133
+ "dueHabits",
1044
1134
  "currentBoard",
1045
1135
  "recentActivity",
1046
1136
  "recentTaskRuns",
@@ -1051,6 +1141,7 @@ export function buildOpenApiDocument() {
1051
1141
  generatedAt: { type: "string", format: "date-time" },
1052
1142
  activeProjects: arrayOf({ $ref: "#/components/schemas/ProjectSummary" }),
1053
1143
  focusTasks: arrayOf({ $ref: "#/components/schemas/Task" }),
1144
+ dueHabits: arrayOf({ $ref: "#/components/schemas/Habit" }),
1054
1145
  currentBoard: {
1055
1146
  type: "object",
1056
1147
  additionalProperties: false,
@@ -1993,6 +2084,8 @@ export function buildOpenApiDocument() {
1993
2084
  ProjectSummary: projectSummary,
1994
2085
  Task: task,
1995
2086
  TaskRun: taskRun,
2087
+ HabitCheckIn: habitCheckIn,
2088
+ Habit: habit,
1996
2089
  ActivityEvent: activityEvent,
1997
2090
  GamificationProfile: gamificationProfile,
1998
2091
  AchievementSignal: achievementSignal,
@@ -2718,6 +2811,90 @@ export function buildOpenApiDocument() {
2718
2811
  }
2719
2812
  }
2720
2813
  },
2814
+ "/api/v1/habits": {
2815
+ get: {
2816
+ summary: "List habits with current streak and due-today state",
2817
+ responses: {
2818
+ "200": jsonResponse({
2819
+ type: "object",
2820
+ required: ["habits"],
2821
+ properties: {
2822
+ habits: arrayOf({ $ref: "#/components/schemas/Habit" })
2823
+ }
2824
+ }, "Habit collection")
2825
+ }
2826
+ },
2827
+ post: {
2828
+ summary: "Create a habit",
2829
+ responses: {
2830
+ "201": jsonResponse({
2831
+ type: "object",
2832
+ required: ["habit"],
2833
+ properties: {
2834
+ habit: { $ref: "#/components/schemas/Habit" }
2835
+ }
2836
+ }, "Created habit"),
2837
+ default: { $ref: "#/components/responses/Error" }
2838
+ }
2839
+ }
2840
+ },
2841
+ "/api/v1/habits/{id}": {
2842
+ get: {
2843
+ summary: "Get a habit",
2844
+ responses: {
2845
+ "200": jsonResponse({
2846
+ type: "object",
2847
+ required: ["habit"],
2848
+ properties: {
2849
+ habit: { $ref: "#/components/schemas/Habit" }
2850
+ }
2851
+ }, "Habit"),
2852
+ "404": { $ref: "#/components/responses/Error" }
2853
+ }
2854
+ },
2855
+ patch: {
2856
+ summary: "Update a habit",
2857
+ responses: {
2858
+ "200": jsonResponse({
2859
+ type: "object",
2860
+ required: ["habit"],
2861
+ properties: {
2862
+ habit: { $ref: "#/components/schemas/Habit" }
2863
+ }
2864
+ }, "Updated habit"),
2865
+ "404": { $ref: "#/components/responses/Error" }
2866
+ }
2867
+ },
2868
+ delete: {
2869
+ summary: "Delete a habit",
2870
+ responses: {
2871
+ "200": jsonResponse({
2872
+ type: "object",
2873
+ required: ["habit"],
2874
+ properties: {
2875
+ habit: { $ref: "#/components/schemas/Habit" }
2876
+ }
2877
+ }, "Deleted habit"),
2878
+ "404": { $ref: "#/components/responses/Error" }
2879
+ }
2880
+ }
2881
+ },
2882
+ "/api/v1/habits/{id}/check-ins": {
2883
+ post: {
2884
+ summary: "Record a habit outcome for one day",
2885
+ responses: {
2886
+ "200": jsonResponse({
2887
+ type: "object",
2888
+ required: ["habit", "metrics"],
2889
+ properties: {
2890
+ habit: { $ref: "#/components/schemas/Habit" },
2891
+ metrics: { $ref: "#/components/schemas/XpMetricsPayload" }
2892
+ }
2893
+ }, "Habit check-in result"),
2894
+ "404": { $ref: "#/components/responses/Error" }
2895
+ }
2896
+ }
2897
+ },
2721
2898
  "/api/v1/tags": {
2722
2899
  get: {
2723
2900
  summary: "List tags",