forge-openclaw-plugin 0.2.15 → 0.2.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/README.md +39 -4
  2. package/dist/assets/{board-C_m78kvK.js → board-8L3uX7_O.js} +2 -2
  3. package/dist/assets/{board-C_m78kvK.js.map → board-8L3uX7_O.js.map} +1 -1
  4. package/dist/assets/index-Cj1IBH_w.js +36 -0
  5. package/dist/assets/index-Cj1IBH_w.js.map +1 -0
  6. package/dist/assets/index-DQT6EbuS.css +1 -0
  7. package/dist/assets/{motion-CpZvZumD.js → motion-1GAqqi8M.js} +2 -2
  8. package/dist/assets/{motion-CpZvZumD.js.map → motion-1GAqqi8M.js.map} +1 -1
  9. package/dist/assets/{table-DtyXTw03.js → table-DBGlgRjk.js} +2 -2
  10. package/dist/assets/{table-DtyXTw03.js.map → table-DBGlgRjk.js.map} +1 -1
  11. package/dist/assets/{ui-BXbpiKyS.js → ui-iTluWjC4.js} +2 -2
  12. package/dist/assets/{ui-BXbpiKyS.js.map → ui-iTluWjC4.js.map} +1 -1
  13. package/dist/assets/{vendor-QBH6qVEe.js → vendor-BvM2F9Dp.js} +151 -81
  14. package/dist/assets/vendor-BvM2F9Dp.js.map +1 -0
  15. package/dist/assets/{viz-w-IMeueL.js → viz-CNeunkfu.js} +2 -2
  16. package/dist/assets/{viz-w-IMeueL.js.map → viz-CNeunkfu.js.map} +1 -1
  17. package/dist/index.html +8 -8
  18. package/dist/openclaw/local-runtime.js +142 -9
  19. package/dist/openclaw/parity.js +1 -0
  20. package/dist/openclaw/plugin-entry-shared.js +7 -1
  21. package/dist/openclaw/routes.js +7 -0
  22. package/dist/openclaw/tools.js +198 -16
  23. package/dist/server/app.js +2615 -251
  24. package/dist/server/managers/platform/secrets-manager.js +44 -1
  25. package/dist/server/managers/runtime.js +3 -1
  26. package/dist/server/openapi.js +2212 -170
  27. package/dist/server/repositories/calendar.js +1101 -0
  28. package/dist/server/repositories/deleted-entities.js +10 -2
  29. package/dist/server/repositories/habits.js +358 -0
  30. package/dist/server/repositories/notes.js +161 -28
  31. package/dist/server/repositories/projects.js +45 -13
  32. package/dist/server/repositories/rewards.js +176 -6
  33. package/dist/server/repositories/settings.js +47 -5
  34. package/dist/server/repositories/task-runs.js +46 -10
  35. package/dist/server/repositories/tasks.js +25 -9
  36. package/dist/server/repositories/weekly-reviews.js +109 -0
  37. package/dist/server/repositories/work-adjustments.js +105 -0
  38. package/dist/server/services/calendar-runtime.js +1301 -0
  39. package/dist/server/services/context.js +16 -6
  40. package/dist/server/services/dashboard.js +6 -3
  41. package/dist/server/services/entity-crud.js +116 -3
  42. package/dist/server/services/gamification.js +66 -18
  43. package/dist/server/services/insights.js +2 -1
  44. package/dist/server/services/projects.js +32 -8
  45. package/dist/server/services/reviews.js +17 -2
  46. package/dist/server/services/work-time.js +27 -0
  47. package/dist/server/types.js +1069 -45
  48. package/openclaw.plugin.json +1 -1
  49. package/package.json +1 -1
  50. package/server/migrations/003_habits.sql +30 -0
  51. package/server/migrations/004_habit_links.sql +8 -0
  52. package/server/migrations/005_habit_psyche_links.sql +24 -0
  53. package/server/migrations/006_work_adjustments.sql +14 -0
  54. package/server/migrations/007_weekly_review_closures.sql +17 -0
  55. package/server/migrations/008_calendar_execution.sql +147 -0
  56. package/server/migrations/009_true_calendar_events.sql +195 -0
  57. package/server/migrations/010_calendar_selection_state.sql +6 -0
  58. package/server/migrations/011_calendar_timezone_backfill.sql +11 -0
  59. package/server/migrations/012_work_block_ranges.sql +7 -0
  60. package/server/migrations/013_microsoft_local_auth_settings.sql +8 -0
  61. package/server/migrations/014_note_tags_and_ephemeral.sql +8 -0
  62. package/skills/forge-openclaw/SKILL.md +130 -10
  63. package/skills/forge-openclaw/cron_jobs.md +395 -0
  64. package/dist/assets/index-BWtLtXwb.js +0 -36
  65. package/dist/assets/index-BWtLtXwb.js.map +0 -1
  66. package/dist/assets/index-Dp5GXY_z.css +0 -1
  67. package/dist/assets/vendor-QBH6qVEe.js.map +0 -1
@@ -1,33 +1,38 @@
1
1
  import Fastify from "fastify";
2
2
  import cors from "@fastify/cors";
3
3
  import { ZodError } from "zod";
4
- import { configureDatabase, configureDatabaseSeeding } from "./db.js";
5
- import { isHttpError } from "./errors.js";
6
- import { listActivityEvents, listActivityEventsForTask, removeActivityEvent } from "./repositories/activity-events.js";
4
+ import { configureDatabase, configureDatabaseSeeding, runInTransaction } from "./db.js";
5
+ import { HttpError, isHttpError } from "./errors.js";
6
+ import { listActivityEvents, listActivityEventsForTask, recordActivityEvent, removeActivityEvent } from "./repositories/activity-events.js";
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";
13
14
  import { createProject, updateProject } from "./repositories/projects.js";
14
- import { createManualRewardGrant, getDailyAmbientXp, getRewardRuleById, listRewardLedger, listRewardRules, recordSessionEvent, updateRewardRule } from "./repositories/rewards.js";
15
+ import { createManualRewardGrant, getDailyAmbientXp, getRewardRuleById, listRewardLedger, listRewardRules, recordWorkAdjustmentReward, recordSessionEvent, updateRewardRule } from "./repositories/rewards.js";
15
16
  import { listAgentIdentities, getSettings, isPsycheAuthRequired, updateSettings, verifyAgentToken } from "./repositories/settings.js";
16
17
  import { createTag, getTagById, listTags, updateTag } from "./repositories/tags.js";
17
18
  import { claimTaskRun, completeTaskRun, focusTaskRun, heartbeatTaskRun, listTaskRuns, recoverTimedOutTaskRuns, releaseTaskRun } from "./repositories/task-runs.js";
18
19
  import { createTask, createTaskWithIdempotency, getTaskById, listTasks, uncompleteTask, updateTask } from "./repositories/tasks.js";
20
+ import { createWorkAdjustment } from "./repositories/work-adjustments.js";
21
+ import { createCalendarEvent, createTaskTimebox, createWorkBlockTemplate, deleteCalendarEvent, deleteTaskTimebox, deleteWorkBlockTemplate, getCalendarConnectionById, getCalendarEventById, listCalendars, listCalendarEvents, listTaskTimeboxes, suggestTaskTimeboxes, listWorkBlockInstances, listWorkBlockTemplates, updateCalendarEvent, updateTaskTimebox, updateWorkBlockTemplate } from "./repositories/calendar.js";
19
22
  import { getDashboard } from "./services/dashboard.js";
20
23
  import { getOverviewContext, getRiskContext, getTodayContext } from "./services/context.js";
21
24
  import { buildGamificationOverview, buildGamificationProfile, buildXpMomentumPulse } from "./services/gamification.js";
22
25
  import { getInsightsPayload } from "./services/insights.js";
23
26
  import { createEntities, deleteEntities, deleteEntity, getSettingsBinPayload, restoreEntities, searchEntities, updateEntities } from "./services/entity-crud.js";
24
27
  import { getPsycheOverview } from "./services/psyche.js";
25
- import { getProjectBoard, listProjectSummaries } from "./services/projects.js";
28
+ import { getProjectBoard, getProjectSummary, listProjectSummaries } from "./services/projects.js";
26
29
  import { getWeeklyReviewPayload } from "./services/reviews.js";
30
+ import { finalizeWeeklyReviewClosure } from "./repositories/weekly-reviews.js";
27
31
  import { createTaskRunWatchdog } from "./services/task-run-watchdog.js";
28
32
  import { suggestTags } from "./services/tagging.js";
33
+ import { CalendarConnectionConflictError, completeMicrosoftCalendarOauth, createCalendarConnection, deleteCalendarEventProjection, discoverCalendarConnection, discoverExistingCalendarConnection, getMicrosoftCalendarOauthSession, listConnectedCalendarConnections, removeCalendarConnection, pushCalendarEventUpdate, readCalendarOverview, syncCalendarConnection, startMicrosoftCalendarOauth, testMicrosoftCalendarOauthConfiguration, listCalendarProviderMetadata, updateCalendarConnectionSelection } from "./services/calendar-runtime.js";
29
34
  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";
35
+ import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createCalendarEventSchema, createHabitCheckInSchema, createCalendarConnectionSchema, discoverCalendarConnectionSchema, startMicrosoftCalendarOauthSchema, testMicrosoftCalendarOauthConfigurationSchema, createHabitSchema, createTaskTimeboxSchema, createWorkBlockTemplateSchema, createSessionEventSchema, createWorkAdjustmentSchema, createTagSchema, calendarOverviewQuerySchema, 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, updateCalendarConnectionSchema, updateCalendarEventSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskTimeboxSchema, updateTaskSchema, updateWorkBlockTemplateSchema, workAdjustmentResultSchema, finalizeWeeklyReviewResultSchema, recommendTaskTimeboxesSchema } from "./types.js";
31
36
  import { buildOpenApiDocument } from "./openapi.js";
32
37
  import { registerWebRoutes } from "./web.js";
33
38
  import { createManagerRuntime } from "./managers/runtime.js";
@@ -105,7 +110,9 @@ function readSingleForwardedHeader(value) {
105
110
  return null;
106
111
  }
107
112
  function getRequestOrigin(request) {
108
- const protocol = readSingleForwardedHeader(request.headers["x-forwarded-proto"]) ?? request.protocol ?? "http";
113
+ const protocol = readSingleForwardedHeader(request.headers["x-forwarded-proto"]) ??
114
+ request.protocol ??
115
+ "http";
109
116
  const host = readSingleForwardedHeader(request.headers["x-forwarded-host"]) ??
110
117
  readSingleForwardedHeader(request.headers.host) ??
111
118
  request.hostname;
@@ -121,20 +128,72 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
121
128
  "Projects should usually link to one goal through goalId.",
122
129
  "Tasks can link directly to a goal when no project exists yet."
123
130
  ],
124
- searchHints: ["Search by title before creating a new goal.", "Use status filters when looking for paused or completed goals."],
131
+ searchHints: [
132
+ "Search by title before creating a new goal.",
133
+ "Use status filters when looking for paused or completed goals."
134
+ ],
125
135
  examples: [
126
136
  '{"title":"Create meaningfully","horizon":"lifetime","description":"Make work that is honest, beautiful, and published."}',
127
137
  '{"title":"Build a beautiful family","horizon":"lifetime","description":"Invest in love, stability, and shared rituals."}'
128
138
  ],
129
139
  fieldGuide: [
130
- { name: "title", type: "string", required: true, description: "Human-readable goal name." },
131
- { name: "description", type: "string", required: false, description: "Why the goal matters or what success looks like.", defaultValue: "" },
132
- { name: "horizon", type: "quarter|year|lifetime", required: false, description: "How far out the goal is meant to live.", enumValues: ["quarter", "year", "lifetime"], defaultValue: "year" },
133
- { name: "status", type: "active|paused|completed", required: false, description: "Current lifecycle state for the goal.", enumValues: ["active", "paused", "completed"], defaultValue: "active" },
134
- { name: "targetPoints", type: "integer", required: false, description: "Approximate XP/point target for the goal.", defaultValue: 400 },
135
- { name: "themeColor", type: "hex-color", required: false, description: "Visual color used in the UI.", defaultValue: "#c8a46b" },
136
- { name: "tagIds", type: "string[]", required: false, description: "Existing tag ids linked to the goal.", defaultValue: [] },
137
- { name: "notes", type: "Array<{ contentMarkdown, author?, links? }>", required: false, description: "Optional nested notes that will auto-link to the new goal.", defaultValue: [] }
140
+ {
141
+ name: "title",
142
+ type: "string",
143
+ required: true,
144
+ description: "Human-readable goal name."
145
+ },
146
+ {
147
+ name: "description",
148
+ type: "string",
149
+ required: false,
150
+ description: "Markdown description for why the goal matters or what success looks like.",
151
+ defaultValue: ""
152
+ },
153
+ {
154
+ name: "horizon",
155
+ type: "quarter|year|lifetime",
156
+ required: false,
157
+ description: "How far out the goal is meant to live.",
158
+ enumValues: ["quarter", "year", "lifetime"],
159
+ defaultValue: "year"
160
+ },
161
+ {
162
+ name: "status",
163
+ type: "active|paused|completed",
164
+ required: false,
165
+ description: "Current lifecycle state for the goal.",
166
+ enumValues: ["active", "paused", "completed"],
167
+ defaultValue: "active"
168
+ },
169
+ {
170
+ name: "targetPoints",
171
+ type: "integer",
172
+ required: false,
173
+ description: "Approximate XP/point target for the goal.",
174
+ defaultValue: 400
175
+ },
176
+ {
177
+ name: "themeColor",
178
+ type: "hex-color",
179
+ required: false,
180
+ description: "Visual color used in the UI.",
181
+ defaultValue: "#c8a46b"
182
+ },
183
+ {
184
+ name: "tagIds",
185
+ type: "string[]",
186
+ required: false,
187
+ description: "Existing tag ids linked to the goal.",
188
+ defaultValue: []
189
+ },
190
+ {
191
+ name: "notes",
192
+ type: "Array<{ contentMarkdown, author?, tags?, destroyAt?, links? }>",
193
+ required: false,
194
+ description: "Optional nested notes that will auto-link to the new goal.",
195
+ defaultValue: []
196
+ }
138
197
  ]
139
198
  },
140
199
  {
@@ -146,16 +205,61 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
146
205
  "Tasks can link to a project through projectId.",
147
206
  "Projects inherit strategic meaning from their parent goal."
148
207
  ],
149
- searchHints: ["Search by title inside the target goal before creating a new project."],
150
- examples: ['{"goalId":"goal_create_meaningfully","title":"Launch the public Forge plugin","description":"Ship a real public release that people can install."}'],
208
+ searchHints: [
209
+ "Search by title inside the target goal before creating a new project."
210
+ ],
211
+ examples: [
212
+ '{"goalId":"goal_create_meaningfully","title":"Launch the public Forge plugin","description":"Ship a real public release that people can install."}'
213
+ ],
151
214
  fieldGuide: [
152
- { name: "goalId", type: "string", required: true, description: "Existing parent goal id." },
153
- { name: "title", type: "string", required: true, description: "Project name." },
154
- { name: "description", type: "string", required: false, description: "Desired outcome or scope.", defaultValue: "" },
155
- { name: "status", type: "active|paused|completed", required: false, description: "Lifecycle state.", enumValues: ["active", "paused", "completed"], defaultValue: "active" },
156
- { name: "targetPoints", type: "integer", required: false, description: "Approximate XP/point target for the project.", defaultValue: 240 },
157
- { name: "themeColor", type: "hex-color", required: false, description: "Visual color used in the UI.", defaultValue: "#c0c1ff" },
158
- { name: "notes", type: "Array<{ contentMarkdown, author?, links? }>", required: false, description: "Optional nested notes that will auto-link to the new project.", defaultValue: [] }
215
+ {
216
+ name: "goalId",
217
+ type: "string",
218
+ required: true,
219
+ description: "Existing parent goal id."
220
+ },
221
+ {
222
+ name: "title",
223
+ type: "string",
224
+ required: true,
225
+ description: "Project name."
226
+ },
227
+ {
228
+ name: "description",
229
+ type: "string",
230
+ required: false,
231
+ description: "Markdown description for the desired outcome or scope.",
232
+ defaultValue: ""
233
+ },
234
+ {
235
+ name: "status",
236
+ type: "active|paused|completed",
237
+ required: false,
238
+ description: "Lifecycle state.",
239
+ enumValues: ["active", "paused", "completed"],
240
+ defaultValue: "active"
241
+ },
242
+ {
243
+ name: "targetPoints",
244
+ type: "integer",
245
+ required: false,
246
+ description: "Approximate XP/point target for the project.",
247
+ defaultValue: 240
248
+ },
249
+ {
250
+ name: "themeColor",
251
+ type: "hex-color",
252
+ required: false,
253
+ description: "Visual color used in the UI.",
254
+ defaultValue: "#c0c1ff"
255
+ },
256
+ {
257
+ name: "notes",
258
+ type: "Array<{ contentMarkdown, author?, tags?, destroyAt?, links? }>",
259
+ required: false,
260
+ description: "Optional nested notes that will auto-link to the new project.",
261
+ defaultValue: []
262
+ }
159
263
  ]
160
264
  },
161
265
  {
@@ -167,42 +271,604 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
167
271
  "Live work is tracked by task runs, not by task status alone.",
168
272
  "A task status of in_progress does not guarantee a live active run."
169
273
  ],
170
- searchHints: ["Search by title before creating a duplicate task.", "Use linkedTo filters when you know the parent goal or project."],
171
- examples: ['{"title":"Write the plugin release notes","projectId":"project_forge_plugin_launch","status":"focus","priority":"high"}'],
274
+ searchHints: [
275
+ "Search by title before creating a duplicate task.",
276
+ "Use linkedTo filters when you know the parent goal or project."
277
+ ],
278
+ examples: [
279
+ '{"title":"Write the plugin release notes","projectId":"project_forge_plugin_launch","status":"focus","priority":"high"}'
280
+ ],
281
+ fieldGuide: [
282
+ {
283
+ name: "title",
284
+ type: "string",
285
+ required: true,
286
+ description: "Concrete action label."
287
+ },
288
+ {
289
+ name: "description",
290
+ type: "string",
291
+ required: false,
292
+ description: "Markdown context, constraints, or acceptance notes.",
293
+ defaultValue: ""
294
+ },
295
+ {
296
+ name: "status",
297
+ type: "backlog|focus|in_progress|blocked|done",
298
+ required: false,
299
+ description: "Board lane or completion state.",
300
+ enumValues: ["backlog", "focus", "in_progress", "blocked", "done"],
301
+ defaultValue: "backlog"
302
+ },
303
+ {
304
+ name: "priority",
305
+ type: "low|medium|high|critical",
306
+ required: false,
307
+ description: "Relative urgency.",
308
+ enumValues: ["low", "medium", "high", "critical"],
309
+ defaultValue: "medium"
310
+ },
311
+ {
312
+ name: "owner",
313
+ type: "string",
314
+ required: false,
315
+ description: "Human-facing owner label.",
316
+ defaultValue: "Albert"
317
+ },
318
+ {
319
+ name: "goalId",
320
+ type: "string|null",
321
+ required: false,
322
+ description: "Linked goal id.",
323
+ defaultValue: null,
324
+ nullable: true
325
+ },
326
+ {
327
+ name: "projectId",
328
+ type: "string|null",
329
+ required: false,
330
+ description: "Linked project id.",
331
+ defaultValue: null,
332
+ nullable: true
333
+ },
334
+ {
335
+ name: "dueDate",
336
+ type: "YYYY-MM-DD|null",
337
+ required: false,
338
+ description: "Optional due date.",
339
+ defaultValue: null,
340
+ nullable: true
341
+ },
342
+ {
343
+ name: "effort",
344
+ type: "light|deep|marathon",
345
+ required: false,
346
+ description: "How heavy the task feels.",
347
+ enumValues: ["light", "deep", "marathon"],
348
+ defaultValue: "deep"
349
+ },
350
+ {
351
+ name: "energy",
352
+ type: "low|steady|high",
353
+ required: false,
354
+ description: "Energy demand.",
355
+ enumValues: ["low", "steady", "high"],
356
+ defaultValue: "steady"
357
+ },
358
+ {
359
+ name: "points",
360
+ type: "integer",
361
+ required: false,
362
+ description: "Reward value for the task.",
363
+ defaultValue: 40
364
+ },
365
+ {
366
+ name: "sortOrder",
367
+ type: "integer",
368
+ required: false,
369
+ description: "Lane ordering hint when set explicitly."
370
+ },
371
+ {
372
+ name: "tagIds",
373
+ type: "string[]",
374
+ required: false,
375
+ description: "Existing tag ids linked to the task.",
376
+ defaultValue: []
377
+ },
378
+ {
379
+ name: "notes",
380
+ type: "Array<{ contentMarkdown, author?, tags?, destroyAt?, links? }>",
381
+ required: false,
382
+ description: "Optional nested notes that will auto-link to the new task.",
383
+ defaultValue: []
384
+ }
385
+ ]
386
+ },
387
+ {
388
+ entityType: "calendar_event",
389
+ purpose: "A canonical Forge calendar event that can live locally first and then project to connected provider calendars.",
390
+ minimumCreateFields: ["title", "startAt", "endAt"],
391
+ relationshipRules: [
392
+ "Forge stores the canonical event first; provider copies are downstream projections.",
393
+ "Use links to connect the event to goals, projects, tasks, habits, notes, or Psyche entities.",
394
+ "If preferredCalendarId is omitted, Forge uses the default writable connected calendar when one exists.",
395
+ "Set preferredCalendarId to null only when the user explicitly wants Forge-only storage."
396
+ ],
397
+ searchHints: [
398
+ "Search by title or linked entity before creating a duplicate event.",
399
+ "Use linkedTo when you know the goal, project, task, or habit the event should already reference."
400
+ ],
401
+ examples: [
402
+ '{"title":"Weekly research supervision","startAt":"2026-04-06T06:00:00.000Z","endAt":"2026-04-06T07:00:00.000Z","timezone":"Europe/Zurich","links":[{"entityType":"project","entityId":"project_123","relationshipType":"meeting_for"}]}'
403
+ ],
404
+ fieldGuide: [
405
+ {
406
+ name: "title",
407
+ type: "string",
408
+ required: true,
409
+ description: "Human-readable event title."
410
+ },
411
+ {
412
+ name: "description",
413
+ type: "string",
414
+ required: false,
415
+ description: "Longer event description.",
416
+ defaultValue: ""
417
+ },
418
+ {
419
+ name: "location",
420
+ type: "string",
421
+ required: false,
422
+ description: "Location or meeting place.",
423
+ defaultValue: ""
424
+ },
425
+ {
426
+ name: "startAt",
427
+ type: "ISO datetime",
428
+ required: true,
429
+ description: "Start instant in ISO-8601 form."
430
+ },
431
+ {
432
+ name: "endAt",
433
+ type: "ISO datetime",
434
+ required: true,
435
+ description: "End instant in ISO-8601 form."
436
+ },
437
+ {
438
+ name: "timezone",
439
+ type: "string",
440
+ required: false,
441
+ description: "IANA timezone label.",
442
+ defaultValue: "UTC"
443
+ },
444
+ {
445
+ name: "isAllDay",
446
+ type: "boolean",
447
+ required: false,
448
+ description: "Whether this is an all-day event.",
449
+ defaultValue: false
450
+ },
451
+ {
452
+ name: "availability",
453
+ type: "busy|free",
454
+ required: false,
455
+ description: "Availability state exposed to scheduling rules.",
456
+ enumValues: ["busy", "free"],
457
+ defaultValue: "busy"
458
+ },
459
+ {
460
+ name: "eventType",
461
+ type: "string",
462
+ required: false,
463
+ description: "Optional event category label used by scheduling rules.",
464
+ defaultValue: ""
465
+ },
466
+ {
467
+ name: "categories",
468
+ type: "string[]",
469
+ required: false,
470
+ description: "Optional provider-style categories.",
471
+ defaultValue: []
472
+ },
473
+ {
474
+ name: "preferredCalendarId",
475
+ type: "string|null",
476
+ required: false,
477
+ description: "Writable connected calendar to project into. Omit it to use the default writable connected calendar. Set null only to force Forge-only storage.",
478
+ defaultValue: "default writable connected calendar when available",
479
+ nullable: true
480
+ },
481
+ {
482
+ name: "links",
483
+ type: "Array<{ entityType, entityId, relationshipType? }>",
484
+ required: false,
485
+ description: "Forge entities linked to this event.",
486
+ defaultValue: []
487
+ }
488
+ ]
489
+ },
490
+ {
491
+ entityType: "work_block_template",
492
+ purpose: "A recurring work-availability template such as Main Activity, Secondary Activity, Third Activity, Rest, Holiday, or Custom.",
493
+ minimumCreateFields: [
494
+ "title",
495
+ "kind",
496
+ "timezone",
497
+ "weekDays",
498
+ "startMinute",
499
+ "endMinute",
500
+ "blockingState"
501
+ ],
502
+ relationshipRules: [
503
+ "Work block templates derive visible calendar instances for the requested range instead of storing one repeated event per day.",
504
+ "startsOn and endsOn are optional active-date bounds. Leaving endsOn null makes the block repeat indefinitely.",
505
+ "They are Forge-owned scheduling structures, not mirrored provider events."
506
+ ],
507
+ searchHints: [
508
+ "Search by title or kind before creating a duplicate recurring block."
509
+ ],
510
+ examples: [
511
+ '{"title":"Main Activity","kind":"main_activity","color":"#f97316","timezone":"Europe/Zurich","weekDays":[1,2,3,4,5],"startMinute":480,"endMinute":720,"startsOn":"2026-04-06","endsOn":null,"blockingState":"blocked"}',
512
+ '{"title":"Summer holiday","kind":"holiday","color":"#14b8a6","timezone":"Europe/Zurich","weekDays":[0,1,2,3,4,5,6],"startMinute":0,"endMinute":1440,"startsOn":"2026-08-01","endsOn":"2026-08-16","blockingState":"blocked"}'
513
+ ],
514
+ fieldGuide: [
515
+ {
516
+ name: "title",
517
+ type: "string",
518
+ required: true,
519
+ description: "Display name for the recurring block."
520
+ },
521
+ {
522
+ name: "kind",
523
+ type: "main_activity|secondary_activity|third_activity|rest|holiday|custom",
524
+ required: true,
525
+ description: "Preset or custom block type.",
526
+ enumValues: [
527
+ "main_activity",
528
+ "secondary_activity",
529
+ "third_activity",
530
+ "rest",
531
+ "holiday",
532
+ "custom"
533
+ ]
534
+ },
535
+ {
536
+ name: "color",
537
+ type: "hex-color",
538
+ required: false,
539
+ description: "UI color for generated instances.",
540
+ defaultValue: "#60a5fa"
541
+ },
542
+ {
543
+ name: "timezone",
544
+ type: "string",
545
+ required: true,
546
+ description: "IANA timezone that defines the recurring window."
547
+ },
548
+ {
549
+ name: "weekDays",
550
+ type: "integer[]",
551
+ required: true,
552
+ description: "Weekday numbers where Sunday is 0 and Saturday is 6."
553
+ },
554
+ {
555
+ name: "startMinute",
556
+ type: "integer",
557
+ required: true,
558
+ description: "Minute from midnight where the block starts."
559
+ },
560
+ {
561
+ name: "endMinute",
562
+ type: "integer",
563
+ required: true,
564
+ description: "Minute from midnight where the block ends."
565
+ },
566
+ {
567
+ name: "startsOn",
568
+ type: "YYYY-MM-DD|null",
569
+ required: false,
570
+ description: "Optional first active date for the recurring block.",
571
+ defaultValue: null,
572
+ nullable: true
573
+ },
574
+ {
575
+ name: "endsOn",
576
+ type: "YYYY-MM-DD|null",
577
+ required: false,
578
+ description: "Optional last active date. Null means repeat indefinitely.",
579
+ defaultValue: null,
580
+ nullable: true
581
+ },
582
+ {
583
+ name: "blockingState",
584
+ type: "allowed|blocked",
585
+ required: true,
586
+ description: "Whether this block generally allows or blocks work.",
587
+ enumValues: ["allowed", "blocked"]
588
+ }
589
+ ]
590
+ },
591
+ {
592
+ entityType: "task_timebox",
593
+ purpose: "A planned or live calendar slot attached to a task.",
594
+ minimumCreateFields: ["taskId", "title", "startsAt", "endsAt"],
595
+ relationshipRules: [
596
+ "Task timeboxes belong to a task and can optionally carry the parent project id.",
597
+ "Live task runs can attach to matching timeboxes later; creating a timebox does not start work by itself."
598
+ ],
599
+ searchHints: [
600
+ "Search by task linkage or title before creating another slot for the same work block."
601
+ ],
602
+ examples: [
603
+ '{"taskId":"task_123","projectId":"project_456","title":"Draft the methods section","startsAt":"2026-04-03T08:00:00.000Z","endsAt":"2026-04-03T09:30:00.000Z","source":"suggested"}'
604
+ ],
172
605
  fieldGuide: [
173
- { name: "title", type: "string", required: true, description: "Concrete action label." },
174
- { name: "description", type: "string", required: false, description: "Helpful context or acceptance notes.", defaultValue: "" },
175
- { name: "status", type: "backlog|focus|in_progress|blocked|done", required: false, description: "Board lane or completion state.", enumValues: ["backlog", "focus", "in_progress", "blocked", "done"], defaultValue: "backlog" },
176
- { name: "priority", type: "low|medium|high|critical", required: false, description: "Relative urgency.", enumValues: ["low", "medium", "high", "critical"], defaultValue: "medium" },
177
- { name: "owner", type: "string", required: false, description: "Human-facing owner label.", defaultValue: "Albert" },
178
- { name: "goalId", type: "string|null", required: false, description: "Linked goal id.", defaultValue: null, nullable: true },
179
- { name: "projectId", type: "string|null", required: false, description: "Linked project id.", defaultValue: null, nullable: true },
180
- { name: "dueDate", type: "YYYY-MM-DD|null", required: false, description: "Optional due date.", defaultValue: null, nullable: true },
181
- { name: "effort", type: "light|deep|marathon", required: false, description: "How heavy the task feels.", enumValues: ["light", "deep", "marathon"], defaultValue: "deep" },
182
- { name: "energy", type: "low|steady|high", required: false, description: "Energy demand.", enumValues: ["low", "steady", "high"], defaultValue: "steady" },
183
- { name: "points", type: "integer", required: false, description: "Reward value for the task.", defaultValue: 40 },
184
- { name: "sortOrder", type: "integer", required: false, description: "Lane ordering hint when set explicitly." },
185
- { name: "tagIds", type: "string[]", required: false, description: "Existing tag ids linked to the task.", defaultValue: [] },
186
- { name: "notes", type: "Array<{ contentMarkdown, author?, links? }>", required: false, description: "Optional nested notes that will auto-link to the new task.", defaultValue: [] }
606
+ {
607
+ name: "taskId",
608
+ type: "string",
609
+ required: true,
610
+ description: "Linked task id."
611
+ },
612
+ {
613
+ name: "projectId",
614
+ type: "string|null",
615
+ required: false,
616
+ description: "Optional parent project id.",
617
+ defaultValue: null,
618
+ nullable: true
619
+ },
620
+ {
621
+ name: "title",
622
+ type: "string",
623
+ required: true,
624
+ description: "Timebox title shown on the calendar."
625
+ },
626
+ {
627
+ name: "startsAt",
628
+ type: "ISO datetime",
629
+ required: true,
630
+ description: "Start instant in ISO-8601 form."
631
+ },
632
+ {
633
+ name: "endsAt",
634
+ type: "ISO datetime",
635
+ required: true,
636
+ description: "End instant in ISO-8601 form."
637
+ },
638
+ {
639
+ name: "source",
640
+ type: "manual|suggested|live_run",
641
+ required: false,
642
+ description: "How the timebox was created.",
643
+ enumValues: ["manual", "suggested", "live_run"],
644
+ defaultValue: "manual"
645
+ },
646
+ {
647
+ name: "status",
648
+ type: "planned|active|completed|cancelled",
649
+ required: false,
650
+ description: "Current timebox state.",
651
+ enumValues: ["planned", "active", "completed", "cancelled"],
652
+ defaultValue: "planned"
653
+ },
654
+ {
655
+ name: "overrideReason",
656
+ type: "string|null",
657
+ required: false,
658
+ description: "Audited reason when the slot overrides a blocked context.",
659
+ defaultValue: null,
660
+ nullable: true
661
+ }
662
+ ]
663
+ },
664
+ {
665
+ entityType: "habit",
666
+ purpose: "A recurring commitment or recurring slip with explicit cadence, graph links, and XP consequences.",
667
+ minimumCreateFields: ["title"],
668
+ relationshipRules: [
669
+ "Habits can link directly to goals, projects, tasks, values, patterns, behaviors, beliefs, modes, and trigger reports.",
670
+ "Habits are recurring records, not task variants, and they participate in search, notes, delete/restore, and XP.",
671
+ "linkedBehaviorId remains a compatibility alias; linkedBehaviorIds is the canonical array form."
672
+ ],
673
+ searchHints: [
674
+ "Search by title before creating a duplicate habit.",
675
+ "Use linkedTo when the habit should already be attached to a goal, project, task, or Psyche entity."
676
+ ],
677
+ examples: [
678
+ '{"title":"Morning training","frequency":"daily","polarity":"positive","linkedGoalIds":["goal_train_body"],"linkedValueIds":["value_steadiness"],"linkedBehaviorIds":["behavior_regulating_walk"]}'
679
+ ],
680
+ fieldGuide: [
681
+ {
682
+ name: "title",
683
+ type: "string",
684
+ required: true,
685
+ description: "Concrete recurring behavior label."
686
+ },
687
+ {
688
+ name: "description",
689
+ type: "string",
690
+ required: false,
691
+ description: "Markdown definition of what counts as success or failure for this habit.",
692
+ defaultValue: ""
693
+ },
694
+ {
695
+ name: "status",
696
+ type: "active|paused|archived",
697
+ required: false,
698
+ description: "Lifecycle state.",
699
+ enumValues: ["active", "paused", "archived"],
700
+ defaultValue: "active"
701
+ },
702
+ {
703
+ name: "polarity",
704
+ type: "positive|negative",
705
+ required: false,
706
+ description: "Whether doing the behavior is aligned or misaligned.",
707
+ enumValues: ["positive", "negative"],
708
+ defaultValue: "positive"
709
+ },
710
+ {
711
+ name: "frequency",
712
+ type: "daily|weekly",
713
+ required: false,
714
+ description: "Recurrence cadence.",
715
+ enumValues: ["daily", "weekly"],
716
+ defaultValue: "daily"
717
+ },
718
+ {
719
+ name: "targetCount",
720
+ type: "integer",
721
+ required: false,
722
+ description: "How many repetitions define the cadence window.",
723
+ defaultValue: 1
724
+ },
725
+ {
726
+ name: "weekDays",
727
+ type: "integer[]",
728
+ required: false,
729
+ description: "Weekday numbers for weekly habits where Monday is 1 and Sunday is 0.",
730
+ defaultValue: []
731
+ },
732
+ {
733
+ name: "linkedGoalIds",
734
+ type: "string[]",
735
+ required: false,
736
+ description: "Linked goal ids.",
737
+ defaultValue: []
738
+ },
739
+ {
740
+ name: "linkedProjectIds",
741
+ type: "string[]",
742
+ required: false,
743
+ description: "Linked project ids.",
744
+ defaultValue: []
745
+ },
746
+ {
747
+ name: "linkedTaskIds",
748
+ type: "string[]",
749
+ required: false,
750
+ description: "Linked task ids.",
751
+ defaultValue: []
752
+ },
753
+ {
754
+ name: "linkedValueIds",
755
+ type: "string[]",
756
+ required: false,
757
+ description: "Linked value ids.",
758
+ defaultValue: []
759
+ },
760
+ {
761
+ name: "linkedPatternIds",
762
+ type: "string[]",
763
+ required: false,
764
+ description: "Linked pattern ids.",
765
+ defaultValue: []
766
+ },
767
+ {
768
+ name: "linkedBehaviorIds",
769
+ type: "string[]",
770
+ required: false,
771
+ description: "Canonical linked behavior ids.",
772
+ defaultValue: []
773
+ },
774
+ {
775
+ name: "linkedBehaviorId",
776
+ type: "string|null",
777
+ required: false,
778
+ description: "Compatibility alias for the first linked behavior id.",
779
+ defaultValue: null,
780
+ nullable: true
781
+ },
782
+ {
783
+ name: "linkedBeliefIds",
784
+ type: "string[]",
785
+ required: false,
786
+ description: "Linked belief ids.",
787
+ defaultValue: []
788
+ },
789
+ {
790
+ name: "linkedModeIds",
791
+ type: "string[]",
792
+ required: false,
793
+ description: "Linked mode ids.",
794
+ defaultValue: []
795
+ },
796
+ {
797
+ name: "linkedReportIds",
798
+ type: "string[]",
799
+ required: false,
800
+ description: "Linked trigger report ids.",
801
+ defaultValue: []
802
+ },
803
+ {
804
+ name: "rewardXp",
805
+ type: "integer",
806
+ required: false,
807
+ description: "XP granted on aligned check-ins.",
808
+ defaultValue: 12
809
+ },
810
+ {
811
+ name: "penaltyXp",
812
+ type: "integer",
813
+ required: false,
814
+ description: "XP removed on misaligned check-ins.",
815
+ defaultValue: 8
816
+ }
187
817
  ]
188
818
  },
189
819
  {
190
820
  entityType: "note",
191
- purpose: "A Markdown note that can link to one or many Forge entities.",
821
+ purpose: "A first-class Markdown note entity that can link to one or many Forge entities.",
192
822
  minimumCreateFields: ["contentMarkdown", "links"],
193
823
  relationshipRules: [
194
824
  "Notes can link to goals, projects, tasks, Psyche records, and other supported Forge entities.",
195
- "When nested under another create flow, notes auto-link to that new entity and can optionally include extra links."
825
+ "When nested under another create flow, notes auto-link to that new entity and can optionally include extra links.",
826
+ "Agents can also create standalone notes directly through forge_create_entities with entityType note."
827
+ ],
828
+ searchHints: [
829
+ "Search by Markdown content, author, or linked entity before creating a duplicate note."
196
830
  ],
197
- searchHints: ["Search by Markdown content, author, or linked entity before creating a duplicate note."],
198
831
  examples: [
199
832
  '{"contentMarkdown":"Finished the review pass and captured the remaining edge cases.","links":[{"entityType":"task","entityId":"task_123"}]}',
200
- '{"contentMarkdown":"Observed a stronger protector response after the meeting.","author":"forge-agent","links":[{"entityType":"trigger_report","entityId":"report_123"},{"entityType":"behavior_pattern","entityId":"pattern_123"}]}'
833
+ '{"contentMarkdown":"Observed a stronger protector response after the meeting.","author":"forge-agent","tags":["Short-term memory","therapy"],"links":[{"entityType":"trigger_report","entityId":"report_123"},{"entityType":"behavior_pattern","entityId":"pattern_123"}]}',
834
+ '{"contentMarkdown":"Scratch capture for what I am actively holding in mind.","tags":["Working memory","handoff"],"destroyAt":"2026-04-04T12:00:00.000Z","links":[{"entityType":"task","entityId":"task_123"}]}'
201
835
  ],
202
836
  fieldGuide: [
203
- { name: "contentMarkdown", type: "string", required: true, description: "Markdown body of the note." },
204
- { name: "author", type: "string|null", required: false, description: "Optional display author for the note.", defaultValue: null, nullable: true },
205
- { name: "links", type: "Array<{ entityType, entityId, anchorKey? }>", required: true, description: "Entities this note should link to." }
837
+ {
838
+ name: "contentMarkdown",
839
+ type: "string",
840
+ required: true,
841
+ description: "Markdown body of the note."
842
+ },
843
+ {
844
+ name: "author",
845
+ type: "string|null",
846
+ required: false,
847
+ description: "Optional display author for the note.",
848
+ defaultValue: null,
849
+ nullable: true
850
+ },
851
+ {
852
+ name: "tags",
853
+ type: "string[]",
854
+ required: false,
855
+ description: "Optional note-owned tags such as Working memory, Short-term memory, Episodic memory, Semantic memory, Procedural memory, or custom labels.",
856
+ defaultValue: []
857
+ },
858
+ {
859
+ name: "destroyAt",
860
+ type: "ISO datetime|null",
861
+ required: false,
862
+ description: "Optional auto-destroy timestamp. If set, Forge deletes the note after that time as ephemeral memory.",
863
+ defaultValue: null,
864
+ nullable: true
865
+ },
866
+ {
867
+ name: "links",
868
+ type: "Array<{ entityType, entityId, anchorKey? }>",
869
+ required: true,
870
+ description: "Entities this note should link to."
871
+ }
206
872
  ]
207
873
  },
208
874
  {
@@ -213,19 +879,83 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
213
879
  "Insights can optionally point at one entity through entityType and entityId.",
214
880
  "Use insights for interpretation or advice, not as a replacement for goals, tasks, or trigger reports."
215
881
  ],
216
- searchHints: ["Search recent insights before posting a new one if the same pattern may already be captured."],
217
- examples: ['{"entityType":"goal","entityId":"goal_create_meaningfully","title":"Admin drag is masking momentum","summary":"Creative progress is happening, but admin cleanup keeps interrupting it.","recommendation":"Protect one clean creative block and isolate admin into a separate recurring task."}'],
882
+ searchHints: [
883
+ "Search recent insights before posting a new one if the same pattern may already be captured."
884
+ ],
885
+ examples: [
886
+ '{"entityType":"goal","entityId":"goal_create_meaningfully","title":"Admin drag is masking momentum","summary":"Creative progress is happening, but admin cleanup keeps interrupting it.","recommendation":"Protect one clean creative block and isolate admin into a separate recurring task."}'
887
+ ],
218
888
  fieldGuide: [
219
- { name: "entityType", type: "string|null", required: false, description: "Optional linked entity type.", defaultValue: null, nullable: true },
220
- { name: "entityId", type: "string|null", required: false, description: "Optional linked entity id.", defaultValue: null, nullable: true },
221
- { name: "timeframeLabel", type: "string|null", required: false, description: "Optional time window label.", defaultValue: null, nullable: true },
222
- { name: "title", type: "string", required: true, description: "Insight title." },
223
- { name: "summary", type: "string", required: true, description: "Short explanation of the pattern or tension." },
224
- { name: "recommendation", type: "string", required: true, description: "Actionable next move or reframing." },
225
- { name: "rationale", type: "string", required: false, description: "Why this insight is grounded in the data.", defaultValue: "" },
226
- { name: "confidence", type: "number", required: false, description: "Confidence from 0 to 1.", defaultValue: 0.7 },
227
- { name: "visibility", type: "string", required: false, description: "Visibility mode for the insight.", defaultValue: "visible" },
228
- { name: "ctaLabel", type: "string", required: false, description: "CTA shown in the UI.", defaultValue: "Review insight" }
889
+ {
890
+ name: "entityType",
891
+ type: "string|null",
892
+ required: false,
893
+ description: "Optional linked entity type.",
894
+ defaultValue: null,
895
+ nullable: true
896
+ },
897
+ {
898
+ name: "entityId",
899
+ type: "string|null",
900
+ required: false,
901
+ description: "Optional linked entity id.",
902
+ defaultValue: null,
903
+ nullable: true
904
+ },
905
+ {
906
+ name: "timeframeLabel",
907
+ type: "string|null",
908
+ required: false,
909
+ description: "Optional time window label.",
910
+ defaultValue: null,
911
+ nullable: true
912
+ },
913
+ {
914
+ name: "title",
915
+ type: "string",
916
+ required: true,
917
+ description: "Insight title."
918
+ },
919
+ {
920
+ name: "summary",
921
+ type: "string",
922
+ required: true,
923
+ description: "Short explanation of the pattern or tension."
924
+ },
925
+ {
926
+ name: "recommendation",
927
+ type: "string",
928
+ required: true,
929
+ description: "Actionable next move or reframing."
930
+ },
931
+ {
932
+ name: "rationale",
933
+ type: "string",
934
+ required: false,
935
+ description: "Why this insight is grounded in the data.",
936
+ defaultValue: ""
937
+ },
938
+ {
939
+ name: "confidence",
940
+ type: "number",
941
+ required: false,
942
+ description: "Confidence from 0 to 1.",
943
+ defaultValue: 0.7
944
+ },
945
+ {
946
+ name: "visibility",
947
+ type: "string",
948
+ required: false,
949
+ description: "Visibility mode for the insight.",
950
+ defaultValue: "visible"
951
+ },
952
+ {
953
+ name: "ctaLabel",
954
+ type: "string",
955
+ required: false,
956
+ description: "CTA shown in the UI.",
957
+ defaultValue: "Review insight"
958
+ }
229
959
  ]
230
960
  },
231
961
  {
@@ -236,10 +966,24 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
236
966
  "Trigger reports can reference one event type through eventTypeId.",
237
967
  "Use event types to normalize repeated report categories instead of inventing new wording every time."
238
968
  ],
239
- searchHints: ["Search by label before creating a new event type.", "Prefer existing event types when one clearly fits the situation."],
969
+ searchHints: [
970
+ "Search by label before creating a new event type.",
971
+ "Prefer existing event types when one clearly fits the situation."
972
+ ],
240
973
  fieldGuide: [
241
- { name: "label", type: "string", required: true, description: "Human-readable event type label." },
242
- { name: "description", type: "string", required: false, description: "What kind of incident this event type represents.", defaultValue: "" }
974
+ {
975
+ name: "label",
976
+ type: "string",
977
+ required: true,
978
+ description: "Human-readable event type label."
979
+ },
980
+ {
981
+ name: "description",
982
+ type: "string",
983
+ required: false,
984
+ description: "What kind of incident this event type represents.",
985
+ defaultValue: ""
986
+ }
243
987
  ]
244
988
  },
245
989
  {
@@ -250,11 +994,31 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
250
994
  "Trigger report emotions can reference an emotion definition through emotionDefinitionId.",
251
995
  "Use emotion definitions to normalize repeated emotional labels across reports."
252
996
  ],
253
- searchHints: ["Search by label before creating a new emotion definition.", "Prefer an existing emotion definition when the label already captures the feeling well."],
997
+ searchHints: [
998
+ "Search by label before creating a new emotion definition.",
999
+ "Prefer an existing emotion definition when the label already captures the feeling well."
1000
+ ],
254
1001
  fieldGuide: [
255
- { name: "label", type: "string", required: true, description: "Emotion label." },
256
- { name: "description", type: "string", required: false, description: "What this emotion label is meant to capture.", defaultValue: "" },
257
- { name: "category", type: "string", required: false, description: "Optional grouping such as threat, grief, anger, or connection.", defaultValue: "" }
1002
+ {
1003
+ name: "label",
1004
+ type: "string",
1005
+ required: true,
1006
+ description: "Emotion label."
1007
+ },
1008
+ {
1009
+ name: "description",
1010
+ type: "string",
1011
+ required: false,
1012
+ description: "What this emotion label is meant to capture.",
1013
+ defaultValue: ""
1014
+ },
1015
+ {
1016
+ name: "category",
1017
+ type: "string",
1018
+ required: false,
1019
+ description: "Optional grouping such as threat, grief, anger, or connection.",
1020
+ defaultValue: ""
1021
+ }
258
1022
  ]
259
1023
  },
260
1024
  {
@@ -265,17 +1029,69 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
265
1029
  "Values can link to goals, projects, and tasks.",
266
1030
  "Patterns, behaviors, beliefs, and reports can all point back to values."
267
1031
  ],
268
- searchHints: ["Search by title before creating a new value.", "Use linkedTo if the value should already be attached to a goal or task."],
269
- examples: ['{"title":"Steadiness","valuedDirection":"Respond calmly instead of collapsing or reacting fast.","whyItMatters":"I want to stay grounded in relationships and work."}'],
1032
+ searchHints: [
1033
+ "Search by title before creating a new value.",
1034
+ "Use linkedTo if the value should already be attached to a goal or task."
1035
+ ],
1036
+ examples: [
1037
+ '{"title":"Steadiness","valuedDirection":"Respond calmly instead of collapsing or reacting fast.","whyItMatters":"I want to stay grounded in relationships and work."}'
1038
+ ],
270
1039
  fieldGuide: [
271
- { name: "title", type: "string", required: true, description: "Value name." },
272
- { name: "description", type: "string", required: false, description: "What the value means in practice.", defaultValue: "" },
273
- { name: "valuedDirection", type: "string", required: false, description: "How the user wants to live or act when guided by this value.", defaultValue: "" },
274
- { name: "whyItMatters", type: "string", required: false, description: "Why the value matters to the user.", defaultValue: "" },
275
- { name: "linkedGoalIds", type: "string[]", required: false, description: "Linked goal ids.", defaultValue: [] },
276
- { name: "linkedProjectIds", type: "string[]", required: false, description: "Linked project ids.", defaultValue: [] },
277
- { name: "linkedTaskIds", type: "string[]", required: false, description: "Linked task ids.", defaultValue: [] },
278
- { name: "committedActions", type: "string[]", required: false, description: "Small concrete actions that enact the value.", defaultValue: [] }
1040
+ {
1041
+ name: "title",
1042
+ type: "string",
1043
+ required: true,
1044
+ description: "Value name."
1045
+ },
1046
+ {
1047
+ name: "description",
1048
+ type: "string",
1049
+ required: false,
1050
+ description: "What the value means in practice.",
1051
+ defaultValue: ""
1052
+ },
1053
+ {
1054
+ name: "valuedDirection",
1055
+ type: "string",
1056
+ required: false,
1057
+ description: "How the user wants to live or act when guided by this value.",
1058
+ defaultValue: ""
1059
+ },
1060
+ {
1061
+ name: "whyItMatters",
1062
+ type: "string",
1063
+ required: false,
1064
+ description: "Why the value matters to the user.",
1065
+ defaultValue: ""
1066
+ },
1067
+ {
1068
+ name: "linkedGoalIds",
1069
+ type: "string[]",
1070
+ required: false,
1071
+ description: "Linked goal ids.",
1072
+ defaultValue: []
1073
+ },
1074
+ {
1075
+ name: "linkedProjectIds",
1076
+ type: "string[]",
1077
+ required: false,
1078
+ description: "Linked project ids.",
1079
+ defaultValue: []
1080
+ },
1081
+ {
1082
+ name: "linkedTaskIds",
1083
+ type: "string[]",
1084
+ required: false,
1085
+ description: "Linked task ids.",
1086
+ defaultValue: []
1087
+ },
1088
+ {
1089
+ name: "committedActions",
1090
+ type: "string[]",
1091
+ required: false,
1092
+ description: "Small concrete actions that enact the value.",
1093
+ defaultValue: []
1094
+ }
279
1095
  ]
280
1096
  },
281
1097
  {
@@ -286,21 +1102,96 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
286
1102
  "Patterns can link to values, beliefs, and modes.",
287
1103
  "Trigger reports can link back to patterns they instantiate."
288
1104
  ],
289
- searchHints: ["Search by title or by trigger language before creating a new pattern."],
290
- examples: ['{"title":"Late-night father text freeze","cueContexts":["Father texts late at night"],"targetBehavior":"Freeze, avoid replying, and doomscroll","shortTermPayoff":"Avoids immediate overwhelm","longTermCost":"Sleep loss, guilt, and dread","preferredResponse":"Pause, regulate, and reply on my own terms the next morning"}'],
1105
+ searchHints: [
1106
+ "Search by title or by trigger language before creating a new pattern."
1107
+ ],
1108
+ examples: [
1109
+ '{"title":"Late-night father text freeze","cueContexts":["Father texts late at night"],"targetBehavior":"Freeze, avoid replying, and doomscroll","shortTermPayoff":"Avoids immediate overwhelm","longTermCost":"Sleep loss, guilt, and dread","preferredResponse":"Pause, regulate, and reply on my own terms the next morning"}'
1110
+ ],
291
1111
  fieldGuide: [
292
- { name: "title", type: "string", required: true, description: "Short pattern name." },
293
- { name: "description", type: "string", required: false, description: "What usually happens in this loop.", defaultValue: "" },
294
- { name: "targetBehavior", type: "string", required: false, description: "The visible behavior this pattern tends to produce.", defaultValue: "" },
295
- { name: "cueContexts", type: "string[]", required: false, description: "Typical cues, contexts, or triggers.", defaultValue: [] },
296
- { name: "shortTermPayoff", type: "string", required: false, description: "What the loop gives immediately.", defaultValue: "" },
297
- { name: "longTermCost", type: "string", required: false, description: "What the loop costs later.", defaultValue: "" },
298
- { name: "preferredResponse", type: "string", required: false, description: "Preferred alternative response.", defaultValue: "" },
299
- { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
300
- { name: "linkedSchemaLabels", type: "string[]", required: false, description: "Schema labels involved in the pattern.", defaultValue: [] },
301
- { name: "linkedModeLabels", type: "string[]", required: false, description: "Mode labels involved in the pattern.", defaultValue: [] },
302
- { name: "linkedModeIds", type: "string[]", required: false, description: "Linked mode ids.", defaultValue: [] },
303
- { name: "linkedBeliefIds", type: "string[]", required: false, description: "Linked belief ids.", defaultValue: [] }
1112
+ {
1113
+ name: "title",
1114
+ type: "string",
1115
+ required: true,
1116
+ description: "Short pattern name."
1117
+ },
1118
+ {
1119
+ name: "description",
1120
+ type: "string",
1121
+ required: false,
1122
+ description: "What usually happens in this loop.",
1123
+ defaultValue: ""
1124
+ },
1125
+ {
1126
+ name: "targetBehavior",
1127
+ type: "string",
1128
+ required: false,
1129
+ description: "The visible behavior this pattern tends to produce.",
1130
+ defaultValue: ""
1131
+ },
1132
+ {
1133
+ name: "cueContexts",
1134
+ type: "string[]",
1135
+ required: false,
1136
+ description: "Typical cues, contexts, or triggers.",
1137
+ defaultValue: []
1138
+ },
1139
+ {
1140
+ name: "shortTermPayoff",
1141
+ type: "string",
1142
+ required: false,
1143
+ description: "What the loop gives immediately.",
1144
+ defaultValue: ""
1145
+ },
1146
+ {
1147
+ name: "longTermCost",
1148
+ type: "string",
1149
+ required: false,
1150
+ description: "What the loop costs later.",
1151
+ defaultValue: ""
1152
+ },
1153
+ {
1154
+ name: "preferredResponse",
1155
+ type: "string",
1156
+ required: false,
1157
+ description: "Preferred alternative response.",
1158
+ defaultValue: ""
1159
+ },
1160
+ {
1161
+ name: "linkedValueIds",
1162
+ type: "string[]",
1163
+ required: false,
1164
+ description: "Linked value ids.",
1165
+ defaultValue: []
1166
+ },
1167
+ {
1168
+ name: "linkedSchemaLabels",
1169
+ type: "string[]",
1170
+ required: false,
1171
+ description: "Schema labels involved in the pattern.",
1172
+ defaultValue: []
1173
+ },
1174
+ {
1175
+ name: "linkedModeLabels",
1176
+ type: "string[]",
1177
+ required: false,
1178
+ description: "Mode labels involved in the pattern.",
1179
+ defaultValue: []
1180
+ },
1181
+ {
1182
+ name: "linkedModeIds",
1183
+ type: "string[]",
1184
+ required: false,
1185
+ description: "Linked mode ids.",
1186
+ defaultValue: []
1187
+ },
1188
+ {
1189
+ name: "linkedBeliefIds",
1190
+ type: "string[]",
1191
+ required: false,
1192
+ description: "Linked belief ids.",
1193
+ defaultValue: []
1194
+ }
304
1195
  ]
305
1196
  },
306
1197
  {
@@ -312,21 +1203,100 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
312
1203
  "Trigger reports can link to behaviors they contained."
313
1204
  ],
314
1205
  searchHints: ["Search by title and kind before creating a new behavior."],
315
- examples: ['{"kind":"away","title":"Doomscroll after conflict cue","commonCues":["Received a critical text"],"shortTermPayoff":"Numbs the anxiety","longTermCost":"Loses time and deepens shame","replacementMove":"Put phone down and take one slow lap outside"}'],
1206
+ examples: [
1207
+ '{"kind":"away","title":"Doomscroll after conflict cue","commonCues":["Received a critical text"],"shortTermPayoff":"Numbs the anxiety","longTermCost":"Loses time and deepens shame","replacementMove":"Put phone down and take one slow lap outside"}'
1208
+ ],
316
1209
  fieldGuide: [
317
- { name: "kind", type: "away|committed|recovery", required: true, description: "Whether the behavior moves away from values, toward them, or repairs after rupture.", enumValues: ["away", "committed", "recovery"] },
318
- { name: "title", type: "string", required: true, description: "Behavior label." },
319
- { name: "description", type: "string", required: false, description: "What the behavior looks like.", defaultValue: "" },
320
- { name: "commonCues", type: "string[]", required: false, description: "Typical cues for this behavior.", defaultValue: [] },
321
- { name: "urgeStory", type: "string", required: false, description: "What the inner urge or story feels like.", defaultValue: "" },
322
- { name: "shortTermPayoff", type: "string", required: false, description: "Immediate payoff.", defaultValue: "" },
323
- { name: "longTermCost", type: "string", required: false, description: "Longer-term cost.", defaultValue: "" },
324
- { name: "replacementMove", type: "string", required: false, description: "Preferred replacement move.", defaultValue: "" },
325
- { name: "repairPlan", type: "string", required: false, description: "Repair plan after the behavior occurs.", defaultValue: "" },
326
- { name: "linkedPatternIds", type: "string[]", required: false, description: "Linked behavior pattern ids.", defaultValue: [] },
327
- { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
328
- { name: "linkedSchemaIds", type: "string[]", required: false, description: "Linked schema ids.", defaultValue: [] },
329
- { name: "linkedModeIds", type: "string[]", required: false, description: "Linked mode ids.", defaultValue: [] }
1210
+ {
1211
+ name: "kind",
1212
+ type: "away|committed|recovery",
1213
+ required: true,
1214
+ description: "Whether the behavior moves away from values, toward them, or repairs after rupture.",
1215
+ enumValues: ["away", "committed", "recovery"]
1216
+ },
1217
+ {
1218
+ name: "title",
1219
+ type: "string",
1220
+ required: true,
1221
+ description: "Behavior label."
1222
+ },
1223
+ {
1224
+ name: "description",
1225
+ type: "string",
1226
+ required: false,
1227
+ description: "What the behavior looks like.",
1228
+ defaultValue: ""
1229
+ },
1230
+ {
1231
+ name: "commonCues",
1232
+ type: "string[]",
1233
+ required: false,
1234
+ description: "Typical cues for this behavior.",
1235
+ defaultValue: []
1236
+ },
1237
+ {
1238
+ name: "urgeStory",
1239
+ type: "string",
1240
+ required: false,
1241
+ description: "What the inner urge or story feels like.",
1242
+ defaultValue: ""
1243
+ },
1244
+ {
1245
+ name: "shortTermPayoff",
1246
+ type: "string",
1247
+ required: false,
1248
+ description: "Immediate payoff.",
1249
+ defaultValue: ""
1250
+ },
1251
+ {
1252
+ name: "longTermCost",
1253
+ type: "string",
1254
+ required: false,
1255
+ description: "Longer-term cost.",
1256
+ defaultValue: ""
1257
+ },
1258
+ {
1259
+ name: "replacementMove",
1260
+ type: "string",
1261
+ required: false,
1262
+ description: "Preferred replacement move.",
1263
+ defaultValue: ""
1264
+ },
1265
+ {
1266
+ name: "repairPlan",
1267
+ type: "string",
1268
+ required: false,
1269
+ description: "Repair plan after the behavior occurs.",
1270
+ defaultValue: ""
1271
+ },
1272
+ {
1273
+ name: "linkedPatternIds",
1274
+ type: "string[]",
1275
+ required: false,
1276
+ description: "Linked behavior pattern ids.",
1277
+ defaultValue: []
1278
+ },
1279
+ {
1280
+ name: "linkedValueIds",
1281
+ type: "string[]",
1282
+ required: false,
1283
+ description: "Linked value ids.",
1284
+ defaultValue: []
1285
+ },
1286
+ {
1287
+ name: "linkedSchemaIds",
1288
+ type: "string[]",
1289
+ required: false,
1290
+ description: "Linked schema ids.",
1291
+ defaultValue: []
1292
+ },
1293
+ {
1294
+ name: "linkedModeIds",
1295
+ type: "string[]",
1296
+ required: false,
1297
+ description: "Linked mode ids.",
1298
+ defaultValue: []
1299
+ }
330
1300
  ]
331
1301
  },
332
1302
  {
@@ -337,21 +1307,97 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
337
1307
  "Beliefs can link to values, behaviors, modes, and trigger reports.",
338
1308
  "Behavior patterns can point to beliefs that keep the loop alive."
339
1309
  ],
340
- searchHints: ["Search by statement or known schema theme before creating a new belief entry."],
341
- examples: ['{"statement":"If I disappoint people, they will leave me.","beliefType":"conditional","confidence":82,"evidenceFor":["People got cold when I failed them before"],"evidenceAgainst":["Some people stayed with me even after conflict"],"flexibleAlternative":"Disappointing someone can strain a relationship, but it does not automatically mean abandonment."}'],
1310
+ searchHints: [
1311
+ "Search by statement or known schema theme before creating a new belief entry."
1312
+ ],
1313
+ examples: [
1314
+ '{"statement":"If I disappoint people, they will leave me.","beliefType":"conditional","confidence":82,"evidenceFor":["People got cold when I failed them before"],"evidenceAgainst":["Some people stayed with me even after conflict"],"flexibleAlternative":"Disappointing someone can strain a relationship, but it does not automatically mean abandonment."}'
1315
+ ],
342
1316
  fieldGuide: [
343
- { name: "schemaId", type: "string|null", required: false, description: "Optional linked schema catalog id.", defaultValue: null, nullable: true },
344
- { name: "statement", type: "string", required: true, description: "Belief statement in the user's own words." },
345
- { name: "beliefType", type: "absolute|conditional", required: true, description: "Whether the belief is absolute or if-then shaped.", enumValues: ["absolute", "conditional"] },
346
- { name: "originNote", type: "string", required: false, description: "Where the belief seems to come from.", defaultValue: "" },
347
- { name: "confidence", type: "integer", required: false, description: "How strongly the belief feels true from 0 to 100.", defaultValue: 60 },
348
- { name: "evidenceFor", type: "string[]", required: false, description: "Evidence that seems to support the belief.", defaultValue: [] },
349
- { name: "evidenceAgainst", type: "string[]", required: false, description: "Evidence that weakens the belief.", defaultValue: [] },
350
- { name: "flexibleAlternative", type: "string", required: false, description: "More flexible alternative belief.", defaultValue: "" },
351
- { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
352
- { name: "linkedBehaviorIds", type: "string[]", required: false, description: "Linked behavior ids.", defaultValue: [] },
353
- { name: "linkedModeIds", type: "string[]", required: false, description: "Linked mode ids.", defaultValue: [] },
354
- { name: "linkedReportIds", type: "string[]", required: false, description: "Linked trigger report ids.", defaultValue: [] }
1317
+ {
1318
+ name: "schemaId",
1319
+ type: "string|null",
1320
+ required: false,
1321
+ description: "Optional linked schema catalog id.",
1322
+ defaultValue: null,
1323
+ nullable: true
1324
+ },
1325
+ {
1326
+ name: "statement",
1327
+ type: "string",
1328
+ required: true,
1329
+ description: "Belief statement in the user's own words."
1330
+ },
1331
+ {
1332
+ name: "beliefType",
1333
+ type: "absolute|conditional",
1334
+ required: true,
1335
+ description: "Whether the belief is absolute or if-then shaped.",
1336
+ enumValues: ["absolute", "conditional"]
1337
+ },
1338
+ {
1339
+ name: "originNote",
1340
+ type: "string",
1341
+ required: false,
1342
+ description: "Where the belief seems to come from.",
1343
+ defaultValue: ""
1344
+ },
1345
+ {
1346
+ name: "confidence",
1347
+ type: "integer",
1348
+ required: false,
1349
+ description: "How strongly the belief feels true from 0 to 100.",
1350
+ defaultValue: 60
1351
+ },
1352
+ {
1353
+ name: "evidenceFor",
1354
+ type: "string[]",
1355
+ required: false,
1356
+ description: "Evidence that seems to support the belief.",
1357
+ defaultValue: []
1358
+ },
1359
+ {
1360
+ name: "evidenceAgainst",
1361
+ type: "string[]",
1362
+ required: false,
1363
+ description: "Evidence that weakens the belief.",
1364
+ defaultValue: []
1365
+ },
1366
+ {
1367
+ name: "flexibleAlternative",
1368
+ type: "string",
1369
+ required: false,
1370
+ description: "More flexible alternative belief.",
1371
+ defaultValue: ""
1372
+ },
1373
+ {
1374
+ name: "linkedValueIds",
1375
+ type: "string[]",
1376
+ required: false,
1377
+ description: "Linked value ids.",
1378
+ defaultValue: []
1379
+ },
1380
+ {
1381
+ name: "linkedBehaviorIds",
1382
+ type: "string[]",
1383
+ required: false,
1384
+ description: "Linked behavior ids.",
1385
+ defaultValue: []
1386
+ },
1387
+ {
1388
+ name: "linkedModeIds",
1389
+ type: "string[]",
1390
+ required: false,
1391
+ description: "Linked mode ids.",
1392
+ defaultValue: []
1393
+ },
1394
+ {
1395
+ name: "linkedReportIds",
1396
+ type: "string[]",
1397
+ required: false,
1398
+ description: "Linked trigger report ids.",
1399
+ defaultValue: []
1400
+ }
355
1401
  ]
356
1402
  },
357
1403
  {
@@ -362,24 +1408,124 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
362
1408
  "Modes can link to patterns, behaviors, and values.",
363
1409
  "Trigger reports can include linkedModeIds and modeOverlays that reference modes."
364
1410
  ],
365
- searchHints: ["Search by title or family before creating a new mode profile."],
366
- examples: ['{"family":"coping","title":"Cold controller","fear":"If I soften, I will be humiliated or lose control.","protectiveJob":"Stay hyper-competent and unreachable when threatened."}'],
1411
+ searchHints: [
1412
+ "Search by title or family before creating a new mode profile."
1413
+ ],
1414
+ examples: [
1415
+ '{"family":"coping","title":"Cold controller","fear":"If I soften, I will be humiliated or lose control.","protectiveJob":"Stay hyper-competent and unreachable when threatened."}'
1416
+ ],
367
1417
  fieldGuide: [
368
- { name: "family", type: "coping|child|critic_parent|healthy_adult|happy_child", required: true, description: "Mode family.", enumValues: ["coping", "child", "critic_parent", "healthy_adult", "happy_child"] },
369
- { name: "title", type: "string", required: true, description: "Mode title." },
370
- { name: "archetype", type: "string", required: false, description: "Optional archetype label.", defaultValue: "" },
371
- { name: "persona", type: "string", required: false, description: "Narrative or felt sense of the mode.", defaultValue: "" },
372
- { name: "imagery", type: "string", required: false, description: "Imagery associated with the mode.", defaultValue: "" },
373
- { name: "symbolicForm", type: "string", required: false, description: "Symbolic form or metaphor.", defaultValue: "" },
374
- { name: "facialExpression", type: "string", required: false, description: "Typical facial expression or posture.", defaultValue: "" },
375
- { name: "fear", type: "string", required: false, description: "Core fear carried by the mode.", defaultValue: "" },
376
- { name: "burden", type: "string", required: false, description: "Burden or pain the mode carries.", defaultValue: "" },
377
- { name: "protectiveJob", type: "string", required: false, description: "What job the mode thinks it is doing.", defaultValue: "" },
378
- { name: "originContext", type: "string", required: false, description: "Where the mode seems to come from.", defaultValue: "" },
379
- { name: "firstAppearanceAt", type: "string|null", required: false, description: "Optional first-seen marker.", defaultValue: null, nullable: true },
380
- { name: "linkedPatternIds", type: "string[]", required: false, description: "Linked pattern ids.", defaultValue: [] },
381
- { name: "linkedBehaviorIds", type: "string[]", required: false, description: "Linked behavior ids.", defaultValue: [] },
382
- { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] }
1418
+ {
1419
+ name: "family",
1420
+ type: "coping|child|critic_parent|healthy_adult|happy_child",
1421
+ required: true,
1422
+ description: "Mode family.",
1423
+ enumValues: [
1424
+ "coping",
1425
+ "child",
1426
+ "critic_parent",
1427
+ "healthy_adult",
1428
+ "happy_child"
1429
+ ]
1430
+ },
1431
+ {
1432
+ name: "title",
1433
+ type: "string",
1434
+ required: true,
1435
+ description: "Mode title."
1436
+ },
1437
+ {
1438
+ name: "archetype",
1439
+ type: "string",
1440
+ required: false,
1441
+ description: "Optional archetype label.",
1442
+ defaultValue: ""
1443
+ },
1444
+ {
1445
+ name: "persona",
1446
+ type: "string",
1447
+ required: false,
1448
+ description: "Narrative or felt sense of the mode.",
1449
+ defaultValue: ""
1450
+ },
1451
+ {
1452
+ name: "imagery",
1453
+ type: "string",
1454
+ required: false,
1455
+ description: "Imagery associated with the mode.",
1456
+ defaultValue: ""
1457
+ },
1458
+ {
1459
+ name: "symbolicForm",
1460
+ type: "string",
1461
+ required: false,
1462
+ description: "Symbolic form or metaphor.",
1463
+ defaultValue: ""
1464
+ },
1465
+ {
1466
+ name: "facialExpression",
1467
+ type: "string",
1468
+ required: false,
1469
+ description: "Typical facial expression or posture.",
1470
+ defaultValue: ""
1471
+ },
1472
+ {
1473
+ name: "fear",
1474
+ type: "string",
1475
+ required: false,
1476
+ description: "Core fear carried by the mode.",
1477
+ defaultValue: ""
1478
+ },
1479
+ {
1480
+ name: "burden",
1481
+ type: "string",
1482
+ required: false,
1483
+ description: "Burden or pain the mode carries.",
1484
+ defaultValue: ""
1485
+ },
1486
+ {
1487
+ name: "protectiveJob",
1488
+ type: "string",
1489
+ required: false,
1490
+ description: "What job the mode thinks it is doing.",
1491
+ defaultValue: ""
1492
+ },
1493
+ {
1494
+ name: "originContext",
1495
+ type: "string",
1496
+ required: false,
1497
+ description: "Where the mode seems to come from.",
1498
+ defaultValue: ""
1499
+ },
1500
+ {
1501
+ name: "firstAppearanceAt",
1502
+ type: "string|null",
1503
+ required: false,
1504
+ description: "Optional first-seen marker.",
1505
+ defaultValue: null,
1506
+ nullable: true
1507
+ },
1508
+ {
1509
+ name: "linkedPatternIds",
1510
+ type: "string[]",
1511
+ required: false,
1512
+ description: "Linked pattern ids.",
1513
+ defaultValue: []
1514
+ },
1515
+ {
1516
+ name: "linkedBehaviorIds",
1517
+ type: "string[]",
1518
+ required: false,
1519
+ description: "Linked behavior ids.",
1520
+ defaultValue: []
1521
+ },
1522
+ {
1523
+ name: "linkedValueIds",
1524
+ type: "string[]",
1525
+ required: false,
1526
+ description: "Linked value ids.",
1527
+ defaultValue: []
1528
+ }
383
1529
  ]
384
1530
  },
385
1531
  {
@@ -390,12 +1536,31 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
390
1536
  "Mode guide sessions help the user reason toward likely modes before or alongside mode profiles.",
391
1537
  "Use mode guide sessions for guided interpretation, not as a replacement for durable mode profiles."
392
1538
  ],
393
- searchHints: ["Search by summary when revisiting a prior guided mode session."],
394
- examples: ['{"summary":"Mapping the part that takes over under criticism","answers":[{"questionKey":"felt_shift","value":"I go cold and rigid"}],"results":[{"family":"coping","archetype":"detached_protector","label":"Cold controller","confidence":0.74,"reasoning":"It distances from shame and tries to stay untouchable."}]}'],
1539
+ searchHints: [
1540
+ "Search by summary when revisiting a prior guided mode session."
1541
+ ],
1542
+ examples: [
1543
+ '{"summary":"Mapping the part that takes over under criticism","answers":[{"questionKey":"felt_shift","value":"I go cold and rigid"}],"results":[{"family":"coping","archetype":"detached_protector","label":"Cold controller","confidence":0.74,"reasoning":"It distances from shame and tries to stay untouchable."}]}'
1544
+ ],
395
1545
  fieldGuide: [
396
- { name: "summary", type: "string", required: true, description: "Short summary of what the guided session explored." },
397
- { name: "answers", type: "array", required: true, description: "List of { questionKey, value } items capturing the user's guided answers." },
398
- { name: "results", type: "array", required: true, description: "List of { family, archetype, label, confidence 0-1, reasoning } candidate mode interpretations." }
1546
+ {
1547
+ name: "summary",
1548
+ type: "string",
1549
+ required: true,
1550
+ description: "Short summary of what the guided session explored."
1551
+ },
1552
+ {
1553
+ name: "answers",
1554
+ type: "array",
1555
+ required: true,
1556
+ description: "List of { questionKey, value } items capturing the user's guided answers."
1557
+ },
1558
+ {
1559
+ name: "results",
1560
+ type: "array",
1561
+ required: true,
1562
+ description: "List of { family, archetype, label, confidence 0-1, reasoning } candidate mode interpretations."
1563
+ }
399
1564
  ]
400
1565
  },
401
1566
  {
@@ -407,31 +1572,168 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
407
1572
  "A report is the best container for one specific emotionally meaningful episode.",
408
1573
  "Use reports when you need one event chain, not just a generic pattern."
409
1574
  ],
410
- searchHints: ["Search by title, event wording, or linked entities before creating a duplicate report."],
411
- examples: ['{"title":"Partner said we need to talk and I spiraled","customEventType":"relationship threat","eventSituation":"My partner texted that we needed to talk tonight.","emotions":[{"label":"fear","intensity":85},{"label":"shame","intensity":60}],"thoughts":[{"text":"This means I messed everything up."}],"behaviors":[{"text":"Paced, catastrophized, and checked my phone repeatedly"}],"nextMoves":["Wait until we speak before predicting the outcome","Write down the facts I actually know"]}'],
1575
+ searchHints: [
1576
+ "Search by title, event wording, or linked entities before creating a duplicate report."
1577
+ ],
1578
+ examples: [
1579
+ '{"title":"Partner said we need to talk and I spiraled","customEventType":"relationship threat","eventSituation":"My partner texted that we needed to talk tonight.","emotions":[{"label":"fear","intensity":85},{"label":"shame","intensity":60}],"thoughts":[{"text":"This means I messed everything up."}],"behaviors":[{"text":"Paced, catastrophized, and checked my phone repeatedly"}],"nextMoves":["Wait until we speak before predicting the outcome","Write down the facts I actually know"]}'
1580
+ ],
412
1581
  fieldGuide: [
413
- { name: "title", type: "string", required: true, description: "Short name for the incident." },
414
- { name: "status", type: "draft|reviewed|integrated", required: false, description: "Reflection progress state.", enumValues: ["draft", "reviewed", "integrated"], defaultValue: "draft" },
415
- { name: "eventTypeId", type: "string|null", required: false, description: "Known event type id if already cataloged.", defaultValue: null, nullable: true },
416
- { name: "customEventType", type: "string", required: false, description: "Free-text event type when no existing type fits.", defaultValue: "" },
417
- { name: "eventSituation", type: "string", required: false, description: "What happened in the situation.", defaultValue: "" },
418
- { name: "occurredAt", type: "string|null", required: false, description: "When it happened.", defaultValue: null, nullable: true },
419
- { name: "emotions", type: "array", required: false, description: "List of { emotionDefinitionId|null, label, intensity 0-100, note } items.", defaultValue: [] },
420
- { name: "thoughts", type: "array", required: false, description: "List of { text, parentMode, criticMode, beliefId|null } items.", defaultValue: [] },
421
- { name: "behaviors", type: "array", required: false, description: "List of { text, mode, behaviorId|null } items.", defaultValue: [] },
422
- { name: "consequences", type: "object", required: false, description: "Object with selfShortTerm, selfLongTerm, othersShortTerm, othersLongTerm string arrays." },
423
- { name: "linkedPatternIds", type: "string[]", required: false, description: "Linked pattern ids.", defaultValue: [] },
424
- { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
425
- { name: "linkedGoalIds", type: "string[]", required: false, description: "Linked goal ids.", defaultValue: [] },
426
- { name: "linkedProjectIds", type: "string[]", required: false, description: "Linked project ids.", defaultValue: [] },
427
- { name: "linkedTaskIds", type: "string[]", required: false, description: "Linked task ids.", defaultValue: [] },
428
- { name: "linkedBehaviorIds", type: "string[]", required: false, description: "Linked behavior ids.", defaultValue: [] },
429
- { name: "linkedBeliefIds", type: "string[]", required: false, description: "Linked belief ids.", defaultValue: [] },
430
- { name: "linkedModeIds", type: "string[]", required: false, description: "Linked mode ids.", defaultValue: [] },
431
- { name: "modeOverlays", type: "string[]", required: false, description: "Extra mode labels noticed during the incident.", defaultValue: [] },
432
- { name: "schemaLinks", type: "string[]", required: false, description: "Schema names or themes that seem related to the incident.", defaultValue: [] },
433
- { name: "modeTimeline", type: "array", required: false, description: "List of { stage, modeId|null, label, note } items describing the sequence of modes.", defaultValue: [] },
434
- { name: "nextMoves", type: "string[]", required: false, description: "Concrete next steps or repair moves.", defaultValue: [] }
1582
+ {
1583
+ name: "title",
1584
+ type: "string",
1585
+ required: true,
1586
+ description: "Short name for the incident."
1587
+ },
1588
+ {
1589
+ name: "status",
1590
+ type: "draft|reviewed|integrated",
1591
+ required: false,
1592
+ description: "Reflection progress state.",
1593
+ enumValues: ["draft", "reviewed", "integrated"],
1594
+ defaultValue: "draft"
1595
+ },
1596
+ {
1597
+ name: "eventTypeId",
1598
+ type: "string|null",
1599
+ required: false,
1600
+ description: "Known event type id if already cataloged.",
1601
+ defaultValue: null,
1602
+ nullable: true
1603
+ },
1604
+ {
1605
+ name: "customEventType",
1606
+ type: "string",
1607
+ required: false,
1608
+ description: "Free-text event type when no existing type fits.",
1609
+ defaultValue: ""
1610
+ },
1611
+ {
1612
+ name: "eventSituation",
1613
+ type: "string",
1614
+ required: false,
1615
+ description: "What happened in the situation.",
1616
+ defaultValue: ""
1617
+ },
1618
+ {
1619
+ name: "occurredAt",
1620
+ type: "string|null",
1621
+ required: false,
1622
+ description: "When it happened.",
1623
+ defaultValue: null,
1624
+ nullable: true
1625
+ },
1626
+ {
1627
+ name: "emotions",
1628
+ type: "array",
1629
+ required: false,
1630
+ description: "List of { emotionDefinitionId|null, label, intensity 0-100, note } items.",
1631
+ defaultValue: []
1632
+ },
1633
+ {
1634
+ name: "thoughts",
1635
+ type: "array",
1636
+ required: false,
1637
+ description: "List of { text, parentMode, criticMode, beliefId|null } items.",
1638
+ defaultValue: []
1639
+ },
1640
+ {
1641
+ name: "behaviors",
1642
+ type: "array",
1643
+ required: false,
1644
+ description: "List of { text, mode, behaviorId|null } items.",
1645
+ defaultValue: []
1646
+ },
1647
+ {
1648
+ name: "consequences",
1649
+ type: "object",
1650
+ required: false,
1651
+ description: "Object with selfShortTerm, selfLongTerm, othersShortTerm, othersLongTerm string arrays."
1652
+ },
1653
+ {
1654
+ name: "linkedPatternIds",
1655
+ type: "string[]",
1656
+ required: false,
1657
+ description: "Linked pattern ids.",
1658
+ defaultValue: []
1659
+ },
1660
+ {
1661
+ name: "linkedValueIds",
1662
+ type: "string[]",
1663
+ required: false,
1664
+ description: "Linked value ids.",
1665
+ defaultValue: []
1666
+ },
1667
+ {
1668
+ name: "linkedGoalIds",
1669
+ type: "string[]",
1670
+ required: false,
1671
+ description: "Linked goal ids.",
1672
+ defaultValue: []
1673
+ },
1674
+ {
1675
+ name: "linkedProjectIds",
1676
+ type: "string[]",
1677
+ required: false,
1678
+ description: "Linked project ids.",
1679
+ defaultValue: []
1680
+ },
1681
+ {
1682
+ name: "linkedTaskIds",
1683
+ type: "string[]",
1684
+ required: false,
1685
+ description: "Linked task ids.",
1686
+ defaultValue: []
1687
+ },
1688
+ {
1689
+ name: "linkedBehaviorIds",
1690
+ type: "string[]",
1691
+ required: false,
1692
+ description: "Linked behavior ids.",
1693
+ defaultValue: []
1694
+ },
1695
+ {
1696
+ name: "linkedBeliefIds",
1697
+ type: "string[]",
1698
+ required: false,
1699
+ description: "Linked belief ids.",
1700
+ defaultValue: []
1701
+ },
1702
+ {
1703
+ name: "linkedModeIds",
1704
+ type: "string[]",
1705
+ required: false,
1706
+ description: "Linked mode ids.",
1707
+ defaultValue: []
1708
+ },
1709
+ {
1710
+ name: "modeOverlays",
1711
+ type: "string[]",
1712
+ required: false,
1713
+ description: "Extra mode labels noticed during the incident.",
1714
+ defaultValue: []
1715
+ },
1716
+ {
1717
+ name: "schemaLinks",
1718
+ type: "string[]",
1719
+ required: false,
1720
+ description: "Schema names or themes that seem related to the incident.",
1721
+ defaultValue: []
1722
+ },
1723
+ {
1724
+ name: "modeTimeline",
1725
+ type: "array",
1726
+ required: false,
1727
+ description: "List of { stage, modeId|null, label, note } items describing the sequence of modes.",
1728
+ defaultValue: []
1729
+ },
1730
+ {
1731
+ name: "nextMoves",
1732
+ type: "string[]",
1733
+ required: false,
1734
+ description: "Concrete next steps or repair moves.",
1735
+ defaultValue: []
1736
+ }
435
1737
  ]
436
1738
  }
437
1739
  ];
@@ -449,7 +1751,17 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
449
1751
  "Name the preferred alternative response."
450
1752
  ],
451
1753
  requiredForCreate: ["title"],
452
- highValueOptionalFields: ["description", "targetBehavior", "cueContexts", "shortTermPayoff", "longTermCost", "preferredResponse", "linkedBeliefIds", "linkedModeIds", "linkedValueIds"],
1754
+ highValueOptionalFields: [
1755
+ "description",
1756
+ "targetBehavior",
1757
+ "cueContexts",
1758
+ "shortTermPayoff",
1759
+ "longTermCost",
1760
+ "preferredResponse",
1761
+ "linkedBeliefIds",
1762
+ "linkedModeIds",
1763
+ "linkedValueIds"
1764
+ ],
453
1765
  exampleQuestions: [
454
1766
  "What usually sets this loop off?",
455
1767
  "What do you tend to do next, outwardly or inwardly?",
@@ -475,7 +1787,17 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
475
1787
  "Link a schemaId only when a real schema catalog match is known."
476
1788
  ],
477
1789
  requiredForCreate: ["statement", "beliefType"],
478
- highValueOptionalFields: ["schemaId", "confidence", "originNote", "evidenceFor", "evidenceAgainst", "flexibleAlternative", "linkedReportIds", "linkedBehaviorIds", "linkedModeIds"],
1790
+ highValueOptionalFields: [
1791
+ "schemaId",
1792
+ "confidence",
1793
+ "originNote",
1794
+ "evidenceFor",
1795
+ "evidenceAgainst",
1796
+ "flexibleAlternative",
1797
+ "linkedReportIds",
1798
+ "linkedBehaviorIds",
1799
+ "linkedModeIds"
1800
+ ],
479
1801
  exampleQuestions: [
480
1802
  "What is the sentence your mind seems to be pushing here?",
481
1803
  "Is it more of an always/never belief, or an if-then rule?",
@@ -500,7 +1822,17 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
500
1822
  "Optionally note origin context and linked patterns or behaviors."
501
1823
  ],
502
1824
  requiredForCreate: ["family", "title"],
503
- highValueOptionalFields: ["persona", "imagery", "fear", "burden", "protectiveJob", "originContext", "linkedPatternIds", "linkedBehaviorIds", "linkedValueIds"],
1825
+ highValueOptionalFields: [
1826
+ "persona",
1827
+ "imagery",
1828
+ "fear",
1829
+ "burden",
1830
+ "protectiveJob",
1831
+ "originContext",
1832
+ "linkedPatternIds",
1833
+ "linkedBehaviorIds",
1834
+ "linkedValueIds"
1835
+ ],
504
1836
  exampleQuestions: [
505
1837
  "What kind of part does this feel like: coping, child, critic-parent, healthy-adult, or happy-child?",
506
1838
  "If you gave this mode a name, what would it be?",
@@ -526,7 +1858,22 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
526
1858
  "Identify next moves and linked patterns, beliefs, modes, values, or tasks."
527
1859
  ],
528
1860
  requiredForCreate: ["title"],
529
- highValueOptionalFields: ["eventTypeId", "customEventType", "eventSituation", "occurredAt", "emotions", "thoughts", "behaviors", "consequences", "modeTimeline", "nextMoves", "linkedPatternIds", "linkedBeliefIds", "linkedModeIds", "linkedValueIds"],
1861
+ highValueOptionalFields: [
1862
+ "eventTypeId",
1863
+ "customEventType",
1864
+ "eventSituation",
1865
+ "occurredAt",
1866
+ "emotions",
1867
+ "thoughts",
1868
+ "behaviors",
1869
+ "consequences",
1870
+ "modeTimeline",
1871
+ "nextMoves",
1872
+ "linkedPatternIds",
1873
+ "linkedBeliefIds",
1874
+ "linkedModeIds",
1875
+ "linkedValueIds"
1876
+ ],
530
1877
  exampleQuestions: [
531
1878
  "What happened, as concretely as you can say it?",
532
1879
  "What emotions were there, and how intense were they?",
@@ -559,11 +1906,16 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
559
1906
  summary: "Create one or more entities in one ordered batch.",
560
1907
  whenToUse: "Use after explicit save intent and after duplicate checks when needed.",
561
1908
  inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, clientRef?: string, data: object }> }",
562
- requiredFields: ["operations", "operations[].entityType", "operations[].data"],
1909
+ requiredFields: [
1910
+ "operations",
1911
+ "operations[].entityType",
1912
+ "operations[].data"
1913
+ ],
563
1914
  notes: [
564
1915
  "entityType alone is never enough; full data is required.",
565
1916
  "Batch multiple related creates together when they come from one user ask.",
566
- "Goal, project, and task creates can include notes: [{ contentMarkdown, author?, links? }] and Forge will auto-link those notes to the newly created entity."
1917
+ "Goal, project, and task creates can include notes: [{ contentMarkdown, author?, tags?, destroyAt?, links? }] and Forge will auto-link those notes to the newly created entity.",
1918
+ "The same batch create route also handles calendar_event, work_block_template, and task_timebox. Calendar-event creates still trigger downstream projection sync when a writable provider calendar is selected."
567
1919
  ],
568
1920
  example: '{"operations":[{"entityType":"task","data":{"title":"Write the public release notes","projectId":"project_123","status":"focus","notes":[{"contentMarkdown":"Starting from the changelog draft and the last QA pass."}]},"clientRef":"task-1"}]}'
569
1921
  },
@@ -572,17 +1924,37 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
572
1924
  summary: "Patch one or more entities in one ordered batch.",
573
1925
  whenToUse: "Use when ids are known and the user explicitly wants a change persisted.",
574
1926
  inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, id: string, clientRef?: string, patch: object }> }",
575
- requiredFields: ["operations", "operations[].entityType", "operations[].id", "operations[].patch"],
576
- notes: ["patch is partial; only send the fields that should change."],
577
- example: '{"operations":[{"entityType":"task","id":"task_123","patch":{"status":"focus","priority":"high"},"clientRef":"task-patch-1"}]}'
1927
+ requiredFields: [
1928
+ "operations",
1929
+ "operations[].entityType",
1930
+ "operations[].id",
1931
+ "operations[].patch"
1932
+ ],
1933
+ notes: [
1934
+ "patch is partial; only send the fields that should change.",
1935
+ "Project lifecycle is status-driven: patch project.status to active, paused, or completed instead of looking for separate suspend, restart, or finish routes.",
1936
+ "Setting project.status to completed finishes the project and auto-completes linked unfinished tasks through the normal task completion path.",
1937
+ "Task and project scheduling rules stay on these same entity patches. Update task.schedulingRules, task.plannedDurationSeconds, or project.schedulingRules here.",
1938
+ "Use this same route to move or relink calendar_event records and to edit work_block_template or task_timebox records without switching to narrower calendar CRUD tools."
1939
+ ],
1940
+ example: '{"operations":[{"entityType":"project","id":"project_123","patch":{"status":"completed"},"clientRef":"project-finish-1"}]}'
578
1941
  },
579
1942
  {
580
1943
  toolName: "forge_delete_entities",
581
1944
  summary: "Delete one or more entities through the batch delete flow.",
582
1945
  whenToUse: "Use for explicit delete intent only.",
583
- inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, id: string, clientRef?: string, mode?: \"soft\"|\"hard\", reason?: string }> }",
584
- requiredFields: ["operations", "operations[].entityType", "operations[].id"],
585
- notes: ["Delete defaults to soft.", "Use mode=hard only for explicit permanent removal."],
1946
+ inputShape: '{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, id: string, clientRef?: string, mode?: "soft"|"hard", reason?: string }> }',
1947
+ requiredFields: [
1948
+ "operations",
1949
+ "operations[].entityType",
1950
+ "operations[].id"
1951
+ ],
1952
+ notes: [
1953
+ "Delete defaults to soft.",
1954
+ "Use mode=hard only for explicit permanent removal.",
1955
+ "Restoration is only possible after soft delete.",
1956
+ "calendar_event, work_block_template, and task_timebox are immediate calendar-domain deletions: calendar events delete remote projections too, and these records do not go through the settings bin."
1957
+ ],
586
1958
  example: '{"operations":[{"entityType":"task","id":"task_123","mode":"soft","reason":"Merged into another task"}]}'
587
1959
  },
588
1960
  {
@@ -590,36 +1962,152 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
590
1962
  summary: "Restore soft-deleted entities from the settings bin.",
591
1963
  whenToUse: "Use when the user wants an entity brought back after a soft delete.",
592
1964
  inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, id: string, clientRef?: string }> }",
593
- requiredFields: ["operations", "operations[].entityType", "operations[].id"],
1965
+ requiredFields: [
1966
+ "operations",
1967
+ "operations[].entityType",
1968
+ "operations[].id"
1969
+ ],
594
1970
  notes: ["Restore only works for soft-deleted entities."],
595
1971
  example: '{"operations":[{"entityType":"goal","id":"goal_123","clientRef":"goal-restore-1"}]}'
596
1972
  },
1973
+ {
1974
+ toolName: "forge_get_calendar_overview",
1975
+ summary: "Read connected calendars, Forge-native events, mirrored events, recurring work blocks, and task timeboxes together.",
1976
+ whenToUse: "Use before calendar-aware planning, slot selection, or scheduling diagnostics.",
1977
+ inputShape: "{ from?: string, to?: string }",
1978
+ requiredFields: [],
1979
+ notes: [
1980
+ "Use ISO datetimes.",
1981
+ "The response includes provider metadata, live connections, mirrored external events, derived work-block instances, and task timeboxes."
1982
+ ],
1983
+ example: '{"from":"2026-04-02T00:00:00.000Z","to":"2026-04-09T00:00:00.000Z"}'
1984
+ },
1985
+ {
1986
+ toolName: "forge_connect_calendar_provider",
1987
+ summary: "Create a Forge calendar connection for Google, Apple, Exchange Online, or custom CalDAV.",
1988
+ whenToUse: "Use only when the operator explicitly wants Forge connected to an external calendar provider.",
1989
+ inputShape: '{ provider: "google"|"apple"|"caldav"|"microsoft", label: string, username?: string, clientId?: string, clientSecret?: string, refreshToken?: string, password?: string, serverUrl?: string, authSessionId?: string, selectedCalendarUrls: string[], forgeCalendarUrl?: string, createForgeCalendar?: boolean }',
1990
+ requiredFields: ["provider", "label", "provider-specific credentials"],
1991
+ notes: [
1992
+ "Google uses OAuth client credentials plus a refresh token.",
1993
+ "Apple starts from https://caldav.icloud.com and autodiscovers the principal plus calendars after authentication.",
1994
+ "Exchange Online uses Microsoft Graph. In the current Forge implementation it is read-only: Forge mirrors the selected calendars but does not publish work blocks or timeboxes back to Microsoft.",
1995
+ "In the current self-hosted local runtime, Exchange Online now uses an interactive Microsoft public-client sign-in flow with PKCE after the operator has saved the Microsoft client ID, tenant, and redirect URI in Settings -> Calendar. Non-interactive callers should treat Microsoft connection setup as a Settings-owned operator action unless a completed authSessionId already exists.",
1996
+ "Custom CalDAV uses an account-level server URL, not a single calendar collection URL.",
1997
+ "Writable providers publish Forge work blocks and timeboxes to the dedicated Forge calendar for that connection."
1998
+ ],
1999
+ example: '{"provider":"apple","label":"Primary Apple","username":"operator@example.com","password":"app-password","selectedCalendarUrls":["https://caldav.icloud.com/.../Family/"],"forgeCalendarUrl":"https://caldav.icloud.com/.../Forge/","createForgeCalendar":false}'
2000
+ },
2001
+ {
2002
+ toolName: "forge_create_work_block_template",
2003
+ summary: "Create a recurring half-day, holiday, or custom work-block template.",
2004
+ whenToUse: "Use when the operator wants recurring time windows such as Main Activity, Secondary Activity, Third Activity, Rest, Holiday, or a custom block.",
2005
+ inputShape: '{ title: string, kind: "main_activity"|"secondary_activity"|"third_activity"|"rest"|"holiday"|"custom", color: string, timezone: string, weekDays: integer[], startMinute: integer, endMinute: integer, startsOn?: "YYYY-MM-DD"|null, endsOn?: "YYYY-MM-DD"|null, blockingState: "allowed"|"blocked" }',
2006
+ requiredFields: [
2007
+ "title",
2008
+ "kind",
2009
+ "timezone",
2010
+ "weekDays",
2011
+ "startMinute",
2012
+ "endMinute",
2013
+ "blockingState"
2014
+ ],
2015
+ notes: [
2016
+ "Minutes are measured from midnight in the selected timezone.",
2017
+ "startsOn and endsOn are optional date bounds. Leaving endsOn null makes the block repeat indefinitely.",
2018
+ "Use kind=holiday with weekDays [0,1,2,3,4,5,6] and minutes 0-1440 for vacations or other full-day blocked ranges.",
2019
+ "Derived instances appear in calendar overview responses immediately after creation.",
2020
+ "This is a convenience helper; agents can also create work_block_template through forge_create_entities."
2021
+ ],
2022
+ example: '{"title":"Summer holiday","kind":"holiday","color":"#14b8a6","timezone":"Europe/Zurich","weekDays":[0,1,2,3,4,5,6],"startMinute":0,"endMinute":1440,"startsOn":"2026-08-01","endsOn":"2026-08-16","blockingState":"blocked"}'
2023
+ },
2024
+ {
2025
+ toolName: "forge_recommend_task_timeboxes",
2026
+ summary: "Suggest future task slots that fit the current calendar rules and schedule.",
2027
+ whenToUse: "Use when preparing focused work in advance.",
2028
+ inputShape: "{ taskId: string, from?: string, to?: string, limit?: integer }",
2029
+ requiredFields: ["taskId"],
2030
+ notes: [
2031
+ "Recommendations consider mirrored calendar events, recurring work blocks, task or project scheduling rules, and the task's planned duration when available.",
2032
+ "Confirm a suggested slot by creating a task timebox."
2033
+ ],
2034
+ example: '{"taskId":"task_123","from":"2026-04-02T00:00:00.000Z","to":"2026-04-09T00:00:00.000Z","limit":6}'
2035
+ },
2036
+ {
2037
+ toolName: "forge_create_task_timebox",
2038
+ summary: "Create a planned task timebox in the Forge calendar domain.",
2039
+ whenToUse: "Use after choosing a valid future slot or when creating a manual timebox directly.",
2040
+ inputShape: '{ taskId: string, projectId?: string|null, title: string, startsAt: string, endsAt: string, source?: "manual"|"suggested"|"live_run" }',
2041
+ requiredFields: ["taskId", "title", "startsAt", "endsAt"],
2042
+ notes: [
2043
+ "Forge publishes these into the dedicated Forge calendar during provider sync.",
2044
+ "Live task runs can later attach to matching timeboxes.",
2045
+ "This is a convenience helper; agents can also create task_timebox through forge_create_entities."
2046
+ ],
2047
+ example: '{"taskId":"task_123","projectId":"project_456","title":"Draft the methods section","startsAt":"2026-04-03T08:00:00.000Z","endsAt":"2026-04-03T09:30:00.000Z","source":"suggested"}'
2048
+ },
2049
+ {
2050
+ toolName: "forge_grant_reward_bonus",
2051
+ summary: "Grant an explicit manual XP bonus or penalty with clear provenance.",
2052
+ whenToUse: "Use when the user or operator explicitly wants an auditable reward adjustment beyond the automatic task and habit reward paths.",
2053
+ inputShape: "{ entityType: RewardableEntityType, entityId: string, deltaXp: integer, reasonTitle: string, reasonSummary?: string, metadata?: object }",
2054
+ requiredFields: ["entityType", "entityId", "deltaXp", "reasonTitle"],
2055
+ notes: [
2056
+ "Requires rewards.manage and write scopes.",
2057
+ "Use this for explicit operator judgement, not as a substitute for normal task_run or habit check-in rewards."
2058
+ ],
2059
+ 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"}}'
2060
+ },
597
2061
  {
598
2062
  toolName: "forge_post_insight",
599
2063
  summary: "Store an agent-authored insight.",
600
2064
  whenToUse: "Use when you have a data-grounded observation or recommendation worth keeping visible in Forge.",
601
2065
  inputShape: "{ entityType?: string|null, entityId?: string|null, timeframeLabel?: string|null, title: string, summary: string, recommendation: string, rationale?: string, confidence?: number, visibility?: string, ctaLabel?: string }",
602
2066
  requiredFields: ["title", "summary", "recommendation"],
603
- notes: ["Insights are for interpretation and advice, not for replacing user-owned goals or tasks."],
2067
+ notes: [
2068
+ "Insights are for interpretation and advice, not for replacing user-owned goals or tasks."
2069
+ ],
604
2070
  example: '{"entityType":"goal","entityId":"goal_123","title":"Admin drag is masking momentum","summary":"Creative progress is happening, but admin cleanup keeps interrupting it.","recommendation":"Protect one clean creative block and isolate admin into a separate recurring task.","confidence":0.82}'
605
2071
  },
2072
+ {
2073
+ toolName: "forge_adjust_work_minutes",
2074
+ summary: "Add or remove tracked work minutes on a task or project without creating a live task run.",
2075
+ whenToUse: "Use for truthful retrospective minute corrections. Use this instead of forge_log_work when the task or project already exists and only tracked minutes need adjusting.",
2076
+ inputShape: '{ entityType: "task"|"project", entityId: string, deltaMinutes: integer, note?: string }',
2077
+ requiredFields: ["entityType", "entityId", "deltaMinutes"],
2078
+ notes: [
2079
+ "Positive deltaMinutes add tracked minutes and may award XP when a progress bucket is crossed.",
2080
+ "Negative deltaMinutes remove tracked minutes and may reverse XP symmetrically when a progress bucket is crossed downward.",
2081
+ "Requires rewards.manage and write scopes."
2082
+ ],
2083
+ example: '{"entityType":"task","entityId":"task_123","deltaMinutes":25,"note":"Captured the off-timer review pass from this morning."}'
2084
+ },
606
2085
  {
607
2086
  toolName: "forge_log_work",
608
2087
  summary: "Log work that already happened.",
609
- whenToUse: "Use for retroactive work, not for starting a live session.",
610
- inputShape: "{ taskId?: string, title?: string, description?: string, summary?: string, goalId?: string|null, projectId?: string|null, owner?: string, status?: TaskStatus, priority?: TaskPriority, dueDate?: string|null, effort?: TaskEffort, energy?: TaskEnergy, points?: number, tagIds?: string[], closeoutNote?: { contentMarkdown: string, author?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
2088
+ whenToUse: "Use for completion-style retroactive work, not for starting a live session or adjusting minutes on an existing record.",
2089
+ inputShape: "{ taskId?: string, title?: string, description?: string, summary?: string, goalId?: string|null, projectId?: string|null, owner?: string, status?: TaskStatus, priority?: TaskPriority, dueDate?: string|null, effort?: TaskEffort, energy?: TaskEnergy, points?: number, tagIds?: string[], closeoutNote?: { contentMarkdown: string, author?: string|null, tags?: string[], destroyAt?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
611
2090
  requiredFields: ["taskId or title"],
612
- notes: ["Use taskId when logging work against an existing task.", "Use title when a new completed work item should be created and logged.", "closeoutNote persists the work summary as a real linked note."],
2091
+ notes: [
2092
+ "Use taskId when logging work against an existing task.",
2093
+ "Use title when a new completed work item should be created and logged.",
2094
+ "Use forge_adjust_work_minutes for signed minute corrections on existing tasks or projects.",
2095
+ "closeoutNote persists the work summary as a real linked note."
2096
+ ],
613
2097
  example: '{"taskId":"task_123","summary":"Finished the review draft and cleaned the notes.","points":40,"closeoutNote":{"contentMarkdown":"Finished the review draft, cleaned the note structure, and left one follow-up for QA."}}'
614
2098
  },
615
2099
  {
616
2100
  toolName: "forge_start_task_run",
617
2101
  summary: "Start truthful live work on a task.",
618
2102
  whenToUse: "Use when the user wants to begin working now.",
619
- inputShape: "{ taskId: string, actor: string, timerMode?: \"planned\"|\"unlimited\", plannedDurationSeconds?: number|null, isCurrent?: boolean, leaseTtlSeconds?: number, note?: string }",
2103
+ inputShape: '{ taskId: string, actor: string, timerMode?: "planned"|"unlimited", plannedDurationSeconds?: number|null, overrideReason?: string|null, isCurrent?: boolean, leaseTtlSeconds?: number, note?: string }',
620
2104
  requiredFields: ["taskId", "actor"],
621
- notes: ["If timerMode is planned, plannedDurationSeconds is required.", "If timerMode is unlimited, plannedDurationSeconds must be null or omitted."],
622
- example: '{"taskId":"task_123","actor":"aurel","timerMode":"planned","plannedDurationSeconds":1500,"isCurrent":true,"leaseTtlSeconds":900,"note":"Starting focused writing block"}'
2105
+ notes: [
2106
+ "If timerMode is planned, plannedDurationSeconds is required.",
2107
+ "If timerMode is unlimited, plannedDurationSeconds must be null or omitted.",
2108
+ "If calendar rules currently block the task, pass an explicit overrideReason to proceed and keep the exception auditable."
2109
+ ],
2110
+ example: '{"taskId":"task_123","actor":"aurel","timerMode":"planned","plannedDurationSeconds":1500,"overrideReason":"Protected creative block after clinic hours.","isCurrent":true,"leaseTtlSeconds":900,"note":"Starting focused writing block"}'
623
2111
  },
624
2112
  {
625
2113
  toolName: "forge_heartbeat_task_run",
@@ -636,25 +2124,33 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
636
2124
  whenToUse: "Use when several runs exist and one should be the visible current run.",
637
2125
  inputShape: "{ taskRunId: string, actor?: string }",
638
2126
  requiredFields: ["taskRunId"],
639
- notes: ["This does not complete or release a run; it just changes current focus."],
2127
+ notes: [
2128
+ "This does not complete or release a run; it just changes current focus."
2129
+ ],
640
2130
  example: '{"taskRunId":"run_123","actor":"aurel"}'
641
2131
  },
642
2132
  {
643
2133
  toolName: "forge_complete_task_run",
644
2134
  summary: "Finish an active run as completed work.",
645
2135
  whenToUse: "Use when the user has finished the live work block.",
646
- inputShape: "{ taskRunId: string, actor?: string, note?: string, closeoutNote?: { contentMarkdown: string, author?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
2136
+ inputShape: "{ taskRunId: string, actor?: string, note?: string, closeoutNote?: { contentMarkdown: string, author?: string|null, tags?: string[], destroyAt?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
647
2137
  requiredFields: ["taskRunId"],
648
- notes: ["This is the truthful way to finish live work and award completion effects.", "closeoutNote persists a real linked note instead of only updating the transient run note."],
2138
+ notes: [
2139
+ "This is the truthful way to finish live work and award completion effects.",
2140
+ "closeoutNote persists a real linked note instead of only updating the transient run note."
2141
+ ],
649
2142
  example: '{"taskRunId":"run_123","actor":"aurel","note":"Finished the review draft","closeoutNote":{"contentMarkdown":"Completed the draft review and listed the follow-up fixes."}}'
650
2143
  },
651
2144
  {
652
2145
  toolName: "forge_release_task_run",
653
2146
  summary: "Stop an active run without marking the task complete.",
654
2147
  whenToUse: "Use when the user is stopping or pausing work without completion.",
655
- inputShape: "{ taskRunId: string, actor?: string, note?: string, closeoutNote?: { contentMarkdown: string, author?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
2148
+ inputShape: "{ taskRunId: string, actor?: string, note?: string, closeoutNote?: { contentMarkdown: string, author?: string|null, tags?: string[], destroyAt?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
656
2149
  requiredFields: ["taskRunId"],
657
- notes: ["Use this instead of faking a stop by only changing task status.", "closeoutNote is useful for documenting blockers or handoff context."],
2150
+ notes: [
2151
+ "Use this instead of faking a stop by only changing task status.",
2152
+ "closeoutNote is useful for documenting blockers or handoff context."
2153
+ ],
658
2154
  example: '{"taskRunId":"run_123","actor":"aurel","note":"Stopping for now; blocked on feedback","closeoutNote":{"contentMarkdown":"Blocked on feedback from design before I can continue."}}'
659
2155
  }
660
2156
  ];
@@ -712,11 +2208,14 @@ function buildAgentOnboardingPayload(request) {
712
2208
  },
713
2209
  conceptModel: {
714
2210
  goal: "Long-horizon direction or outcome. Goals anchor projects and sometimes tasks directly.",
715
- project: "A multi-step workstream under one goal. Projects organize related tasks.",
2211
+ project: "A multi-step workstream under one goal. Projects organize related tasks. Project lifecycle is driven by status: active means in play, paused means suspended, and completed means finished. Setting a project to completed auto-completes linked unfinished tasks.",
716
2212
  task: "A concrete actionable work item. Task status is board state, not proof of live work.",
717
2213
  taskRun: "A live work session attached to a task. Start, heartbeat, focus, complete, and release runs instead of faking work with status alone.",
718
2214
  note: "A Markdown work note that can link to one or many entities. Use notes for progress evidence, context, and close-out summaries.",
719
2215
  insight: "An agent-authored observation or recommendation grounded in Forge data.",
2216
+ calendar: "A connected calendar source mirrored into Forge. Calendar state combines provider events, recurring work blocks, and task timeboxes.",
2217
+ workBlock: "A recurring half-day or custom time window such as Main Activity, Secondary Activity, Third Activity, Rest, Holiday, or Custom. Work blocks can allow or block work by default, can define active date bounds, and remain editable through the calendar surface.",
2218
+ taskTimebox: "A planned or live calendar slot tied to a task. Timeboxes can be suggested in advance or created automatically from active task runs.",
720
2219
  psyche: "Forge Psyche is the reflective domain for values, patterns, behaviors, beliefs, modes, and trigger reports. It is sensitive and should be handled deliberately."
721
2220
  },
722
2221
  psycheSubmoduleModel: {
@@ -736,6 +2235,7 @@ function buildAgentOnboardingPayload(request) {
736
2235
  "Goals are the top-level strategic layer.",
737
2236
  "Projects belong to one goal through goalId.",
738
2237
  "Tasks can belong to a goal, a project, both, or neither.",
2238
+ "Habits are recurring records that can connect directly to goals, projects, tasks, and durable Psyche entities.",
739
2239
  "Task runs represent live work sessions on tasks and are separate from task status.",
740
2240
  "Notes can link to one or many entities and are the canonical place for Markdown progress context or close-out evidence.",
741
2241
  "Psyche values can link to goals, projects, and tasks.",
@@ -748,6 +2248,7 @@ function buildAgentOnboardingPayload(request) {
748
2248
  context: "/api/v1/context",
749
2249
  xpMetrics: "/api/v1/metrics/xp",
750
2250
  weeklyReview: "/api/v1/reviews/weekly",
2251
+ calendarOverview: "/api/v1/calendar/overview",
751
2252
  settingsBin: "/api/v1/settings/bin",
752
2253
  batchSearch: "/api/v1/entities/search",
753
2254
  psycheSchemaCatalog: "/api/v1/psyche/schema-catalog",
@@ -771,7 +2272,9 @@ function buildAgentOnboardingPayload(request) {
771
2272
  "forge_delete_entities",
772
2273
  "forge_restore_entities"
773
2274
  ],
2275
+ rewardWorkflow: ["forge_grant_reward_bonus"],
774
2276
  workWorkflow: [
2277
+ "forge_adjust_work_minutes",
775
2278
  "forge_log_work",
776
2279
  "forge_start_task_run",
777
2280
  "forge_heartbeat_task_run",
@@ -779,6 +2282,14 @@ function buildAgentOnboardingPayload(request) {
779
2282
  "forge_complete_task_run",
780
2283
  "forge_release_task_run"
781
2284
  ],
2285
+ calendarWorkflow: [
2286
+ "forge_get_calendar_overview",
2287
+ "forge_connect_calendar_provider",
2288
+ "forge_sync_calendar_connection",
2289
+ "forge_create_work_block_template",
2290
+ "forge_recommend_task_timeboxes",
2291
+ "forge_create_task_timebox"
2292
+ ],
782
2293
  insightWorkflow: ["forge_post_insight"]
783
2294
  },
784
2295
  interactionGuidance: {
@@ -801,14 +2312,14 @@ function buildAgentOnboardingPayload(request) {
801
2312
  },
802
2313
  deleteDefault: "soft",
803
2314
  hardDeleteRequiresExplicitMode: true,
804
- restoreSummary: "Restore soft-deleted entities through the restore route or the settings bin.",
805
- entityDeleteSummary: "Entity DELETE routes default to soft delete. Pass mode=hard only when permanent removal is intended.",
2315
+ restoreSummary: "Restore soft-deleted entities through the restore route or the settings bin. Calendar-domain deletes for calendar_event, work_block_template, and task_timebox are immediate and do not enter the bin.",
2316
+ entityDeleteSummary: "Entity DELETE routes default to soft delete. Pass mode=hard only when permanent removal is intended. Calendar-event deletes still remove remote projections downstream.",
806
2317
  batchingRule: "forge_create_entities, forge_update_entities, forge_delete_entities, and forge_restore_entities all accept operations as arrays. Batch multiple related mutations together in one request when possible.",
807
2318
  searchRule: "forge_search_entities accepts searches as an array. Search before create or update when duplicate risk exists.",
808
- createRule: "Each create operation must include entityType and full data. entityType alone is not enough.",
809
- updateRule: "Each update operation must include entityType, id, and patch.",
2319
+ createRule: "Each create operation must include entityType and full data. entityType alone is not enough. This includes calendar_event, work_block_template, and task_timebox alongside the usual planning and Psyche entities.",
2320
+ updateRule: "Each update operation must include entityType, id, and patch. For projects, lifecycle changes are status patches: active to restart, paused to suspend, completed to finish. Keep task and project scheduling rules on those same patch payloads. Calendar-event updates still run downstream provider projection sync.",
810
2321
  createExample: '{"operations":[{"entityType":"goal","data":{"title":"Create meaningfully"},"clientRef":"goal-create-1"},{"entityType":"goal","data":{"title":"Build a beautiful family"},"clientRef":"goal-create-2"}]}',
811
- updateExample: '{"operations":[{"entityType":"task","id":"task_123","patch":{"status":"focus","priority":"high"},"clientRef":"task-update-1"}]}'
2322
+ updateExample: '{"operations":[{"entityType":"project","id":"project_123","patch":{"status":"paused","schedulingRules":{"blockWorkBlockKinds":["main_activity"],"allowWorkBlockKinds":["secondary_activity"]}},"clientRef":"project-suspend-1"},{"entityType":"task","id":"task_456","patch":{"plannedDurationSeconds":5400,"schedulingRules":{"allowEventKeywords":["creative"],"blockEventKeywords":["clinic"]}},"clientRef":"task-scheduling-1"}]}'
812
2323
  }
813
2324
  };
814
2325
  }
@@ -872,7 +2383,9 @@ function parseActivityContext(headers) {
872
2383
  if (Array.isArray(rawSource)) {
873
2384
  throw new Error("X-Forge-Source must be a single header value");
874
2385
  }
875
- const source = rawSource === undefined ? "ui" : activitySourceSchema.parse(typeof rawSource === "string" ? rawSource.trim() : rawSource);
2386
+ const source = rawSource === undefined
2387
+ ? "ui"
2388
+ : activitySourceSchema.parse(typeof rawSource === "string" ? rawSource.trim() : rawSource);
876
2389
  return {
877
2390
  source,
878
2391
  actor: parseOptionalActorHeader(headers)
@@ -915,7 +2428,8 @@ function hasTokenScope(token, scope) {
915
2428
  return Boolean(token?.scopes.includes(scope));
916
2429
  }
917
2430
  function isPsycheEntityType(entityType) {
918
- return Boolean(entityType && PSYCHE_ENTITY_TYPES.includes(entityType));
2431
+ return Boolean(entityType &&
2432
+ PSYCHE_ENTITY_TYPES.includes(entityType));
919
2433
  }
920
2434
  function getWatchdogHealth(taskRunWatchdog) {
921
2435
  if (!taskRunWatchdog) {
@@ -955,7 +2469,17 @@ function buildHealthPayload(taskRunWatchdog, extras = {}) {
955
2469
  ...extras
956
2470
  };
957
2471
  }
2472
+ function shouldIncludeRuntimeProbe(headers) {
2473
+ const probeHeader = headers["x-forge-runtime-probe"];
2474
+ if (Array.isArray(probeHeader)) {
2475
+ return probeHeader.some((value) => typeof value === "string" && value.trim() === "1");
2476
+ }
2477
+ return typeof probeHeader === "string" && probeHeader.trim() === "1";
2478
+ }
958
2479
  function buildV1Context() {
2480
+ const goals = listGoals();
2481
+ const tasks = listTasks();
2482
+ const habits = listHabits();
959
2483
  return {
960
2484
  meta: {
961
2485
  apiVersion: "v1",
@@ -964,15 +2488,16 @@ function buildV1Context() {
964
2488
  backend: "forge-node-runtime",
965
2489
  mode: "transitional-node"
966
2490
  },
967
- metrics: buildGamificationProfile(listGoals(), listTasks()),
2491
+ metrics: buildGamificationProfile(goals, tasks, habits),
968
2492
  dashboard: getDashboard(),
969
2493
  overview: getOverviewContext(),
970
2494
  today: getTodayContext(),
971
2495
  risk: getRiskContext(),
972
- goals: listGoals(),
2496
+ goals,
973
2497
  projects: listProjectSummaries(),
974
2498
  tags: listTags(),
975
- tasks: listTasks(),
2499
+ tasks,
2500
+ habits,
976
2501
  activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
977
2502
  activity: listActivityEvents({ limit: 25 })
978
2503
  };
@@ -980,8 +2505,9 @@ function buildV1Context() {
980
2505
  function buildXpMetricsPayload() {
981
2506
  const goals = listGoals();
982
2507
  const tasks = listTasks();
2508
+ const habits = listHabits();
983
2509
  const rules = listRewardRules();
984
- const gamificationOverview = buildGamificationOverview(goals, tasks);
2510
+ const gamificationOverview = buildGamificationOverview(goals, tasks, habits);
985
2511
  const dailyAmbientCap = rules
986
2512
  .filter((rule) => rule.family === "ambient")
987
2513
  .reduce((max, rule) => Math.max(max, Number(rule.config.dailyCap ?? 0)), 0) || 12;
@@ -989,15 +2515,58 @@ function buildXpMetricsPayload() {
989
2515
  profile: gamificationOverview.profile,
990
2516
  achievements: gamificationOverview.achievements,
991
2517
  milestoneRewards: gamificationOverview.milestoneRewards,
992
- momentumPulse: buildXpMomentumPulse(goals, tasks),
2518
+ momentumPulse: buildXpMomentumPulse(goals, tasks, habits),
993
2519
  recentLedger: listRewardLedger({ limit: 25 }),
994
2520
  rules,
995
2521
  dailyAmbientXp: getDailyAmbientXp(new Date().toISOString().slice(0, 10)),
996
2522
  dailyAmbientCap
997
2523
  };
998
2524
  }
2525
+ function resolveWorkAdjustmentTarget(entityType, entityId) {
2526
+ if (entityType === "task") {
2527
+ const task = getTaskById(entityId);
2528
+ return task
2529
+ ? {
2530
+ entityType,
2531
+ entityId: task.id,
2532
+ title: task.title,
2533
+ time: task.time
2534
+ }
2535
+ : null;
2536
+ }
2537
+ const project = getProjectSummary(entityId);
2538
+ return project
2539
+ ? {
2540
+ entityType,
2541
+ entityId: project.id,
2542
+ title: project.title,
2543
+ time: project.time
2544
+ }
2545
+ : null;
2546
+ }
2547
+ function clampWorkAdjustmentMinutes(deltaMinutes, currentCreditedSeconds) {
2548
+ if (deltaMinutes >= 0) {
2549
+ return deltaMinutes;
2550
+ }
2551
+ const maxRemovableMinutes = Math.max(0, Math.floor(currentCreditedSeconds / 60));
2552
+ return -Math.min(Math.abs(deltaMinutes), maxRemovableMinutes);
2553
+ }
2554
+ function describeWorkAdjustment(input) {
2555
+ const entityLabel = input.entityType === "task" ? "Task" : "Project";
2556
+ const requestedLabel = `${Math.abs(input.requestedDeltaMinutes)} minute${Math.abs(input.requestedDeltaMinutes) === 1 ? "" : "s"}`;
2557
+ const appliedLabel = `${Math.abs(input.appliedDeltaMinutes)} minute${Math.abs(input.appliedDeltaMinutes) === 1 ? "" : "s"}`;
2558
+ const direction = input.appliedDeltaMinutes >= 0 ? "added" : "removed";
2559
+ const clamped = input.requestedDeltaMinutes !== input.appliedDeltaMinutes;
2560
+ return {
2561
+ title: `${entityLabel} work adjusted: ${input.targetTitle}`,
2562
+ description: clamped
2563
+ ? `${requestedLabel} requested, ${appliedLabel} ${direction} after clamping to the currently tracked time.`
2564
+ : `${appliedLabel} ${direction} from the tracked work total.`
2565
+ };
2566
+ }
999
2567
  function buildOperatorContext() {
1000
2568
  const tasks = listTasks();
2569
+ const dueHabits = listHabits({ dueToday: true }).slice(0, 12);
1001
2570
  const activeProjects = listProjectSummaries({ status: "active" }).filter((project) => project.activeTaskCount > 0 || project.completedTaskCount > 0);
1002
2571
  const focusTasks = tasks.filter((task) => task.status === "focus" || task.status === "in_progress");
1003
2572
  const recommendedNextTask = focusTasks[0] ??
@@ -1008,10 +2577,13 @@ function buildOperatorContext() {
1008
2577
  generatedAt: new Date().toISOString(),
1009
2578
  activeProjects: activeProjects.slice(0, 8),
1010
2579
  focusTasks: focusTasks.slice(0, 12),
2580
+ dueHabits,
1011
2581
  currentBoard: {
1012
2582
  backlog: tasks.filter((task) => task.status === "backlog").slice(0, 20),
1013
2583
  focus: tasks.filter((task) => task.status === "focus").slice(0, 20),
1014
- inProgress: tasks.filter((task) => task.status === "in_progress").slice(0, 20),
2584
+ inProgress: tasks
2585
+ .filter((task) => task.status === "in_progress")
2586
+ .slice(0, 20),
1015
2587
  blocked: tasks.filter((task) => task.status === "blocked").slice(0, 20),
1016
2588
  done: tasks.filter((task) => task.status === "done").slice(0, 20)
1017
2589
  },
@@ -1085,6 +2657,12 @@ function buildOperatorOverviewRouteGuide() {
1085
2657
  summary: "Preferred multi-entity mutation surface for agents. Delete defaults to soft delete and restore reverses soft deletion.",
1086
2658
  requiredScope: "write"
1087
2659
  },
2660
+ {
2661
+ id: "work_adjustments",
2662
+ path: "/api/v1/work-adjustments",
2663
+ summary: "Signed retrospective minute adjustments for existing tasks or projects, with symmetric progress-XP updates and clamp protection.",
2664
+ requiredScope: "write"
2665
+ },
1088
2666
  {
1089
2667
  id: "operator_log_work",
1090
2668
  path: "/api/v1/operator/log-work",
@@ -1102,8 +2680,14 @@ function buildOperatorOverviewRouteGuide() {
1102
2680
  }
1103
2681
  function buildOperatorOverview(request) {
1104
2682
  const auth = parseRequestAuth(request.headers);
1105
- const canReadPsyche = auth.token ? hasTokenScope(auth.token, "psyche.read") : true;
1106
- const warnings = canReadPsyche ? [] : ["Psyche summary omitted because the active token does not include psyche.read."];
2683
+ const canReadPsyche = auth.token
2684
+ ? hasTokenScope(auth.token, "psyche.read")
2685
+ : true;
2686
+ const warnings = canReadPsyche
2687
+ ? []
2688
+ : [
2689
+ "Psyche summary omitted because the active token does not include psyche.read."
2690
+ ];
1107
2691
  return {
1108
2692
  generatedAt: new Date().toISOString(),
1109
2693
  snapshot: buildV1Context(),
@@ -1115,9 +2699,15 @@ function buildOperatorOverview(request) {
1115
2699
  tokenPresent: Boolean(auth.token),
1116
2700
  scopes: auth.token?.scopes ?? [],
1117
2701
  canReadPsyche,
1118
- canWritePsyche: auth.token ? hasTokenScope(auth.token, "psyche.write") : true,
1119
- canManageModes: auth.token ? hasTokenScope(auth.token, "psyche.mode") : true,
1120
- canManageRewards: auth.token ? hasTokenScope(auth.token, "rewards.manage") : true
2702
+ canWritePsyche: auth.token
2703
+ ? hasTokenScope(auth.token, "psyche.write")
2704
+ : true,
2705
+ canManageModes: auth.token
2706
+ ? hasTokenScope(auth.token, "psyche.mode")
2707
+ : true,
2708
+ canManageRewards: auth.token
2709
+ ? hasTokenScope(auth.token, "rewards.manage")
2710
+ : true
1121
2711
  },
1122
2712
  warnings,
1123
2713
  routeGuide: buildOperatorOverviewRouteGuide()
@@ -1125,7 +2715,21 @@ function buildOperatorOverview(request) {
1125
2715
  }
1126
2716
  export async function buildServer(options = {}) {
1127
2717
  const managers = createManagerRuntime({ dataRoot: options.dataRoot });
1128
- const runtimeConfig = managers.configuration.readRuntimeConfig({ dataRoot: options.dataRoot });
2718
+ managers.externalServices.register("google_calendar", {
2719
+ provider: "google",
2720
+ label: "Google Calendar"
2721
+ });
2722
+ managers.externalServices.register("microsoft_graph_calendar", {
2723
+ provider: "microsoft",
2724
+ label: "Exchange Online"
2725
+ });
2726
+ managers.externalServices.register("caldav", {
2727
+ provider: "caldav",
2728
+ label: "CalDAV"
2729
+ });
2730
+ const runtimeConfig = managers.configuration.readRuntimeConfig({
2731
+ dataRoot: options.dataRoot
2732
+ });
1129
2733
  configureDatabase({ dataRoot: runtimeConfig.dataRoot ?? undefined });
1130
2734
  configureDatabaseSeeding(options.seedDemoData ?? false);
1131
2735
  await managers.migration.initialize();
@@ -1133,7 +2737,9 @@ export async function buildServer(options = {}) {
1133
2737
  logger: false,
1134
2738
  rewriteUrl: (request) => rewriteMountPath(request.url ?? "/")
1135
2739
  });
1136
- const taskRunWatchdog = options.taskRunWatchdog === false ? null : createTaskRunWatchdog(options.taskRunWatchdog);
2740
+ const taskRunWatchdog = options.taskRunWatchdog === false
2741
+ ? null
2742
+ : createTaskRunWatchdog(options.taskRunWatchdog);
1137
2743
  await app.register(cors, {
1138
2744
  origin: (origin, callback) => {
1139
2745
  if (!origin) {
@@ -1164,7 +2770,9 @@ export async function buildServer(options = {}) {
1164
2770
  : statusCode === 400
1165
2771
  ? "invalid_request"
1166
2772
  : "internal_error",
1167
- error: validationIssues ? "Request validation failed" : getErrorMessage(error),
2773
+ error: validationIssues
2774
+ ? "Request validation failed"
2775
+ : getErrorMessage(error),
1168
2776
  statusCode,
1169
2777
  ...(validationIssues ? { details: validationIssues } : {}),
1170
2778
  ...(isHttpError(error) && error.details ? error.details : {}),
@@ -1176,6 +2784,112 @@ export async function buildServer(options = {}) {
1176
2784
  actor: context.actor,
1177
2785
  source: context.source
1178
2786
  });
2787
+ const applyBatchCalendarEntityEffects = async (results, auth, action) => {
2788
+ for (const result of results) {
2789
+ if (!result.ok ||
2790
+ typeof result.entityType !== "string" ||
2791
+ typeof result.id !== "string") {
2792
+ continue;
2793
+ }
2794
+ if (result.entityType === "calendar_event") {
2795
+ if (action === "delete") {
2796
+ await deleteCalendarEventProjection(result.id, managers.secrets);
2797
+ const event = (result.entity ?? {});
2798
+ recordActivityEvent({
2799
+ entityType: "calendar_event",
2800
+ entityId: result.id,
2801
+ eventType: "calendar_event_deleted",
2802
+ title: `Calendar event deleted: ${typeof event.title === "string" ? event.title : result.id}`,
2803
+ description: "The Forge calendar event was removed and any projected remote copies were deleted.",
2804
+ actor: auth.actor ?? null,
2805
+ source: auth.source,
2806
+ metadata: {
2807
+ calendarId: typeof event.calendarId === "string" ? event.calendarId : null,
2808
+ originType: typeof event.originType === "string" ? event.originType : null
2809
+ }
2810
+ });
2811
+ continue;
2812
+ }
2813
+ await pushCalendarEventUpdate(result.id, managers.secrets);
2814
+ const refreshed = getCalendarEventById(result.id);
2815
+ if (!refreshed) {
2816
+ continue;
2817
+ }
2818
+ result.entity = refreshed;
2819
+ recordActivityEvent({
2820
+ entityType: "calendar_event",
2821
+ entityId: refreshed.id,
2822
+ eventType: action === "create"
2823
+ ? "calendar_event_created"
2824
+ : "calendar_event_updated",
2825
+ title: `Calendar event ${action === "create" ? "created" : "updated"}: ${refreshed.title}`,
2826
+ description: action === "create"
2827
+ ? "A native Forge calendar event was created."
2828
+ : "The Forge calendar event was updated and projected to remote calendars when configured.",
2829
+ actor: auth.actor ?? null,
2830
+ source: auth.source,
2831
+ metadata: {
2832
+ calendarId: refreshed.calendarId,
2833
+ originType: refreshed.originType
2834
+ }
2835
+ });
2836
+ continue;
2837
+ }
2838
+ if (result.entityType === "work_block_template" &&
2839
+ result.entity &&
2840
+ typeof result.entity === "object") {
2841
+ const template = result.entity;
2842
+ recordActivityEvent({
2843
+ entityType: "work_block",
2844
+ entityId: template.id,
2845
+ eventType: action === "create"
2846
+ ? "work_block_created"
2847
+ : action === "update"
2848
+ ? "work_block_updated"
2849
+ : "work_block_deleted",
2850
+ title: `Work block ${action}: ${template.title}`,
2851
+ description: action === "create"
2852
+ ? "A recurring work block was added to Forge."
2853
+ : action === "update"
2854
+ ? "The recurring work block was updated."
2855
+ : "The recurring work block was removed.",
2856
+ actor: auth.actor ?? null,
2857
+ source: auth.source,
2858
+ metadata: {
2859
+ kind: template.kind ?? null,
2860
+ blockingState: action === "delete" ? null : (template.blockingState ?? null)
2861
+ }
2862
+ });
2863
+ continue;
2864
+ }
2865
+ if (result.entityType === "task_timebox" &&
2866
+ result.entity &&
2867
+ typeof result.entity === "object") {
2868
+ const timebox = result.entity;
2869
+ recordActivityEvent({
2870
+ entityType: "task_timebox",
2871
+ entityId: timebox.id,
2872
+ eventType: action === "create"
2873
+ ? "task_timebox_created"
2874
+ : action === "update"
2875
+ ? "task_timebox_updated"
2876
+ : "task_timebox_deleted",
2877
+ title: `Task timebox ${action}: ${timebox.title}`,
2878
+ description: action === "create"
2879
+ ? "A future work slot was planned in Forge."
2880
+ : action === "update"
2881
+ ? "The planned work slot was updated."
2882
+ : "The planned work slot was removed.",
2883
+ actor: auth.actor ?? null,
2884
+ source: auth.source,
2885
+ metadata: {
2886
+ taskId: timebox.taskId ?? null,
2887
+ status: action === "delete" ? null : (timebox.status ?? null)
2888
+ }
2889
+ });
2890
+ }
2891
+ }
2892
+ };
1179
2893
  const requireOperatorSession = (headers, detail) => {
1180
2894
  const context = authenticateRequest(headers);
1181
2895
  managers.authorization.requireAuthenticatedOperator(context, detail);
@@ -1231,9 +2945,18 @@ export async function buildServer(options = {}) {
1231
2945
  return context;
1232
2946
  };
1233
2947
  app.get("/api/health", async () => buildHealthPayload(taskRunWatchdog));
1234
- app.get("/api/v1/health", async () => buildHealthPayload(taskRunWatchdog, {
2948
+ app.get("/api/v1/health", async (request) => buildHealthPayload(taskRunWatchdog, {
1235
2949
  apiVersion: "v1",
1236
- backend: "forge-node-runtime"
2950
+ backend: "forge-node-runtime",
2951
+ ...(shouldIncludeRuntimeProbe(request.headers)
2952
+ ? {
2953
+ runtime: {
2954
+ pid: process.pid,
2955
+ storageRoot: runtimeConfig.dataRoot ?? process.cwd(),
2956
+ basePath: runtimeConfig.basePath
2957
+ }
2958
+ }
2959
+ : {})
1237
2960
  }));
1238
2961
  app.get("/api/v1/auth/operator-session", async (request, reply) => ({
1239
2962
  session: managers.session.ensureLocalOperatorSession(request.headers, reply)
@@ -1244,13 +2967,17 @@ export async function buildServer(options = {}) {
1244
2967
  app.get("/api/v1/openapi.json", async () => buildOpenApiDocument());
1245
2968
  app.get("/api/v1/context", async () => buildV1Context());
1246
2969
  app.get("/api/v1/operator/context", async (request) => {
1247
- requireOperatorSession(request.headers, { route: "/api/v1/operator/context" });
2970
+ requireOperatorSession(request.headers, {
2971
+ route: "/api/v1/operator/context"
2972
+ });
1248
2973
  return {
1249
2974
  context: buildOperatorContext()
1250
2975
  };
1251
2976
  });
1252
2977
  app.get("/api/v1/operator/overview", async (request) => {
1253
- requireOperatorSession(request.headers, { route: "/api/v1/operator/overview" });
2978
+ requireOperatorSession(request.headers, {
2979
+ route: "/api/v1/operator/overview"
2980
+ });
1254
2981
  return {
1255
2982
  overview: buildOperatorOverview(request)
1256
2983
  };
@@ -1606,8 +3333,16 @@ export async function buildServer(options = {}) {
1606
3333
  }
1607
3334
  return {
1608
3335
  report,
1609
- notes: listNotes({ linkedEntityType: "trigger_report", linkedEntityId: id, limit: 50 }),
1610
- insights: listInsights({ entityType: "trigger_report", entityId: id, limit: 50 })
3336
+ notes: listNotes({
3337
+ linkedEntityType: "trigger_report",
3338
+ linkedEntityId: id,
3339
+ limit: 50
3340
+ }),
3341
+ insights: listInsights({
3342
+ entityType: "trigger_report",
3343
+ entityId: id,
3344
+ limit: 50
3345
+ })
1611
3346
  };
1612
3347
  });
1613
3348
  app.patch("/api/v1/psyche/reports/:id", async (request, reply) => {
@@ -1686,7 +3421,10 @@ export async function buildServer(options = {}) {
1686
3421
  app.delete("/api/v1/notes/:id", async (request, reply) => {
1687
3422
  const { id } = request.params;
1688
3423
  const current = getNoteById(id);
1689
- const linkedEntityType = current?.links.find((link) => isPsycheEntityType(link.entityType))?.entityType ?? current?.links[0]?.entityType ?? null;
3424
+ const linkedEntityType = current?.links.find((link) => isPsycheEntityType(link.entityType))
3425
+ ?.entityType ??
3426
+ current?.links[0]?.entityType ??
3427
+ null;
1690
3428
  const auth = requireNoteAccess(request.headers, linkedEntityType, {
1691
3429
  route: "/api/v1/notes/:id",
1692
3430
  entityType: linkedEntityType
@@ -1721,6 +3459,160 @@ export async function buildServer(options = {}) {
1721
3459
  const query = taskListQuerySchema.parse(request.query ?? {});
1722
3460
  return { tasks: listTasks(query) };
1723
3461
  });
3462
+ app.get("/api/v1/calendar/overview", async (request) => {
3463
+ const query = calendarOverviewQuerySchema.parse(request.query ?? {});
3464
+ const now = new Date();
3465
+ const from = query.from ??
3466
+ new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
3467
+ const to = query.to ??
3468
+ new Date(now.getTime() + 21 * 24 * 60 * 60 * 1000).toISOString();
3469
+ return { calendar: readCalendarOverview({ from, to }) };
3470
+ });
3471
+ app.get("/api/v1/calendar/agenda", async (request) => {
3472
+ const query = calendarOverviewQuerySchema.parse(request.query ?? {});
3473
+ const now = new Date();
3474
+ const from = query.from ??
3475
+ new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
3476
+ const to = query.to ??
3477
+ new Date(now.getTime() + 21 * 24 * 60 * 60 * 1000).toISOString();
3478
+ return {
3479
+ providers: listCalendarProviderMetadata(),
3480
+ calendars: listCalendars(),
3481
+ events: listCalendarEvents({ from, to }),
3482
+ workBlocks: listWorkBlockInstances({ from, to }),
3483
+ timeboxes: listTaskTimeboxes({ from, to })
3484
+ };
3485
+ });
3486
+ app.get("/api/v1/calendar/connections", async () => ({
3487
+ providers: listCalendarProviderMetadata(),
3488
+ connections: listConnectedCalendarConnections()
3489
+ }));
3490
+ app.post("/api/v1/calendar/oauth/microsoft/start", async (request) => {
3491
+ requireScopedAccess(request.headers, ["write"], {
3492
+ route: "/api/v1/calendar/oauth/microsoft/start"
3493
+ });
3494
+ return await startMicrosoftCalendarOauth(startMicrosoftCalendarOauthSchema.parse(request.body ?? {}), getRequestOrigin(request));
3495
+ });
3496
+ app.post("/api/v1/calendar/oauth/microsoft/test-config", async (request) => {
3497
+ requireScopedAccess(request.headers, ["write"], {
3498
+ route: "/api/v1/calendar/oauth/microsoft/test-config"
3499
+ });
3500
+ return {
3501
+ result: await testMicrosoftCalendarOauthConfiguration(testMicrosoftCalendarOauthConfigurationSchema.parse(request.body ?? {}))
3502
+ };
3503
+ });
3504
+ app.get("/api/v1/calendar/oauth/microsoft/session/:id", async (request, reply) => {
3505
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/oauth/microsoft/session/:id" });
3506
+ try {
3507
+ return getMicrosoftCalendarOauthSession(request.params.id);
3508
+ }
3509
+ catch (error) {
3510
+ if (error instanceof Error &&
3511
+ error.message.startsWith("Unknown Microsoft calendar auth session")) {
3512
+ reply.code(404);
3513
+ return { error: "Microsoft calendar auth session not found" };
3514
+ }
3515
+ throw error;
3516
+ }
3517
+ });
3518
+ app.get("/api/v1/calendar/oauth/microsoft/callback", async (request, reply) => {
3519
+ const query = request.query;
3520
+ const result = await completeMicrosoftCalendarOauth({
3521
+ state: query.state ?? null,
3522
+ code: query.code ?? null,
3523
+ error: query.error ?? null,
3524
+ errorDescription: query.error_description ?? null
3525
+ });
3526
+ const session = result.session;
3527
+ const escapedOrigin = JSON.stringify(result.openerOrigin || "*");
3528
+ const escapedMessage = JSON.stringify({
3529
+ type: "forge:microsoft-calendar-auth",
3530
+ sessionId: session.sessionId,
3531
+ status: session.status
3532
+ });
3533
+ const body = `<!doctype html>
3534
+ <html lang="en">
3535
+ <head>
3536
+ <meta charset="utf-8" />
3537
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
3538
+ <title>Forge Microsoft sign-in</title>
3539
+ <style>
3540
+ body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;background:#0b1320;color:#f8fafc;display:grid;place-items:center;min-height:100vh}
3541
+ main{max-width:28rem;padding:2rem;border:1px solid rgba(255,255,255,.08);border-radius:24px;background:linear-gradient(180deg,rgba(18,28,38,.98),rgba(11,17,28,.98))}
3542
+ h1{margin:0 0 .75rem;font-size:1.15rem}
3543
+ p{margin:0;color:rgba(248,250,252,.72);line-height:1.6}
3544
+ </style>
3545
+ </head>
3546
+ <body>
3547
+ <main>
3548
+ <h1>${session.status === "authorized" ? "Microsoft account connected" : "Microsoft sign-in needs attention"}</h1>
3549
+ <p>${session.status === "authorized" ? "Forge received your Microsoft account and sent the result back to the calendar setup flow. You can close this window." : (session.error ?? "Forge could not complete Microsoft sign-in. You can close this window and try again from Settings.")}</p>
3550
+ </main>
3551
+ <script>
3552
+ const message = ${escapedMessage};
3553
+ const targetOrigin = ${escapedOrigin};
3554
+ try {
3555
+ if (window.opener && !window.opener.closed) {
3556
+ window.opener.postMessage(message, targetOrigin);
3557
+ }
3558
+ } catch {}
3559
+ setTimeout(() => window.close(), 180);
3560
+ </script>
3561
+ </body>
3562
+ </html>`;
3563
+ reply.type("text/html; charset=utf-8");
3564
+ return body;
3565
+ });
3566
+ app.post("/api/v1/calendar/discovery", async (request) => {
3567
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/discovery" });
3568
+ const discovery = await discoverCalendarConnection(discoverCalendarConnectionSchema.parse(request.body ?? {}));
3569
+ recordActivityEvent({
3570
+ entityType: "calendar_connection",
3571
+ entityId: "calendar_discovery",
3572
+ eventType: "calendar_connection_discovered",
3573
+ title: `Calendar discovery completed for ${discovery.provider}`,
3574
+ description: "Forge discovered provider calendars before connection setup.",
3575
+ actor: auth.actor ?? null,
3576
+ source: auth.source,
3577
+ metadata: {
3578
+ provider: discovery.provider,
3579
+ calendars: discovery.calendars.length
3580
+ }
3581
+ });
3582
+ return { discovery };
3583
+ });
3584
+ app.get("/api/v1/calendar/calendars", async () => ({
3585
+ calendars: listCalendars()
3586
+ }));
3587
+ app.get("/api/v1/calendar/connections/:id/discovery", async (request, reply) => {
3588
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/connections/:id/discovery" });
3589
+ const { id } = request.params;
3590
+ try {
3591
+ const discovery = await discoverExistingCalendarConnection(id, managers.secrets);
3592
+ return { discovery };
3593
+ }
3594
+ catch (error) {
3595
+ if (error instanceof Error &&
3596
+ error.message.startsWith("Unknown calendar connection")) {
3597
+ reply.code(404);
3598
+ return { error: "Calendar connection not found" };
3599
+ }
3600
+ throw error;
3601
+ }
3602
+ });
3603
+ app.get("/api/v1/habits", async (request) => {
3604
+ const query = habitListQuerySchema.parse(request.query ?? {});
3605
+ return { habits: listHabits(query) };
3606
+ });
3607
+ app.get("/api/v1/habits/:id", async (request, reply) => {
3608
+ const { id } = request.params;
3609
+ const habit = getHabitById(id);
3610
+ if (!habit) {
3611
+ reply.code(404);
3612
+ return { error: "Habit not found" };
3613
+ }
3614
+ return { habit };
3615
+ });
1724
3616
  app.get("/api/v1/projects/:id", async (request, reply) => {
1725
3617
  const { id } = request.params;
1726
3618
  const project = listProjectSummaries().find((entry) => entry.id === id);
@@ -1754,7 +3646,9 @@ export async function buildServer(options = {}) {
1754
3646
  return { activity: listActivityEvents(query) };
1755
3647
  });
1756
3648
  app.post("/api/v1/activity/:id/remove", async (request, reply) => {
1757
- requireScopedAccess(request.headers, ["write"], { route: "/api/v1/activity/:id/remove" });
3649
+ requireScopedAccess(request.headers, ["write"], {
3650
+ route: "/api/v1/activity/:id/remove"
3651
+ });
1758
3652
  const { id } = request.params;
1759
3653
  const event = removeActivityEvent(id, removeActivityEventSchema.parse(request.body ?? {}), parseActivityContext(request.headers));
1760
3654
  if (!event) {
@@ -1764,7 +3658,7 @@ export async function buildServer(options = {}) {
1764
3658
  return { event };
1765
3659
  });
1766
3660
  app.get("/api/v1/metrics", async () => ({
1767
- metrics: buildGamificationOverview(listGoals(), listTasks())
3661
+ metrics: buildGamificationOverview(listGoals(), listTasks(), listHabits())
1768
3662
  }));
1769
3663
  app.get("/api/v1/metrics/xp", async () => ({
1770
3664
  metrics: buildXpMetricsPayload()
@@ -1778,7 +3672,10 @@ export async function buildServer(options = {}) {
1778
3672
  route: "/api/v1/insights",
1779
3673
  entityType: input.entityType
1780
3674
  });
1781
- const insight = createInsight(input, { actor: auth.actor, source: auth.source });
3675
+ const insight = createInsight(input, {
3676
+ actor: auth.actor,
3677
+ source: auth.source
3678
+ });
1782
3679
  reply.code(201);
1783
3680
  return { insight };
1784
3681
  });
@@ -1815,7 +3712,10 @@ export async function buildServer(options = {}) {
1815
3712
  const query = entityDeleteQuerySchema.parse(request.query ?? {});
1816
3713
  const insight = query.mode === "hard"
1817
3714
  ? deleteInsight(id, { actor: auth.actor, source: auth.source })
1818
- : deleteEntity("insight", id, query, { actor: auth.actor, source: auth.source });
3715
+ : deleteEntity("insight", id, query, {
3716
+ actor: auth.actor,
3717
+ source: auth.source
3718
+ });
1819
3719
  if (!insight) {
1820
3720
  reply.code(404);
1821
3721
  return { error: "Insight not found" };
@@ -1837,7 +3737,9 @@ export async function buildServer(options = {}) {
1837
3737
  return { feedback };
1838
3738
  });
1839
3739
  app.get("/api/v1/approval-requests", async (request) => {
1840
- requireOperatorSession(request.headers, { route: "/api/v1/approval-requests" });
3740
+ requireOperatorSession(request.headers, {
3741
+ route: "/api/v1/approval-requests"
3742
+ });
1841
3743
  const query = request.query;
1842
3744
  return { approvalRequests: listApprovalRequests(query?.status) };
1843
3745
  });
@@ -1845,7 +3747,9 @@ export async function buildServer(options = {}) {
1845
3747
  const context = requireOperatorSession(request.headers, { route: "/api/v1/approval-requests/:id/approve" });
1846
3748
  const { id } = request.params;
1847
3749
  const body = resolveApprovalRequestSchema.parse(request.body ?? {});
1848
- const approvalRequest = approveApprovalRequest(id, body.note, body.actor ?? context.actor ?? parseOptionalActorHeader(request.headers));
3750
+ const approvalRequest = approveApprovalRequest(id, body.note, body.actor ??
3751
+ context.actor ??
3752
+ parseOptionalActorHeader(request.headers));
1849
3753
  if (!approvalRequest) {
1850
3754
  reply.code(404);
1851
3755
  return { error: "Approval request not found" };
@@ -1856,7 +3760,9 @@ export async function buildServer(options = {}) {
1856
3760
  const context = requireOperatorSession(request.headers, { route: "/api/v1/approval-requests/:id/reject" });
1857
3761
  const { id } = request.params;
1858
3762
  const body = resolveApprovalRequestSchema.parse(request.body ?? {});
1859
- const approvalRequest = rejectApprovalRequest(id, body.note, body.actor ?? context.actor ?? parseOptionalActorHeader(request.headers));
3763
+ const approvalRequest = rejectApprovalRequest(id, body.note, body.actor ??
3764
+ context.actor ??
3765
+ parseOptionalActorHeader(request.headers));
1860
3766
  if (!approvalRequest) {
1861
3767
  reply.code(404);
1862
3768
  return { error: "Approval request not found" };
@@ -1877,16 +3783,24 @@ export async function buildServer(options = {}) {
1877
3783
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/agent-actions" });
1878
3784
  const input = createAgentActionSchema.parse(request.body ?? {});
1879
3785
  const idempotencyKey = parseIdempotencyKey(request.headers);
1880
- const result = createAgentAction(input, { actor: auth.actor, source: auth.source, token: auth.token ? managers.token.getTokenById(auth.token.id) : null }, idempotencyKey);
3786
+ const result = createAgentAction(input, {
3787
+ actor: auth.actor,
3788
+ source: auth.source,
3789
+ token: auth.token ? managers.token.getTokenById(auth.token.id) : null
3790
+ }, idempotencyKey);
1881
3791
  reply.code(result.approvalRequest ? 202 : 201);
1882
3792
  return result;
1883
3793
  });
1884
3794
  app.get("/api/v1/rewards/rules", async (request) => {
1885
- requireOperatorSession(request.headers, { route: "/api/v1/rewards/rules" });
3795
+ requireOperatorSession(request.headers, {
3796
+ route: "/api/v1/rewards/rules"
3797
+ });
1886
3798
  return { rules: listRewardRules() };
1887
3799
  });
1888
3800
  app.get("/api/v1/rewards/rules/:id", async (request, reply) => {
1889
- requireOperatorSession(request.headers, { route: "/api/v1/rewards/rules/:id" });
3801
+ requireOperatorSession(request.headers, {
3802
+ route: "/api/v1/rewards/rules/:id"
3803
+ });
1890
3804
  const { id } = request.params;
1891
3805
  const rule = getRewardRuleById(id);
1892
3806
  if (!rule) {
@@ -1906,7 +3820,9 @@ export async function buildServer(options = {}) {
1906
3820
  return { rule };
1907
3821
  });
1908
3822
  app.get("/api/v1/rewards/ledger", async (request) => {
1909
- requireOperatorSession(request.headers, { route: "/api/v1/rewards/ledger" });
3823
+ requireOperatorSession(request.headers, {
3824
+ route: "/api/v1/rewards/ledger"
3825
+ });
1910
3826
  const query = rewardsLedgerQuerySchema.parse(request.query ?? {});
1911
3827
  return { ledger: listRewardLedger(query) };
1912
3828
  });
@@ -1919,7 +3835,10 @@ export async function buildServer(options = {}) {
1919
3835
  app.post("/api/v1/session-events", async (request, reply) => {
1920
3836
  const auth = requireAuthenticatedActor(request.headers, { route: "/api/v1/session-events" });
1921
3837
  const payload = createSessionEventSchema.parse(request.body ?? {});
1922
- const event = recordSessionEvent(payload, { actor: auth.actor, source: auth.source });
3838
+ const event = recordSessionEvent(payload, {
3839
+ actor: auth.actor,
3840
+ source: auth.source
3841
+ });
1923
3842
  reply.code(201);
1924
3843
  return event;
1925
3844
  });
@@ -1930,6 +3849,27 @@ export async function buildServer(options = {}) {
1930
3849
  app.get("/api/v1/reviews/weekly", async () => ({
1931
3850
  review: getWeeklyReviewPayload()
1932
3851
  }));
3852
+ app.post("/api/v1/reviews/weekly/finalize", async (request, reply) => {
3853
+ const auth = requireAuthenticatedActor(request.headers, { route: "/api/v1/reviews/weekly/finalize" });
3854
+ const currentReview = getWeeklyReviewPayload();
3855
+ const finalized = finalizeWeeklyReviewClosure({
3856
+ weekKey: currentReview.weekKey,
3857
+ weekStartDate: currentReview.weekStartDate,
3858
+ weekEndDate: currentReview.weekEndDate,
3859
+ windowLabel: currentReview.windowLabel,
3860
+ rewardXp: currentReview.reward.rewardXp,
3861
+ actor: auth.actor,
3862
+ source: auth.source
3863
+ });
3864
+ const result = finalizeWeeklyReviewResultSchema.parse({
3865
+ closure: finalized.closure,
3866
+ reward: finalized.reward,
3867
+ review: getWeeklyReviewPayload(),
3868
+ metrics: buildXpMetricsPayload()
3869
+ });
3870
+ reply.code(finalized.created ? 201 : 200);
3871
+ return result;
3872
+ });
1933
3873
  app.get("/api/v1/settings", async (request) => {
1934
3874
  requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/settings" });
1935
3875
  return { settings: getSettings() };
@@ -1944,6 +3884,301 @@ export async function buildServer(options = {}) {
1944
3884
  reply.code(201);
1945
3885
  return { project };
1946
3886
  });
3887
+ app.post("/api/v1/calendar/connections", async (request, reply) => {
3888
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/connections" });
3889
+ try {
3890
+ const connection = await createCalendarConnection(createCalendarConnectionSchema.parse(request.body ?? {}), managers.secrets, toActivityContext(auth));
3891
+ reply.code(201);
3892
+ return { connection };
3893
+ }
3894
+ catch (error) {
3895
+ if (error instanceof CalendarConnectionConflictError) {
3896
+ reply.code(409);
3897
+ return {
3898
+ code: "calendar_connection_duplicate",
3899
+ error: error.message,
3900
+ existingConnectionId: error.connectionId
3901
+ };
3902
+ }
3903
+ throw error;
3904
+ }
3905
+ });
3906
+ app.patch("/api/v1/calendar/connections/:id", async (request, reply) => {
3907
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/connections/:id" });
3908
+ const { id } = request.params;
3909
+ const patch = updateCalendarConnectionSchema.parse(request.body ?? {});
3910
+ try {
3911
+ const connection = patch.label !== undefined || patch.selectedCalendarUrls !== undefined
3912
+ ? await updateCalendarConnectionSelection(id, {
3913
+ label: patch.label,
3914
+ selectedCalendarUrls: patch.selectedCalendarUrls
3915
+ }, managers.secrets, toActivityContext(auth))
3916
+ : getCalendarConnectionById(id);
3917
+ if (!connection) {
3918
+ reply.code(404);
3919
+ return { error: "Calendar connection not found" };
3920
+ }
3921
+ return {
3922
+ connection: listConnectedCalendarConnections().find((entry) => entry.id === id)
3923
+ };
3924
+ }
3925
+ catch (error) {
3926
+ if (error instanceof Error &&
3927
+ error.message.startsWith("Unknown calendar connection")) {
3928
+ reply.code(404);
3929
+ return { error: "Calendar connection not found" };
3930
+ }
3931
+ throw error;
3932
+ }
3933
+ });
3934
+ app.post("/api/v1/calendar/connections/:id/sync", async (request, reply) => {
3935
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/connections/:id/sync" });
3936
+ const { id } = request.params;
3937
+ const connection = await syncCalendarConnection(id, managers.secrets, toActivityContext(auth));
3938
+ if (!connection) {
3939
+ reply.code(404);
3940
+ return { error: "Calendar connection not found" };
3941
+ }
3942
+ return {
3943
+ connection: listConnectedCalendarConnections().find((entry) => entry.id === id)
3944
+ };
3945
+ });
3946
+ app.delete("/api/v1/calendar/connections/:id", async (request, reply) => {
3947
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/connections/:id" });
3948
+ const { id } = request.params;
3949
+ const connection = await removeCalendarConnection(id, managers.secrets, toActivityContext(auth));
3950
+ if (!connection) {
3951
+ reply.code(404);
3952
+ return { error: "Calendar connection not found" };
3953
+ }
3954
+ return { connection };
3955
+ });
3956
+ app.get("/api/v1/calendar/work-block-templates", async () => ({
3957
+ templates: listWorkBlockTemplates()
3958
+ }));
3959
+ app.post("/api/v1/calendar/work-block-templates", async (request, reply) => {
3960
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/work-block-templates" });
3961
+ const template = createWorkBlockTemplate(createWorkBlockTemplateSchema.parse(request.body ?? {}));
3962
+ recordActivityEvent({
3963
+ entityType: "work_block",
3964
+ entityId: template.id,
3965
+ eventType: "work_block_created",
3966
+ title: `Work block created: ${template.title}`,
3967
+ description: "A recurring work block was added to Forge.",
3968
+ actor: auth.actor ?? null,
3969
+ source: auth.source,
3970
+ metadata: {
3971
+ kind: template.kind,
3972
+ blockingState: template.blockingState
3973
+ }
3974
+ });
3975
+ reply.code(201);
3976
+ return { template };
3977
+ });
3978
+ app.patch("/api/v1/calendar/work-block-templates/:id", async (request, reply) => {
3979
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/work-block-templates/:id" });
3980
+ const { id } = request.params;
3981
+ const template = updateWorkBlockTemplate(id, updateWorkBlockTemplateSchema.parse(request.body ?? {}));
3982
+ if (!template) {
3983
+ reply.code(404);
3984
+ return { error: "Work block template not found" };
3985
+ }
3986
+ recordActivityEvent({
3987
+ entityType: "work_block",
3988
+ entityId: template.id,
3989
+ eventType: "work_block_updated",
3990
+ title: `Work block updated: ${template.title}`,
3991
+ description: "The recurring work block was updated.",
3992
+ actor: auth.actor ?? null,
3993
+ source: auth.source,
3994
+ metadata: {
3995
+ kind: template.kind,
3996
+ blockingState: template.blockingState
3997
+ }
3998
+ });
3999
+ return { template };
4000
+ });
4001
+ app.delete("/api/v1/calendar/work-block-templates/:id", async (request, reply) => {
4002
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/work-block-templates/:id" });
4003
+ const { id } = request.params;
4004
+ const template = deleteWorkBlockTemplate(id);
4005
+ if (!template) {
4006
+ reply.code(404);
4007
+ return { error: "Work block template not found" };
4008
+ }
4009
+ recordActivityEvent({
4010
+ entityType: "work_block",
4011
+ entityId: template.id,
4012
+ eventType: "work_block_deleted",
4013
+ title: `Work block deleted: ${template.title}`,
4014
+ description: "The recurring work block was removed.",
4015
+ actor: auth.actor ?? null,
4016
+ source: auth.source,
4017
+ metadata: {
4018
+ kind: template.kind
4019
+ }
4020
+ });
4021
+ return { template };
4022
+ });
4023
+ app.get("/api/v1/calendar/timeboxes", async (request) => {
4024
+ const query = calendarOverviewQuerySchema.parse(request.query ?? {});
4025
+ const now = new Date();
4026
+ const from = query.from ??
4027
+ new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
4028
+ const to = query.to ??
4029
+ new Date(now.getTime() + 21 * 24 * 60 * 60 * 1000).toISOString();
4030
+ return { timeboxes: listTaskTimeboxes({ from, to }) };
4031
+ });
4032
+ app.post("/api/v1/calendar/timeboxes", async (request, reply) => {
4033
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/timeboxes" });
4034
+ const timebox = createTaskTimebox(createTaskTimeboxSchema.parse(request.body ?? {}));
4035
+ recordActivityEvent({
4036
+ entityType: "task_timebox",
4037
+ entityId: timebox.id,
4038
+ eventType: "task_timebox_created",
4039
+ title: `Task timebox created: ${timebox.title}`,
4040
+ description: "A future work slot was planned in Forge.",
4041
+ actor: auth.actor ?? null,
4042
+ source: auth.source,
4043
+ metadata: {
4044
+ taskId: timebox.taskId,
4045
+ status: timebox.status
4046
+ }
4047
+ });
4048
+ reply.code(201);
4049
+ return { timebox };
4050
+ });
4051
+ app.patch("/api/v1/calendar/timeboxes/:id", async (request, reply) => {
4052
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/timeboxes/:id" });
4053
+ const { id } = request.params;
4054
+ const timebox = updateTaskTimebox(id, updateTaskTimeboxSchema.parse(request.body ?? {}));
4055
+ if (!timebox) {
4056
+ reply.code(404);
4057
+ return { error: "Task timebox not found" };
4058
+ }
4059
+ recordActivityEvent({
4060
+ entityType: "task_timebox",
4061
+ entityId: timebox.id,
4062
+ eventType: "task_timebox_updated",
4063
+ title: `Task timebox updated: ${timebox.title}`,
4064
+ description: "The planned work slot was updated.",
4065
+ actor: auth.actor ?? null,
4066
+ source: auth.source,
4067
+ metadata: {
4068
+ taskId: timebox.taskId,
4069
+ status: timebox.status
4070
+ }
4071
+ });
4072
+ return { timebox };
4073
+ });
4074
+ app.delete("/api/v1/calendar/timeboxes/:id", async (request, reply) => {
4075
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/timeboxes/:id" });
4076
+ const { id } = request.params;
4077
+ const timebox = deleteTaskTimebox(id);
4078
+ if (!timebox) {
4079
+ reply.code(404);
4080
+ return { error: "Task timebox not found" };
4081
+ }
4082
+ recordActivityEvent({
4083
+ entityType: "task_timebox",
4084
+ entityId: timebox.id,
4085
+ eventType: "task_timebox_deleted",
4086
+ title: `Task timebox deleted: ${timebox.title}`,
4087
+ description: "The planned work slot was removed.",
4088
+ actor: auth.actor ?? null,
4089
+ source: auth.source,
4090
+ metadata: {
4091
+ taskId: timebox.taskId
4092
+ }
4093
+ });
4094
+ return { timebox };
4095
+ });
4096
+ app.post("/api/v1/calendar/timeboxes/recommend", async (request) => {
4097
+ const input = recommendTaskTimeboxesSchema.parse(request.body ?? {});
4098
+ return {
4099
+ timeboxes: suggestTaskTimeboxes(input.taskId, {
4100
+ from: input.from,
4101
+ to: input.to,
4102
+ limit: input.limit
4103
+ })
4104
+ };
4105
+ });
4106
+ app.post("/api/v1/calendar/events", async (request, reply) => {
4107
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/events" });
4108
+ const event = createCalendarEvent(createCalendarEventSchema.parse(request.body ?? {}));
4109
+ await pushCalendarEventUpdate(event.id, managers.secrets);
4110
+ const refreshed = getCalendarEventById(event.id);
4111
+ recordActivityEvent({
4112
+ entityType: "calendar_event",
4113
+ entityId: refreshed.id,
4114
+ eventType: "calendar_event_created",
4115
+ title: `Calendar event created: ${refreshed.title}`,
4116
+ description: "A native Forge calendar event was created.",
4117
+ actor: auth.actor ?? null,
4118
+ source: auth.source,
4119
+ metadata: {
4120
+ calendarId: refreshed.calendarId,
4121
+ originType: refreshed.originType
4122
+ }
4123
+ });
4124
+ reply.code(201);
4125
+ return { event: refreshed };
4126
+ });
4127
+ app.patch("/api/v1/calendar/events/:id", async (request, reply) => {
4128
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/events/:id" });
4129
+ const { id } = request.params;
4130
+ const event = updateCalendarEvent(id, updateCalendarEventSchema.parse(request.body ?? {}));
4131
+ if (!event) {
4132
+ reply.code(404);
4133
+ return { error: "Calendar event not found" };
4134
+ }
4135
+ await pushCalendarEventUpdate(id, managers.secrets);
4136
+ const refreshed = getCalendarEventById(id);
4137
+ recordActivityEvent({
4138
+ entityType: "calendar_event",
4139
+ entityId: refreshed.id,
4140
+ eventType: "calendar_event_updated",
4141
+ title: `Calendar event updated: ${refreshed.title}`,
4142
+ description: "The Forge calendar event was updated and projected to remote calendars when configured.",
4143
+ actor: auth.actor ?? null,
4144
+ source: auth.source,
4145
+ metadata: {
4146
+ calendarId: refreshed.calendarId,
4147
+ originType: refreshed.originType
4148
+ }
4149
+ });
4150
+ return { event: refreshed };
4151
+ });
4152
+ app.delete("/api/v1/calendar/events/:id", async (request, reply) => {
4153
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/events/:id" });
4154
+ const { id } = request.params;
4155
+ const event = deleteCalendarEvent(id);
4156
+ if (!event) {
4157
+ reply.code(404);
4158
+ return { error: "Calendar event not found" };
4159
+ }
4160
+ await deleteCalendarEventProjection(id, managers.secrets);
4161
+ recordActivityEvent({
4162
+ entityType: "calendar_event",
4163
+ entityId: event.id,
4164
+ eventType: "calendar_event_deleted",
4165
+ title: `Calendar event deleted: ${event.title}`,
4166
+ description: "The Forge calendar event was removed and any projected remote copies were deleted.",
4167
+ actor: auth.actor ?? null,
4168
+ source: auth.source,
4169
+ metadata: {
4170
+ calendarId: event.calendarId,
4171
+ originType: event.originType
4172
+ }
4173
+ });
4174
+ return { event };
4175
+ });
4176
+ app.post("/api/v1/habits", async (request, reply) => {
4177
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits" });
4178
+ const habit = createHabit(createHabitSchema.parse(request.body ?? {}), toActivityContext(auth));
4179
+ reply.code(201);
4180
+ return { habit };
4181
+ });
1947
4182
  app.patch("/api/v1/projects/:id", async (request, reply) => {
1948
4183
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/projects/:id" });
1949
4184
  const { id } = request.params;
@@ -1964,6 +4199,36 @@ export async function buildServer(options = {}) {
1964
4199
  }
1965
4200
  return { project };
1966
4201
  });
4202
+ app.patch("/api/v1/habits/:id", async (request, reply) => {
4203
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id" });
4204
+ const { id } = request.params;
4205
+ const habit = updateHabit(id, updateHabitSchema.parse(request.body ?? {}), toActivityContext(auth));
4206
+ if (!habit) {
4207
+ reply.code(404);
4208
+ return { error: "Habit not found" };
4209
+ }
4210
+ return { habit };
4211
+ });
4212
+ app.delete("/api/v1/habits/:id", async (request, reply) => {
4213
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id" });
4214
+ const { id } = request.params;
4215
+ const habit = deleteEntity("habit", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
4216
+ if (!habit) {
4217
+ reply.code(404);
4218
+ return { error: "Habit not found" };
4219
+ }
4220
+ return { habit };
4221
+ });
4222
+ app.post("/api/v1/habits/:id/check-ins", async (request, reply) => {
4223
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id/check-ins" });
4224
+ const { id } = request.params;
4225
+ const habit = createHabitCheckIn(id, createHabitCheckInSchema.parse(request.body ?? {}), toActivityContext(auth));
4226
+ if (!habit) {
4227
+ reply.code(404);
4228
+ return { error: "Habit not found" };
4229
+ }
4230
+ return { habit, metrics: buildXpMetricsPayload() };
4231
+ });
1967
4232
  app.patch("/api/v1/settings", async (request) => {
1968
4233
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/settings" });
1969
4234
  return {
@@ -2072,7 +4337,7 @@ export async function buildServer(options = {}) {
2072
4337
  app.get("/api/metrics", async (_request, reply) => {
2073
4338
  markCompatibilityRoute(reply);
2074
4339
  return {
2075
- metrics: buildGamificationProfile(listGoals(), listTasks())
4340
+ metrics: buildGamificationProfile(listGoals(), listTasks(), listHabits())
2076
4341
  };
2077
4342
  });
2078
4343
  app.get("/api/task-runs", async (request, reply) => {
@@ -2102,7 +4367,7 @@ export async function buildServer(options = {}) {
2102
4367
  markCompatibilityRoute(reply);
2103
4368
  const query = taskListQuerySchema.parse(request.query ?? {});
2104
4369
  return {
2105
- metrics: buildGamificationProfile(listGoals(), listTasks()),
4370
+ metrics: buildGamificationProfile(listGoals(), listTasks(), listHabits()),
2106
4371
  dashboard: getDashboard(),
2107
4372
  overview: getOverviewContext(),
2108
4373
  today: getTodayContext(),
@@ -2111,6 +4376,7 @@ export async function buildServer(options = {}) {
2111
4376
  projects: listProjectSummaries(),
2112
4377
  tags: listTags(),
2113
4378
  tasks: listTasks(query),
4379
+ habits: listHabits(),
2114
4380
  activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
2115
4381
  activity: listActivityEvents({ limit: 25 })
2116
4382
  };
@@ -2145,8 +4411,10 @@ export async function buildServer(options = {}) {
2145
4411
  const taskRuns = listTaskRuns({ taskId: id, limit: 10 });
2146
4412
  return taskContextPayloadSchema.parse({
2147
4413
  task,
2148
- goal: task.goalId ? getGoalById(task.goalId) ?? null : null,
2149
- project: task.projectId ? listProjectSummaries().find((project) => project.id === task.projectId) ?? null : null,
4414
+ goal: task.goalId ? (getGoalById(task.goalId) ?? null) : null,
4415
+ project: task.projectId
4416
+ ? (listProjectSummaries().find((project) => project.id === task.projectId) ?? null)
4417
+ : null,
2150
4418
  activeTaskRun: taskRuns.find((run) => run.status === "active") ?? null,
2151
4419
  taskRuns,
2152
4420
  activity: listActivityEventsForTask(id, 20),
@@ -2163,8 +4431,10 @@ export async function buildServer(options = {}) {
2163
4431
  const taskRuns = listTaskRuns({ taskId: id, limit: 10 });
2164
4432
  return taskContextPayloadSchema.parse({
2165
4433
  task,
2166
- goal: task.goalId ? getGoalById(task.goalId) ?? null : null,
2167
- project: task.projectId ? listProjectSummaries().find((project) => project.id === task.projectId) ?? null : null,
4434
+ goal: task.goalId ? (getGoalById(task.goalId) ?? null) : null,
4435
+ project: task.projectId
4436
+ ? (listProjectSummaries().find((project) => project.id === task.projectId) ?? null)
4437
+ : null,
2168
4438
  activeTaskRun: taskRuns.find((run) => run.status === "active") ?? null,
2169
4439
  taskRuns,
2170
4440
  activity: listActivityEventsForTask(id, 20),
@@ -2340,7 +4610,9 @@ export async function buildServer(options = {}) {
2340
4610
  const input = operatorLogWorkSchema.parse(request.body ?? {});
2341
4611
  if (input.taskId) {
2342
4612
  const task = updateTask(input.taskId, {
2343
- title: input.title && input.title.trim().length > 0 ? input.title : undefined,
4613
+ title: input.title && input.title.trim().length > 0
4614
+ ? input.title
4615
+ : undefined,
2344
4616
  description: typeof input.description === "string"
2345
4617
  ? input.description
2346
4618
  : input.summary.trim().length > 0
@@ -2386,6 +4658,72 @@ export async function buildServer(options = {}) {
2386
4658
  reply.code(201);
2387
4659
  return { task, xp: buildXpMetricsPayload() };
2388
4660
  });
4661
+ app.post("/api/v1/work-adjustments", async (request, reply) => {
4662
+ const auth = requireScopedAccess(request.headers, ["write", "rewards.manage"], { route: "/api/v1/work-adjustments" });
4663
+ const input = createWorkAdjustmentSchema.parse(request.body ?? {});
4664
+ const currentTarget = resolveWorkAdjustmentTarget(input.entityType, input.entityId);
4665
+ if (!currentTarget) {
4666
+ reply.code(404);
4667
+ return {
4668
+ error: `${input.entityType === "task" ? "Task" : "Project"} not found`
4669
+ };
4670
+ }
4671
+ const appliedDeltaMinutes = clampWorkAdjustmentMinutes(input.deltaMinutes, currentTarget.time.totalCreditedSeconds);
4672
+ const nextCreditedSeconds = Math.max(0, currentTarget.time.totalCreditedSeconds + appliedDeltaMinutes * 60);
4673
+ const result = runInTransaction(() => {
4674
+ const adjustment = createWorkAdjustment({
4675
+ ...input,
4676
+ appliedDeltaMinutes
4677
+ }, toActivityContext(auth));
4678
+ const reward = recordWorkAdjustmentReward({
4679
+ entityType: input.entityType,
4680
+ entityId: input.entityId,
4681
+ targetTitle: currentTarget.title,
4682
+ actor: auth.actor ?? null,
4683
+ source: auth.source,
4684
+ requestedDeltaMinutes: input.deltaMinutes,
4685
+ appliedDeltaMinutes,
4686
+ previousCreditedSeconds: currentTarget.time.totalCreditedSeconds,
4687
+ nextCreditedSeconds,
4688
+ adjustmentId: adjustment.id
4689
+ });
4690
+ const copy = describeWorkAdjustment({
4691
+ entityType: input.entityType,
4692
+ targetTitle: currentTarget.title,
4693
+ requestedDeltaMinutes: input.deltaMinutes,
4694
+ appliedDeltaMinutes
4695
+ });
4696
+ recordActivityEvent({
4697
+ entityType: input.entityType,
4698
+ entityId: input.entityId,
4699
+ eventType: "work_adjusted",
4700
+ title: copy.title,
4701
+ description: copy.description,
4702
+ actor: auth.actor ?? null,
4703
+ source: auth.source,
4704
+ metadata: {
4705
+ adjustmentId: adjustment.id,
4706
+ requestedDeltaMinutes: input.deltaMinutes,
4707
+ appliedDeltaMinutes,
4708
+ rewardDeltaXp: reward?.deltaXp ?? 0,
4709
+ rewardId: reward?.id ?? null,
4710
+ note: input.note || null
4711
+ }
4712
+ });
4713
+ return { adjustment, reward };
4714
+ });
4715
+ const updatedTarget = resolveWorkAdjustmentTarget(input.entityType, input.entityId);
4716
+ if (!updatedTarget) {
4717
+ throw new HttpError(500, "work_adjustment_target_missing", `Could not reload ${input.entityType} ${input.entityId} after adjustment`);
4718
+ }
4719
+ reply.code(201);
4720
+ return workAdjustmentResultSchema.parse({
4721
+ adjustment: result.adjustment,
4722
+ target: updatedTarget,
4723
+ reward: result.reward,
4724
+ metrics: buildXpMetricsPayload()
4725
+ });
4726
+ });
2389
4727
  app.post("/api/v1/tasks/:id/uncomplete", async (request, reply) => {
2390
4728
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/tasks/:id/uncomplete" });
2391
4729
  const { id } = request.params;
@@ -2399,18 +4737,26 @@ export async function buildServer(options = {}) {
2399
4737
  });
2400
4738
  app.post("/api/v1/entities/create", async (request) => {
2401
4739
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/entities/create" });
2402
- return createEntities(batchCreateEntitiesSchema.parse(request.body ?? {}), toActivityContext(auth));
4740
+ const result = createEntities(batchCreateEntitiesSchema.parse(request.body ?? {}), toActivityContext(auth));
4741
+ await applyBatchCalendarEntityEffects(result.results, auth, "create");
4742
+ return result;
2403
4743
  });
2404
4744
  app.post("/api/v1/entities/update", async (request) => {
2405
4745
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/entities/update" });
2406
- return updateEntities(batchUpdateEntitiesSchema.parse(request.body ?? {}), toActivityContext(auth));
4746
+ const result = updateEntities(batchUpdateEntitiesSchema.parse(request.body ?? {}), toActivityContext(auth));
4747
+ await applyBatchCalendarEntityEffects(result.results, auth, "update");
4748
+ return result;
2407
4749
  });
2408
4750
  app.post("/api/v1/entities/delete", async (request) => {
2409
4751
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/entities/delete" });
2410
- return deleteEntities(batchDeleteEntitiesSchema.parse(request.body ?? {}), toActivityContext(auth));
4752
+ const result = deleteEntities(batchDeleteEntitiesSchema.parse(request.body ?? {}), toActivityContext(auth));
4753
+ await applyBatchCalendarEntityEffects(result.results, auth, "delete");
4754
+ return result;
2411
4755
  });
2412
4756
  app.post("/api/v1/entities/restore", async (request) => {
2413
- requireScopedAccess(request.headers, ["write"], { route: "/api/v1/entities/restore" });
4757
+ requireScopedAccess(request.headers, ["write"], {
4758
+ route: "/api/v1/entities/restore"
4759
+ });
2414
4760
  return restoreEntities(batchRestoreEntitiesSchema.parse(request.body ?? {}));
2415
4761
  });
2416
4762
  app.post("/api/v1/entities/search", async (request) => {
@@ -2419,7 +4765,9 @@ export async function buildServer(options = {}) {
2419
4765
  });
2420
4766
  app.post("/api/task-runs/recover", async (request, reply) => {
2421
4767
  markCompatibilityRoute(reply);
2422
- const payload = taskRunListQuerySchema.pick({ limit: true }).parse(request.body ?? {});
4768
+ const payload = taskRunListQuerySchema
4769
+ .pick({ limit: true })
4770
+ .parse(request.body ?? {});
2423
4771
  return { timedOutRuns: recoverTimedOutTaskRuns({ limit: payload.limit }) };
2424
4772
  });
2425
4773
  app.post("/api/task-runs/:id/heartbeat", async (request, reply) => {
@@ -2427,52 +4775,68 @@ export async function buildServer(options = {}) {
2427
4775
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/heartbeat" });
2428
4776
  const { id } = request.params;
2429
4777
  const input = taskRunHeartbeatSchema.parse(request.body ?? {});
2430
- return { taskRun: heartbeatTaskRun(id, input, new Date(), toActivityContext(auth)) };
4778
+ return {
4779
+ taskRun: heartbeatTaskRun(id, input, new Date(), toActivityContext(auth))
4780
+ };
2431
4781
  });
2432
4782
  app.post("/api/v1/task-runs/:id/heartbeat", async (request) => {
2433
4783
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/heartbeat" });
2434
4784
  const { id } = request.params;
2435
4785
  const input = taskRunHeartbeatSchema.parse(request.body ?? {});
2436
- return { taskRun: heartbeatTaskRun(id, input, new Date(), toActivityContext(auth)) };
4786
+ return {
4787
+ taskRun: heartbeatTaskRun(id, input, new Date(), toActivityContext(auth))
4788
+ };
2437
4789
  });
2438
4790
  app.post("/api/task-runs/:id/focus", async (request, reply) => {
2439
4791
  markCompatibilityRoute(reply);
2440
4792
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/focus" });
2441
4793
  const { id } = request.params;
2442
4794
  const input = taskRunFocusSchema.parse(request.body ?? {});
2443
- return { taskRun: focusTaskRun(id, input, new Date(), toActivityContext(auth)) };
4795
+ return {
4796
+ taskRun: focusTaskRun(id, input, new Date(), toActivityContext(auth))
4797
+ };
2444
4798
  });
2445
4799
  app.post("/api/v1/task-runs/:id/focus", async (request) => {
2446
4800
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/focus" });
2447
4801
  const { id } = request.params;
2448
4802
  const input = taskRunFocusSchema.parse(request.body ?? {});
2449
- return { taskRun: focusTaskRun(id, input, new Date(), toActivityContext(auth)) };
4803
+ return {
4804
+ taskRun: focusTaskRun(id, input, new Date(), toActivityContext(auth))
4805
+ };
2450
4806
  });
2451
4807
  app.post("/api/task-runs/:id/complete", async (request, reply) => {
2452
4808
  markCompatibilityRoute(reply);
2453
4809
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/complete" });
2454
4810
  const { id } = request.params;
2455
4811
  const input = taskRunFinishSchema.parse(request.body ?? {});
2456
- return { taskRun: completeTaskRun(id, input, new Date(), toActivityContext(auth)) };
4812
+ return {
4813
+ taskRun: completeTaskRun(id, input, new Date(), toActivityContext(auth))
4814
+ };
2457
4815
  });
2458
4816
  app.post("/api/v1/task-runs/:id/complete", async (request) => {
2459
4817
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/complete" });
2460
4818
  const { id } = request.params;
2461
4819
  const input = taskRunFinishSchema.parse(request.body ?? {});
2462
- return { taskRun: completeTaskRun(id, input, new Date(), toActivityContext(auth)) };
4820
+ return {
4821
+ taskRun: completeTaskRun(id, input, new Date(), toActivityContext(auth))
4822
+ };
2463
4823
  });
2464
4824
  app.post("/api/task-runs/:id/release", async (request, reply) => {
2465
4825
  markCompatibilityRoute(reply);
2466
4826
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/release" });
2467
4827
  const { id } = request.params;
2468
4828
  const input = taskRunFinishSchema.parse(request.body ?? {});
2469
- return { taskRun: releaseTaskRun(id, input, new Date(), toActivityContext(auth)) };
4829
+ return {
4830
+ taskRun: releaseTaskRun(id, input, new Date(), toActivityContext(auth))
4831
+ };
2470
4832
  });
2471
4833
  app.post("/api/v1/task-runs/:id/release", async (request) => {
2472
4834
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/release" });
2473
4835
  const { id } = request.params;
2474
4836
  const input = taskRunFinishSchema.parse(request.body ?? {});
2475
- return { taskRun: releaseTaskRun(id, input, new Date(), toActivityContext(auth)) };
4837
+ return {
4838
+ taskRun: releaseTaskRun(id, input, new Date(), toActivityContext(auth))
4839
+ };
2476
4840
  });
2477
4841
  app.post("/api/tags/suggestions", async (request, reply) => {
2478
4842
  markCompatibilityRoute(reply);