forge-openclaw-plugin 0.2.18 → 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 (56) hide show
  1. package/README.md +36 -4
  2. package/dist/assets/{board-2KevHCI0.js → board-8L3uX7_O.js} +2 -2
  3. package/dist/assets/{board-2KevHCI0.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-q19HPmWs.js → motion-1GAqqi8M.js} +2 -2
  8. package/dist/assets/{motion-q19HPmWs.js.map → motion-1GAqqi8M.js.map} +1 -1
  9. package/dist/assets/{table-BDMHBY4a.js → table-DBGlgRjk.js} +2 -2
  10. package/dist/assets/{table-BDMHBY4a.js.map → table-DBGlgRjk.js.map} +1 -1
  11. package/dist/assets/{ui-CQ_AsFs8.js → ui-iTluWjC4.js} +2 -2
  12. package/dist/assets/{ui-CQ_AsFs8.js.map → ui-iTluWjC4.js.map} +1 -1
  13. package/dist/assets/{vendor-5HifrnRK.js → vendor-BvM2F9Dp.js} +139 -84
  14. package/dist/assets/vendor-BvM2F9Dp.js.map +1 -0
  15. package/dist/assets/{viz-CQzkRnTu.js → viz-CNeunkfu.js} +2 -2
  16. package/dist/assets/{viz-CQzkRnTu.js.map → viz-CNeunkfu.js.map} +1 -1
  17. package/dist/index.html +8 -8
  18. package/dist/openclaw/parity.js +1 -0
  19. package/dist/openclaw/routes.js +7 -0
  20. package/dist/openclaw/tools.js +183 -16
  21. package/dist/server/app.js +2509 -263
  22. package/dist/server/managers/platform/secrets-manager.js +44 -1
  23. package/dist/server/managers/runtime.js +3 -1
  24. package/dist/server/openapi.js +2037 -172
  25. package/dist/server/repositories/calendar.js +1101 -0
  26. package/dist/server/repositories/deleted-entities.js +10 -2
  27. package/dist/server/repositories/notes.js +161 -28
  28. package/dist/server/repositories/projects.js +45 -13
  29. package/dist/server/repositories/rewards.js +114 -6
  30. package/dist/server/repositories/settings.js +47 -5
  31. package/dist/server/repositories/task-runs.js +46 -10
  32. package/dist/server/repositories/tasks.js +25 -9
  33. package/dist/server/repositories/weekly-reviews.js +109 -0
  34. package/dist/server/repositories/work-adjustments.js +105 -0
  35. package/dist/server/services/calendar-runtime.js +1301 -0
  36. package/dist/server/services/entity-crud.js +94 -3
  37. package/dist/server/services/projects.js +32 -8
  38. package/dist/server/services/reviews.js +15 -1
  39. package/dist/server/services/work-time.js +27 -0
  40. package/dist/server/types.js +934 -49
  41. package/openclaw.plugin.json +1 -1
  42. package/package.json +1 -1
  43. package/server/migrations/006_work_adjustments.sql +14 -0
  44. package/server/migrations/007_weekly_review_closures.sql +17 -0
  45. package/server/migrations/008_calendar_execution.sql +147 -0
  46. package/server/migrations/009_true_calendar_events.sql +195 -0
  47. package/server/migrations/010_calendar_selection_state.sql +6 -0
  48. package/server/migrations/011_calendar_timezone_backfill.sql +11 -0
  49. package/server/migrations/012_work_block_ranges.sql +7 -0
  50. package/server/migrations/013_microsoft_local_auth_settings.sql +8 -0
  51. package/server/migrations/014_note_tags_and_ephemeral.sql +8 -0
  52. package/skills/forge-openclaw/SKILL.md +117 -11
  53. package/dist/assets/index-CDYW4WDH.js +0 -36
  54. package/dist/assets/index-CDYW4WDH.js.map +0 -1
  55. package/dist/assets/index-yroQr6YZ.css +0 -1
  56. package/dist/assets/vendor-5HifrnRK.js.map +0 -1
@@ -1,9 +1,9 @@
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";
@@ -12,23 +12,27 @@ import { listDomains } from "./repositories/domains.js";
12
12
  import { buildNotesSummaryByEntity, createNote, getNoteById, listNotes, updateNote } from "./repositories/notes.js";
13
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";
14
14
  import { createProject, updateProject } from "./repositories/projects.js";
15
- 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";
16
16
  import { listAgentIdentities, getSettings, isPsycheAuthRequired, updateSettings, verifyAgentToken } from "./repositories/settings.js";
17
17
  import { createTag, getTagById, listTags, updateTag } from "./repositories/tags.js";
18
18
  import { claimTaskRun, completeTaskRun, focusTaskRun, heartbeatTaskRun, listTaskRuns, recoverTimedOutTaskRuns, releaseTaskRun } from "./repositories/task-runs.js";
19
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";
20
22
  import { getDashboard } from "./services/dashboard.js";
21
23
  import { getOverviewContext, getRiskContext, getTodayContext } from "./services/context.js";
22
24
  import { buildGamificationOverview, buildGamificationProfile, buildXpMomentumPulse } from "./services/gamification.js";
23
25
  import { getInsightsPayload } from "./services/insights.js";
24
26
  import { createEntities, deleteEntities, deleteEntity, getSettingsBinPayload, restoreEntities, searchEntities, updateEntities } from "./services/entity-crud.js";
25
27
  import { getPsycheOverview } from "./services/psyche.js";
26
- import { getProjectBoard, listProjectSummaries } from "./services/projects.js";
28
+ import { getProjectBoard, getProjectSummary, listProjectSummaries } from "./services/projects.js";
27
29
  import { getWeeklyReviewPayload } from "./services/reviews.js";
30
+ import { finalizeWeeklyReviewClosure } from "./repositories/weekly-reviews.js";
28
31
  import { createTaskRunWatchdog } from "./services/task-run-watchdog.js";
29
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";
30
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";
31
- import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createHabitCheckInSchema, createHabitSchema, createSessionEventSchema, createTagSchema, notesListQuerySchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskSchema } from "./types.js";
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";
32
36
  import { buildOpenApiDocument } from "./openapi.js";
33
37
  import { registerWebRoutes } from "./web.js";
34
38
  import { createManagerRuntime } from "./managers/runtime.js";
@@ -106,7 +110,9 @@ function readSingleForwardedHeader(value) {
106
110
  return null;
107
111
  }
108
112
  function getRequestOrigin(request) {
109
- 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";
110
116
  const host = readSingleForwardedHeader(request.headers["x-forwarded-host"]) ??
111
117
  readSingleForwardedHeader(request.headers.host) ??
112
118
  request.hostname;
@@ -122,20 +128,72 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
122
128
  "Projects should usually link to one goal through goalId.",
123
129
  "Tasks can link directly to a goal when no project exists yet."
124
130
  ],
125
- 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
+ ],
126
135
  examples: [
127
136
  '{"title":"Create meaningfully","horizon":"lifetime","description":"Make work that is honest, beautiful, and published."}',
128
137
  '{"title":"Build a beautiful family","horizon":"lifetime","description":"Invest in love, stability, and shared rituals."}'
129
138
  ],
130
139
  fieldGuide: [
131
- { name: "title", type: "string", required: true, description: "Human-readable goal name." },
132
- { name: "description", type: "string", required: false, description: "Why the goal matters or what success looks like.", defaultValue: "" },
133
- { name: "horizon", type: "quarter|year|lifetime", required: false, description: "How far out the goal is meant to live.", enumValues: ["quarter", "year", "lifetime"], defaultValue: "year" },
134
- { name: "status", type: "active|paused|completed", required: false, description: "Current lifecycle state for the goal.", enumValues: ["active", "paused", "completed"], defaultValue: "active" },
135
- { name: "targetPoints", type: "integer", required: false, description: "Approximate XP/point target for the goal.", defaultValue: 400 },
136
- { name: "themeColor", type: "hex-color", required: false, description: "Visual color used in the UI.", defaultValue: "#c8a46b" },
137
- { name: "tagIds", type: "string[]", required: false, description: "Existing tag ids linked to the goal.", defaultValue: [] },
138
- { 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
+ }
139
197
  ]
140
198
  },
141
199
  {
@@ -147,16 +205,61 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
147
205
  "Tasks can link to a project through projectId.",
148
206
  "Projects inherit strategic meaning from their parent goal."
149
207
  ],
150
- searchHints: ["Search by title inside the target goal before creating a new project."],
151
- 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
+ ],
152
214
  fieldGuide: [
153
- { name: "goalId", type: "string", required: true, description: "Existing parent goal id." },
154
- { name: "title", type: "string", required: true, description: "Project name." },
155
- { name: "description", type: "string", required: false, description: "Desired outcome or scope.", defaultValue: "" },
156
- { name: "status", type: "active|paused|completed", required: false, description: "Lifecycle state.", enumValues: ["active", "paused", "completed"], defaultValue: "active" },
157
- { name: "targetPoints", type: "integer", required: false, description: "Approximate XP/point target for the project.", defaultValue: 240 },
158
- { name: "themeColor", type: "hex-color", required: false, description: "Visual color used in the UI.", defaultValue: "#c0c1ff" },
159
- { 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
+ }
160
263
  ]
161
264
  },
162
265
  {
@@ -168,23 +271,394 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
168
271
  "Live work is tracked by task runs, not by task status alone.",
169
272
  "A task status of in_progress does not guarantee a live active run."
170
273
  ],
171
- searchHints: ["Search by title before creating a duplicate task.", "Use linkedTo filters when you know the parent goal or project."],
172
- 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
+ ],
173
281
  fieldGuide: [
174
- { name: "title", type: "string", required: true, description: "Concrete action label." },
175
- { name: "description", type: "string", required: false, description: "Helpful context or acceptance notes.", defaultValue: "" },
176
- { 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" },
177
- { name: "priority", type: "low|medium|high|critical", required: false, description: "Relative urgency.", enumValues: ["low", "medium", "high", "critical"], defaultValue: "medium" },
178
- { name: "owner", type: "string", required: false, description: "Human-facing owner label.", defaultValue: "Albert" },
179
- { name: "goalId", type: "string|null", required: false, description: "Linked goal id.", defaultValue: null, nullable: true },
180
- { name: "projectId", type: "string|null", required: false, description: "Linked project id.", defaultValue: null, nullable: true },
181
- { name: "dueDate", type: "YYYY-MM-DD|null", required: false, description: "Optional due date.", defaultValue: null, nullable: true },
182
- { name: "effort", type: "light|deep|marathon", required: false, description: "How heavy the task feels.", enumValues: ["light", "deep", "marathon"], defaultValue: "deep" },
183
- { name: "energy", type: "low|steady|high", required: false, description: "Energy demand.", enumValues: ["low", "steady", "high"], defaultValue: "steady" },
184
- { name: "points", type: "integer", required: false, description: "Reward value for the task.", defaultValue: 40 },
185
- { name: "sortOrder", type: "integer", required: false, description: "Lane ordering hint when set explicitly." },
186
- { name: "tagIds", type: "string[]", required: false, description: "Existing tag ids linked to the task.", defaultValue: [] },
187
- { name: "notes", type: "Array<{ contentMarkdown, author?, links? }>", required: false, description: "Optional nested notes that will auto-link to the new task.", defaultValue: [] }
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
+ ],
605
+ fieldGuide: [
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
+ }
188
662
  ]
189
663
  },
190
664
  {
@@ -196,47 +670,205 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
196
670
  "Habits are recurring records, not task variants, and they participate in search, notes, delete/restore, and XP.",
197
671
  "linkedBehaviorId remains a compatibility alias; linkedBehaviorIds is the canonical array form."
198
672
  ],
199
- searchHints: ["Search by title before creating a duplicate habit.", "Use linkedTo when the habit should already be attached to a goal, project, task, or Psyche entity."],
200
- examples: ['{"title":"Morning training","frequency":"daily","polarity":"positive","linkedGoalIds":["goal_train_body"],"linkedValueIds":["value_steadiness"],"linkedBehaviorIds":["behavior_regulating_walk"]}'],
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
+ ],
201
680
  fieldGuide: [
202
- { name: "title", type: "string", required: true, description: "Concrete recurring behavior label." },
203
- { name: "description", type: "string", required: false, description: "What counts as success or failure for this habit.", defaultValue: "" },
204
- { name: "status", type: "active|paused|archived", required: false, description: "Lifecycle state.", enumValues: ["active", "paused", "archived"], defaultValue: "active" },
205
- { name: "polarity", type: "positive|negative", required: false, description: "Whether doing the behavior is aligned or misaligned.", enumValues: ["positive", "negative"], defaultValue: "positive" },
206
- { name: "frequency", type: "daily|weekly", required: false, description: "Recurrence cadence.", enumValues: ["daily", "weekly"], defaultValue: "daily" },
207
- { name: "targetCount", type: "integer", required: false, description: "How many repetitions define the cadence window.", defaultValue: 1 },
208
- { name: "weekDays", type: "integer[]", required: false, description: "Weekday numbers for weekly habits where Monday is 1 and Sunday is 0.", defaultValue: [] },
209
- { name: "linkedGoalIds", type: "string[]", required: false, description: "Linked goal ids.", defaultValue: [] },
210
- { name: "linkedProjectIds", type: "string[]", required: false, description: "Linked project ids.", defaultValue: [] },
211
- { name: "linkedTaskIds", type: "string[]", required: false, description: "Linked task ids.", defaultValue: [] },
212
- { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
213
- { name: "linkedPatternIds", type: "string[]", required: false, description: "Linked pattern ids.", defaultValue: [] },
214
- { name: "linkedBehaviorIds", type: "string[]", required: false, description: "Canonical linked behavior ids.", defaultValue: [] },
215
- { name: "linkedBehaviorId", type: "string|null", required: false, description: "Compatibility alias for the first linked behavior id.", defaultValue: null, nullable: true },
216
- { name: "linkedBeliefIds", type: "string[]", required: false, description: "Linked belief ids.", defaultValue: [] },
217
- { name: "linkedModeIds", type: "string[]", required: false, description: "Linked mode ids.", defaultValue: [] },
218
- { name: "linkedReportIds", type: "string[]", required: false, description: "Linked trigger report ids.", defaultValue: [] },
219
- { name: "rewardXp", type: "integer", required: false, description: "XP granted on aligned check-ins.", defaultValue: 12 },
220
- { name: "penaltyXp", type: "integer", required: false, description: "XP removed on misaligned check-ins.", defaultValue: 8 }
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
+ }
221
817
  ]
222
818
  },
223
819
  {
224
820
  entityType: "note",
225
- 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.",
226
822
  minimumCreateFields: ["contentMarkdown", "links"],
227
823
  relationshipRules: [
228
824
  "Notes can link to goals, projects, tasks, Psyche records, and other supported Forge entities.",
229
- "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."
230
830
  ],
231
- searchHints: ["Search by Markdown content, author, or linked entity before creating a duplicate note."],
232
831
  examples: [
233
832
  '{"contentMarkdown":"Finished the review pass and captured the remaining edge cases.","links":[{"entityType":"task","entityId":"task_123"}]}',
234
- '{"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"}]}'
235
835
  ],
236
836
  fieldGuide: [
237
- { name: "contentMarkdown", type: "string", required: true, description: "Markdown body of the note." },
238
- { name: "author", type: "string|null", required: false, description: "Optional display author for the note.", defaultValue: null, nullable: true },
239
- { 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
+ }
240
872
  ]
241
873
  },
242
874
  {
@@ -247,19 +879,83 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
247
879
  "Insights can optionally point at one entity through entityType and entityId.",
248
880
  "Use insights for interpretation or advice, not as a replacement for goals, tasks, or trigger reports."
249
881
  ],
250
- searchHints: ["Search recent insights before posting a new one if the same pattern may already be captured."],
251
- 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
+ ],
252
888
  fieldGuide: [
253
- { name: "entityType", type: "string|null", required: false, description: "Optional linked entity type.", defaultValue: null, nullable: true },
254
- { name: "entityId", type: "string|null", required: false, description: "Optional linked entity id.", defaultValue: null, nullable: true },
255
- { name: "timeframeLabel", type: "string|null", required: false, description: "Optional time window label.", defaultValue: null, nullable: true },
256
- { name: "title", type: "string", required: true, description: "Insight title." },
257
- { name: "summary", type: "string", required: true, description: "Short explanation of the pattern or tension." },
258
- { name: "recommendation", type: "string", required: true, description: "Actionable next move or reframing." },
259
- { name: "rationale", type: "string", required: false, description: "Why this insight is grounded in the data.", defaultValue: "" },
260
- { name: "confidence", type: "number", required: false, description: "Confidence from 0 to 1.", defaultValue: 0.7 },
261
- { name: "visibility", type: "string", required: false, description: "Visibility mode for the insight.", defaultValue: "visible" },
262
- { 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
+ }
263
959
  ]
264
960
  },
265
961
  {
@@ -270,10 +966,24 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
270
966
  "Trigger reports can reference one event type through eventTypeId.",
271
967
  "Use event types to normalize repeated report categories instead of inventing new wording every time."
272
968
  ],
273
- 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
+ ],
274
973
  fieldGuide: [
275
- { name: "label", type: "string", required: true, description: "Human-readable event type label." },
276
- { 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
+ }
277
987
  ]
278
988
  },
279
989
  {
@@ -284,11 +994,31 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
284
994
  "Trigger report emotions can reference an emotion definition through emotionDefinitionId.",
285
995
  "Use emotion definitions to normalize repeated emotional labels across reports."
286
996
  ],
287
- 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
+ ],
288
1001
  fieldGuide: [
289
- { name: "label", type: "string", required: true, description: "Emotion label." },
290
- { name: "description", type: "string", required: false, description: "What this emotion label is meant to capture.", defaultValue: "" },
291
- { 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
+ }
292
1022
  ]
293
1023
  },
294
1024
  {
@@ -299,17 +1029,69 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
299
1029
  "Values can link to goals, projects, and tasks.",
300
1030
  "Patterns, behaviors, beliefs, and reports can all point back to values."
301
1031
  ],
302
- searchHints: ["Search by title before creating a new value.", "Use linkedTo if the value should already be attached to a goal or task."],
303
- 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
+ ],
304
1039
  fieldGuide: [
305
- { name: "title", type: "string", required: true, description: "Value name." },
306
- { name: "description", type: "string", required: false, description: "What the value means in practice.", defaultValue: "" },
307
- { name: "valuedDirection", type: "string", required: false, description: "How the user wants to live or act when guided by this value.", defaultValue: "" },
308
- { name: "whyItMatters", type: "string", required: false, description: "Why the value matters to the user.", defaultValue: "" },
309
- { name: "linkedGoalIds", type: "string[]", required: false, description: "Linked goal ids.", defaultValue: [] },
310
- { name: "linkedProjectIds", type: "string[]", required: false, description: "Linked project ids.", defaultValue: [] },
311
- { name: "linkedTaskIds", type: "string[]", required: false, description: "Linked task ids.", defaultValue: [] },
312
- { 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
+ }
313
1095
  ]
314
1096
  },
315
1097
  {
@@ -320,21 +1102,96 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
320
1102
  "Patterns can link to values, beliefs, and modes.",
321
1103
  "Trigger reports can link back to patterns they instantiate."
322
1104
  ],
323
- searchHints: ["Search by title or by trigger language before creating a new pattern."],
324
- 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
+ ],
325
1111
  fieldGuide: [
326
- { name: "title", type: "string", required: true, description: "Short pattern name." },
327
- { name: "description", type: "string", required: false, description: "What usually happens in this loop.", defaultValue: "" },
328
- { name: "targetBehavior", type: "string", required: false, description: "The visible behavior this pattern tends to produce.", defaultValue: "" },
329
- { name: "cueContexts", type: "string[]", required: false, description: "Typical cues, contexts, or triggers.", defaultValue: [] },
330
- { name: "shortTermPayoff", type: "string", required: false, description: "What the loop gives immediately.", defaultValue: "" },
331
- { name: "longTermCost", type: "string", required: false, description: "What the loop costs later.", defaultValue: "" },
332
- { name: "preferredResponse", type: "string", required: false, description: "Preferred alternative response.", defaultValue: "" },
333
- { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
334
- { name: "linkedSchemaLabels", type: "string[]", required: false, description: "Schema labels involved in the pattern.", defaultValue: [] },
335
- { name: "linkedModeLabels", type: "string[]", required: false, description: "Mode labels involved in the pattern.", defaultValue: [] },
336
- { name: "linkedModeIds", type: "string[]", required: false, description: "Linked mode ids.", defaultValue: [] },
337
- { 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
+ }
338
1195
  ]
339
1196
  },
340
1197
  {
@@ -346,21 +1203,100 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
346
1203
  "Trigger reports can link to behaviors they contained."
347
1204
  ],
348
1205
  searchHints: ["Search by title and kind before creating a new behavior."],
349
- 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
+ ],
350
1209
  fieldGuide: [
351
- { 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"] },
352
- { name: "title", type: "string", required: true, description: "Behavior label." },
353
- { name: "description", type: "string", required: false, description: "What the behavior looks like.", defaultValue: "" },
354
- { name: "commonCues", type: "string[]", required: false, description: "Typical cues for this behavior.", defaultValue: [] },
355
- { name: "urgeStory", type: "string", required: false, description: "What the inner urge or story feels like.", defaultValue: "" },
356
- { name: "shortTermPayoff", type: "string", required: false, description: "Immediate payoff.", defaultValue: "" },
357
- { name: "longTermCost", type: "string", required: false, description: "Longer-term cost.", defaultValue: "" },
358
- { name: "replacementMove", type: "string", required: false, description: "Preferred replacement move.", defaultValue: "" },
359
- { name: "repairPlan", type: "string", required: false, description: "Repair plan after the behavior occurs.", defaultValue: "" },
360
- { name: "linkedPatternIds", type: "string[]", required: false, description: "Linked behavior pattern ids.", defaultValue: [] },
361
- { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
362
- { name: "linkedSchemaIds", type: "string[]", required: false, description: "Linked schema ids.", defaultValue: [] },
363
- { 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
+ }
364
1300
  ]
365
1301
  },
366
1302
  {
@@ -371,21 +1307,97 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
371
1307
  "Beliefs can link to values, behaviors, modes, and trigger reports.",
372
1308
  "Behavior patterns can point to beliefs that keep the loop alive."
373
1309
  ],
374
- searchHints: ["Search by statement or known schema theme before creating a new belief entry."],
375
- 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
+ ],
376
1316
  fieldGuide: [
377
- { name: "schemaId", type: "string|null", required: false, description: "Optional linked schema catalog id.", defaultValue: null, nullable: true },
378
- { name: "statement", type: "string", required: true, description: "Belief statement in the user's own words." },
379
- { name: "beliefType", type: "absolute|conditional", required: true, description: "Whether the belief is absolute or if-then shaped.", enumValues: ["absolute", "conditional"] },
380
- { name: "originNote", type: "string", required: false, description: "Where the belief seems to come from.", defaultValue: "" },
381
- { name: "confidence", type: "integer", required: false, description: "How strongly the belief feels true from 0 to 100.", defaultValue: 60 },
382
- { name: "evidenceFor", type: "string[]", required: false, description: "Evidence that seems to support the belief.", defaultValue: [] },
383
- { name: "evidenceAgainst", type: "string[]", required: false, description: "Evidence that weakens the belief.", defaultValue: [] },
384
- { name: "flexibleAlternative", type: "string", required: false, description: "More flexible alternative belief.", defaultValue: "" },
385
- { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
386
- { name: "linkedBehaviorIds", type: "string[]", required: false, description: "Linked behavior ids.", defaultValue: [] },
387
- { name: "linkedModeIds", type: "string[]", required: false, description: "Linked mode ids.", defaultValue: [] },
388
- { 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
+ }
389
1401
  ]
390
1402
  },
391
1403
  {
@@ -396,24 +1408,124 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
396
1408
  "Modes can link to patterns, behaviors, and values.",
397
1409
  "Trigger reports can include linkedModeIds and modeOverlays that reference modes."
398
1410
  ],
399
- searchHints: ["Search by title or family before creating a new mode profile."],
400
- 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
+ ],
401
1417
  fieldGuide: [
402
- { 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"] },
403
- { name: "title", type: "string", required: true, description: "Mode title." },
404
- { name: "archetype", type: "string", required: false, description: "Optional archetype label.", defaultValue: "" },
405
- { name: "persona", type: "string", required: false, description: "Narrative or felt sense of the mode.", defaultValue: "" },
406
- { name: "imagery", type: "string", required: false, description: "Imagery associated with the mode.", defaultValue: "" },
407
- { name: "symbolicForm", type: "string", required: false, description: "Symbolic form or metaphor.", defaultValue: "" },
408
- { name: "facialExpression", type: "string", required: false, description: "Typical facial expression or posture.", defaultValue: "" },
409
- { name: "fear", type: "string", required: false, description: "Core fear carried by the mode.", defaultValue: "" },
410
- { name: "burden", type: "string", required: false, description: "Burden or pain the mode carries.", defaultValue: "" },
411
- { name: "protectiveJob", type: "string", required: false, description: "What job the mode thinks it is doing.", defaultValue: "" },
412
- { name: "originContext", type: "string", required: false, description: "Where the mode seems to come from.", defaultValue: "" },
413
- { name: "firstAppearanceAt", type: "string|null", required: false, description: "Optional first-seen marker.", defaultValue: null, nullable: true },
414
- { name: "linkedPatternIds", type: "string[]", required: false, description: "Linked pattern ids.", defaultValue: [] },
415
- { name: "linkedBehaviorIds", type: "string[]", required: false, description: "Linked behavior ids.", defaultValue: [] },
416
- { 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
+ }
417
1529
  ]
418
1530
  },
419
1531
  {
@@ -424,12 +1536,31 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
424
1536
  "Mode guide sessions help the user reason toward likely modes before or alongside mode profiles.",
425
1537
  "Use mode guide sessions for guided interpretation, not as a replacement for durable mode profiles."
426
1538
  ],
427
- searchHints: ["Search by summary when revisiting a prior guided mode session."],
428
- 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
+ ],
429
1545
  fieldGuide: [
430
- { name: "summary", type: "string", required: true, description: "Short summary of what the guided session explored." },
431
- { name: "answers", type: "array", required: true, description: "List of { questionKey, value } items capturing the user's guided answers." },
432
- { 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
+ }
433
1564
  ]
434
1565
  },
435
1566
  {
@@ -441,31 +1572,168 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
441
1572
  "A report is the best container for one specific emotionally meaningful episode.",
442
1573
  "Use reports when you need one event chain, not just a generic pattern."
443
1574
  ],
444
- searchHints: ["Search by title, event wording, or linked entities before creating a duplicate report."],
445
- 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
+ ],
446
1581
  fieldGuide: [
447
- { name: "title", type: "string", required: true, description: "Short name for the incident." },
448
- { name: "status", type: "draft|reviewed|integrated", required: false, description: "Reflection progress state.", enumValues: ["draft", "reviewed", "integrated"], defaultValue: "draft" },
449
- { name: "eventTypeId", type: "string|null", required: false, description: "Known event type id if already cataloged.", defaultValue: null, nullable: true },
450
- { name: "customEventType", type: "string", required: false, description: "Free-text event type when no existing type fits.", defaultValue: "" },
451
- { name: "eventSituation", type: "string", required: false, description: "What happened in the situation.", defaultValue: "" },
452
- { name: "occurredAt", type: "string|null", required: false, description: "When it happened.", defaultValue: null, nullable: true },
453
- { name: "emotions", type: "array", required: false, description: "List of { emotionDefinitionId|null, label, intensity 0-100, note } items.", defaultValue: [] },
454
- { name: "thoughts", type: "array", required: false, description: "List of { text, parentMode, criticMode, beliefId|null } items.", defaultValue: [] },
455
- { name: "behaviors", type: "array", required: false, description: "List of { text, mode, behaviorId|null } items.", defaultValue: [] },
456
- { name: "consequences", type: "object", required: false, description: "Object with selfShortTerm, selfLongTerm, othersShortTerm, othersLongTerm string arrays." },
457
- { name: "linkedPatternIds", type: "string[]", required: false, description: "Linked pattern ids.", defaultValue: [] },
458
- { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
459
- { name: "linkedGoalIds", type: "string[]", required: false, description: "Linked goal ids.", defaultValue: [] },
460
- { name: "linkedProjectIds", type: "string[]", required: false, description: "Linked project ids.", defaultValue: [] },
461
- { name: "linkedTaskIds", type: "string[]", required: false, description: "Linked task ids.", defaultValue: [] },
462
- { name: "linkedBehaviorIds", type: "string[]", required: false, description: "Linked behavior ids.", defaultValue: [] },
463
- { name: "linkedBeliefIds", type: "string[]", required: false, description: "Linked belief ids.", defaultValue: [] },
464
- { name: "linkedModeIds", type: "string[]", required: false, description: "Linked mode ids.", defaultValue: [] },
465
- { name: "modeOverlays", type: "string[]", required: false, description: "Extra mode labels noticed during the incident.", defaultValue: [] },
466
- { name: "schemaLinks", type: "string[]", required: false, description: "Schema names or themes that seem related to the incident.", defaultValue: [] },
467
- { name: "modeTimeline", type: "array", required: false, description: "List of { stage, modeId|null, label, note } items describing the sequence of modes.", defaultValue: [] },
468
- { 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
+ }
469
1737
  ]
470
1738
  }
471
1739
  ];
@@ -483,7 +1751,17 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
483
1751
  "Name the preferred alternative response."
484
1752
  ],
485
1753
  requiredForCreate: ["title"],
486
- 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
+ ],
487
1765
  exampleQuestions: [
488
1766
  "What usually sets this loop off?",
489
1767
  "What do you tend to do next, outwardly or inwardly?",
@@ -509,7 +1787,17 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
509
1787
  "Link a schemaId only when a real schema catalog match is known."
510
1788
  ],
511
1789
  requiredForCreate: ["statement", "beliefType"],
512
- 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
+ ],
513
1801
  exampleQuestions: [
514
1802
  "What is the sentence your mind seems to be pushing here?",
515
1803
  "Is it more of an always/never belief, or an if-then rule?",
@@ -534,7 +1822,17 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
534
1822
  "Optionally note origin context and linked patterns or behaviors."
535
1823
  ],
536
1824
  requiredForCreate: ["family", "title"],
537
- 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
+ ],
538
1836
  exampleQuestions: [
539
1837
  "What kind of part does this feel like: coping, child, critic-parent, healthy-adult, or happy-child?",
540
1838
  "If you gave this mode a name, what would it be?",
@@ -560,7 +1858,22 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
560
1858
  "Identify next moves and linked patterns, beliefs, modes, values, or tasks."
561
1859
  ],
562
1860
  requiredForCreate: ["title"],
563
- 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
+ ],
564
1877
  exampleQuestions: [
565
1878
  "What happened, as concretely as you can say it?",
566
1879
  "What emotions were there, and how intense were they?",
@@ -593,11 +1906,16 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
593
1906
  summary: "Create one or more entities in one ordered batch.",
594
1907
  whenToUse: "Use after explicit save intent and after duplicate checks when needed.",
595
1908
  inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, clientRef?: string, data: object }> }",
596
- requiredFields: ["operations", "operations[].entityType", "operations[].data"],
1909
+ requiredFields: [
1910
+ "operations",
1911
+ "operations[].entityType",
1912
+ "operations[].data"
1913
+ ],
597
1914
  notes: [
598
1915
  "entityType alone is never enough; full data is required.",
599
1916
  "Batch multiple related creates together when they come from one user ask.",
600
- "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."
601
1919
  ],
602
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"}]}'
603
1921
  },
@@ -606,17 +1924,37 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
606
1924
  summary: "Patch one or more entities in one ordered batch.",
607
1925
  whenToUse: "Use when ids are known and the user explicitly wants a change persisted.",
608
1926
  inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, id: string, clientRef?: string, patch: object }> }",
609
- requiredFields: ["operations", "operations[].entityType", "operations[].id", "operations[].patch"],
610
- notes: ["patch is partial; only send the fields that should change."],
611
- 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"}]}'
612
1941
  },
613
1942
  {
614
1943
  toolName: "forge_delete_entities",
615
1944
  summary: "Delete one or more entities through the batch delete flow.",
616
1945
  whenToUse: "Use for explicit delete intent only.",
617
- inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, id: string, clientRef?: string, mode?: \"soft\"|\"hard\", reason?: string }> }",
618
- requiredFields: ["operations", "operations[].entityType", "operations[].id"],
619
- 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
+ ],
620
1958
  example: '{"operations":[{"entityType":"task","id":"task_123","mode":"soft","reason":"Merged into another task"}]}'
621
1959
  },
622
1960
  {
@@ -624,17 +1962,100 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
624
1962
  summary: "Restore soft-deleted entities from the settings bin.",
625
1963
  whenToUse: "Use when the user wants an entity brought back after a soft delete.",
626
1964
  inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, id: string, clientRef?: string }> }",
627
- requiredFields: ["operations", "operations[].entityType", "operations[].id"],
1965
+ requiredFields: [
1966
+ "operations",
1967
+ "operations[].entityType",
1968
+ "operations[].id"
1969
+ ],
628
1970
  notes: ["Restore only works for soft-deleted entities."],
629
1971
  example: '{"operations":[{"entityType":"goal","id":"goal_123","clientRef":"goal-restore-1"}]}'
630
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
+ },
631
2049
  {
632
2050
  toolName: "forge_grant_reward_bonus",
633
2051
  summary: "Grant an explicit manual XP bonus or penalty with clear provenance.",
634
2052
  whenToUse: "Use when the user or operator explicitly wants an auditable reward adjustment beyond the automatic task and habit reward paths.",
635
2053
  inputShape: "{ entityType: RewardableEntityType, entityId: string, deltaXp: integer, reasonTitle: string, reasonSummary?: string, metadata?: object }",
636
2054
  requiredFields: ["entityType", "entityId", "deltaXp", "reasonTitle"],
637
- notes: ["Requires rewards.manage and write scopes.", "Use this for explicit operator judgement, not as a substitute for normal task_run or habit check-in rewards."],
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
+ ],
638
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"}}'
639
2060
  },
640
2061
  {
@@ -643,26 +2064,50 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
643
2064
  whenToUse: "Use when you have a data-grounded observation or recommendation worth keeping visible in Forge.",
644
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 }",
645
2066
  requiredFields: ["title", "summary", "recommendation"],
646
- 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
+ ],
647
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}'
648
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
+ },
649
2085
  {
650
2086
  toolName: "forge_log_work",
651
2087
  summary: "Log work that already happened.",
652
- whenToUse: "Use for retroactive work, not for starting a live session.",
653
- 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? }> } }",
654
2090
  requiredFields: ["taskId or title"],
655
- 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
+ ],
656
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."}}'
657
2098
  },
658
2099
  {
659
2100
  toolName: "forge_start_task_run",
660
2101
  summary: "Start truthful live work on a task.",
661
2102
  whenToUse: "Use when the user wants to begin working now.",
662
- 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 }',
663
2104
  requiredFields: ["taskId", "actor"],
664
- notes: ["If timerMode is planned, plannedDurationSeconds is required.", "If timerMode is unlimited, plannedDurationSeconds must be null or omitted."],
665
- 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"}'
666
2111
  },
667
2112
  {
668
2113
  toolName: "forge_heartbeat_task_run",
@@ -679,25 +2124,33 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
679
2124
  whenToUse: "Use when several runs exist and one should be the visible current run.",
680
2125
  inputShape: "{ taskRunId: string, actor?: string }",
681
2126
  requiredFields: ["taskRunId"],
682
- 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
+ ],
683
2130
  example: '{"taskRunId":"run_123","actor":"aurel"}'
684
2131
  },
685
2132
  {
686
2133
  toolName: "forge_complete_task_run",
687
2134
  summary: "Finish an active run as completed work.",
688
2135
  whenToUse: "Use when the user has finished the live work block.",
689
- 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? }> } }",
690
2137
  requiredFields: ["taskRunId"],
691
- 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
+ ],
692
2142
  example: '{"taskRunId":"run_123","actor":"aurel","note":"Finished the review draft","closeoutNote":{"contentMarkdown":"Completed the draft review and listed the follow-up fixes."}}'
693
2143
  },
694
2144
  {
695
2145
  toolName: "forge_release_task_run",
696
2146
  summary: "Stop an active run without marking the task complete.",
697
2147
  whenToUse: "Use when the user is stopping or pausing work without completion.",
698
- 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? }> } }",
699
2149
  requiredFields: ["taskRunId"],
700
- 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
+ ],
701
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."}}'
702
2155
  }
703
2156
  ];
@@ -755,11 +2208,14 @@ function buildAgentOnboardingPayload(request) {
755
2208
  },
756
2209
  conceptModel: {
757
2210
  goal: "Long-horizon direction or outcome. Goals anchor projects and sometimes tasks directly.",
758
- 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.",
759
2212
  task: "A concrete actionable work item. Task status is board state, not proof of live work.",
760
2213
  taskRun: "A live work session attached to a task. Start, heartbeat, focus, complete, and release runs instead of faking work with status alone.",
761
2214
  note: "A Markdown work note that can link to one or many entities. Use notes for progress evidence, context, and close-out summaries.",
762
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.",
763
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."
764
2220
  },
765
2221
  psycheSubmoduleModel: {
@@ -792,6 +2248,7 @@ function buildAgentOnboardingPayload(request) {
792
2248
  context: "/api/v1/context",
793
2249
  xpMetrics: "/api/v1/metrics/xp",
794
2250
  weeklyReview: "/api/v1/reviews/weekly",
2251
+ calendarOverview: "/api/v1/calendar/overview",
795
2252
  settingsBin: "/api/v1/settings/bin",
796
2253
  batchSearch: "/api/v1/entities/search",
797
2254
  psycheSchemaCatalog: "/api/v1/psyche/schema-catalog",
@@ -817,6 +2274,7 @@ function buildAgentOnboardingPayload(request) {
817
2274
  ],
818
2275
  rewardWorkflow: ["forge_grant_reward_bonus"],
819
2276
  workWorkflow: [
2277
+ "forge_adjust_work_minutes",
820
2278
  "forge_log_work",
821
2279
  "forge_start_task_run",
822
2280
  "forge_heartbeat_task_run",
@@ -824,6 +2282,14 @@ function buildAgentOnboardingPayload(request) {
824
2282
  "forge_complete_task_run",
825
2283
  "forge_release_task_run"
826
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
+ ],
827
2293
  insightWorkflow: ["forge_post_insight"]
828
2294
  },
829
2295
  interactionGuidance: {
@@ -846,14 +2312,14 @@ function buildAgentOnboardingPayload(request) {
846
2312
  },
847
2313
  deleteDefault: "soft",
848
2314
  hardDeleteRequiresExplicitMode: true,
849
- restoreSummary: "Restore soft-deleted entities through the restore route or the settings bin.",
850
- 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.",
851
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.",
852
2318
  searchRule: "forge_search_entities accepts searches as an array. Search before create or update when duplicate risk exists.",
853
- createRule: "Each create operation must include entityType and full data. entityType alone is not enough.",
854
- 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.",
855
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"}]}',
856
- 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"}]}'
857
2323
  }
858
2324
  };
859
2325
  }
@@ -917,7 +2383,9 @@ function parseActivityContext(headers) {
917
2383
  if (Array.isArray(rawSource)) {
918
2384
  throw new Error("X-Forge-Source must be a single header value");
919
2385
  }
920
- 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);
921
2389
  return {
922
2390
  source,
923
2391
  actor: parseOptionalActorHeader(headers)
@@ -960,7 +2428,8 @@ function hasTokenScope(token, scope) {
960
2428
  return Boolean(token?.scopes.includes(scope));
961
2429
  }
962
2430
  function isPsycheEntityType(entityType) {
963
- return Boolean(entityType && PSYCHE_ENTITY_TYPES.includes(entityType));
2431
+ return Boolean(entityType &&
2432
+ PSYCHE_ENTITY_TYPES.includes(entityType));
964
2433
  }
965
2434
  function getWatchdogHealth(taskRunWatchdog) {
966
2435
  if (!taskRunWatchdog) {
@@ -1053,6 +2522,48 @@ function buildXpMetricsPayload() {
1053
2522
  dailyAmbientCap
1054
2523
  };
1055
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
+ }
1056
2567
  function buildOperatorContext() {
1057
2568
  const tasks = listTasks();
1058
2569
  const dueHabits = listHabits({ dueToday: true }).slice(0, 12);
@@ -1070,7 +2581,9 @@ function buildOperatorContext() {
1070
2581
  currentBoard: {
1071
2582
  backlog: tasks.filter((task) => task.status === "backlog").slice(0, 20),
1072
2583
  focus: tasks.filter((task) => task.status === "focus").slice(0, 20),
1073
- 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),
1074
2587
  blocked: tasks.filter((task) => task.status === "blocked").slice(0, 20),
1075
2588
  done: tasks.filter((task) => task.status === "done").slice(0, 20)
1076
2589
  },
@@ -1144,6 +2657,12 @@ function buildOperatorOverviewRouteGuide() {
1144
2657
  summary: "Preferred multi-entity mutation surface for agents. Delete defaults to soft delete and restore reverses soft deletion.",
1145
2658
  requiredScope: "write"
1146
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
+ },
1147
2666
  {
1148
2667
  id: "operator_log_work",
1149
2668
  path: "/api/v1/operator/log-work",
@@ -1161,8 +2680,14 @@ function buildOperatorOverviewRouteGuide() {
1161
2680
  }
1162
2681
  function buildOperatorOverview(request) {
1163
2682
  const auth = parseRequestAuth(request.headers);
1164
- const canReadPsyche = auth.token ? hasTokenScope(auth.token, "psyche.read") : true;
1165
- 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
+ ];
1166
2691
  return {
1167
2692
  generatedAt: new Date().toISOString(),
1168
2693
  snapshot: buildV1Context(),
@@ -1174,9 +2699,15 @@ function buildOperatorOverview(request) {
1174
2699
  tokenPresent: Boolean(auth.token),
1175
2700
  scopes: auth.token?.scopes ?? [],
1176
2701
  canReadPsyche,
1177
- canWritePsyche: auth.token ? hasTokenScope(auth.token, "psyche.write") : true,
1178
- canManageModes: auth.token ? hasTokenScope(auth.token, "psyche.mode") : true,
1179
- 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
1180
2711
  },
1181
2712
  warnings,
1182
2713
  routeGuide: buildOperatorOverviewRouteGuide()
@@ -1184,7 +2715,21 @@ function buildOperatorOverview(request) {
1184
2715
  }
1185
2716
  export async function buildServer(options = {}) {
1186
2717
  const managers = createManagerRuntime({ dataRoot: options.dataRoot });
1187
- 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
+ });
1188
2733
  configureDatabase({ dataRoot: runtimeConfig.dataRoot ?? undefined });
1189
2734
  configureDatabaseSeeding(options.seedDemoData ?? false);
1190
2735
  await managers.migration.initialize();
@@ -1192,7 +2737,9 @@ export async function buildServer(options = {}) {
1192
2737
  logger: false,
1193
2738
  rewriteUrl: (request) => rewriteMountPath(request.url ?? "/")
1194
2739
  });
1195
- const taskRunWatchdog = options.taskRunWatchdog === false ? null : createTaskRunWatchdog(options.taskRunWatchdog);
2740
+ const taskRunWatchdog = options.taskRunWatchdog === false
2741
+ ? null
2742
+ : createTaskRunWatchdog(options.taskRunWatchdog);
1196
2743
  await app.register(cors, {
1197
2744
  origin: (origin, callback) => {
1198
2745
  if (!origin) {
@@ -1223,7 +2770,9 @@ export async function buildServer(options = {}) {
1223
2770
  : statusCode === 400
1224
2771
  ? "invalid_request"
1225
2772
  : "internal_error",
1226
- error: validationIssues ? "Request validation failed" : getErrorMessage(error),
2773
+ error: validationIssues
2774
+ ? "Request validation failed"
2775
+ : getErrorMessage(error),
1227
2776
  statusCode,
1228
2777
  ...(validationIssues ? { details: validationIssues } : {}),
1229
2778
  ...(isHttpError(error) && error.details ? error.details : {}),
@@ -1235,6 +2784,112 @@ export async function buildServer(options = {}) {
1235
2784
  actor: context.actor,
1236
2785
  source: context.source
1237
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
+ };
1238
2893
  const requireOperatorSession = (headers, detail) => {
1239
2894
  const context = authenticateRequest(headers);
1240
2895
  managers.authorization.requireAuthenticatedOperator(context, detail);
@@ -1312,13 +2967,17 @@ export async function buildServer(options = {}) {
1312
2967
  app.get("/api/v1/openapi.json", async () => buildOpenApiDocument());
1313
2968
  app.get("/api/v1/context", async () => buildV1Context());
1314
2969
  app.get("/api/v1/operator/context", async (request) => {
1315
- requireOperatorSession(request.headers, { route: "/api/v1/operator/context" });
2970
+ requireOperatorSession(request.headers, {
2971
+ route: "/api/v1/operator/context"
2972
+ });
1316
2973
  return {
1317
2974
  context: buildOperatorContext()
1318
2975
  };
1319
2976
  });
1320
2977
  app.get("/api/v1/operator/overview", async (request) => {
1321
- requireOperatorSession(request.headers, { route: "/api/v1/operator/overview" });
2978
+ requireOperatorSession(request.headers, {
2979
+ route: "/api/v1/operator/overview"
2980
+ });
1322
2981
  return {
1323
2982
  overview: buildOperatorOverview(request)
1324
2983
  };
@@ -1674,8 +3333,16 @@ export async function buildServer(options = {}) {
1674
3333
  }
1675
3334
  return {
1676
3335
  report,
1677
- notes: listNotes({ linkedEntityType: "trigger_report", linkedEntityId: id, limit: 50 }),
1678
- 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
+ })
1679
3346
  };
1680
3347
  });
1681
3348
  app.patch("/api/v1/psyche/reports/:id", async (request, reply) => {
@@ -1754,7 +3421,10 @@ export async function buildServer(options = {}) {
1754
3421
  app.delete("/api/v1/notes/:id", async (request, reply) => {
1755
3422
  const { id } = request.params;
1756
3423
  const current = getNoteById(id);
1757
- 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;
1758
3428
  const auth = requireNoteAccess(request.headers, linkedEntityType, {
1759
3429
  route: "/api/v1/notes/:id",
1760
3430
  entityType: linkedEntityType
@@ -1789,6 +3459,147 @@ export async function buildServer(options = {}) {
1789
3459
  const query = taskListQuerySchema.parse(request.query ?? {});
1790
3460
  return { tasks: listTasks(query) };
1791
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
+ });
1792
3603
  app.get("/api/v1/habits", async (request) => {
1793
3604
  const query = habitListQuerySchema.parse(request.query ?? {});
1794
3605
  return { habits: listHabits(query) };
@@ -1835,7 +3646,9 @@ export async function buildServer(options = {}) {
1835
3646
  return { activity: listActivityEvents(query) };
1836
3647
  });
1837
3648
  app.post("/api/v1/activity/:id/remove", async (request, reply) => {
1838
- requireScopedAccess(request.headers, ["write"], { route: "/api/v1/activity/:id/remove" });
3649
+ requireScopedAccess(request.headers, ["write"], {
3650
+ route: "/api/v1/activity/:id/remove"
3651
+ });
1839
3652
  const { id } = request.params;
1840
3653
  const event = removeActivityEvent(id, removeActivityEventSchema.parse(request.body ?? {}), parseActivityContext(request.headers));
1841
3654
  if (!event) {
@@ -1859,7 +3672,10 @@ export async function buildServer(options = {}) {
1859
3672
  route: "/api/v1/insights",
1860
3673
  entityType: input.entityType
1861
3674
  });
1862
- const insight = createInsight(input, { actor: auth.actor, source: auth.source });
3675
+ const insight = createInsight(input, {
3676
+ actor: auth.actor,
3677
+ source: auth.source
3678
+ });
1863
3679
  reply.code(201);
1864
3680
  return { insight };
1865
3681
  });
@@ -1896,7 +3712,10 @@ export async function buildServer(options = {}) {
1896
3712
  const query = entityDeleteQuerySchema.parse(request.query ?? {});
1897
3713
  const insight = query.mode === "hard"
1898
3714
  ? deleteInsight(id, { actor: auth.actor, source: auth.source })
1899
- : deleteEntity("insight", id, query, { actor: auth.actor, source: auth.source });
3715
+ : deleteEntity("insight", id, query, {
3716
+ actor: auth.actor,
3717
+ source: auth.source
3718
+ });
1900
3719
  if (!insight) {
1901
3720
  reply.code(404);
1902
3721
  return { error: "Insight not found" };
@@ -1918,7 +3737,9 @@ export async function buildServer(options = {}) {
1918
3737
  return { feedback };
1919
3738
  });
1920
3739
  app.get("/api/v1/approval-requests", async (request) => {
1921
- requireOperatorSession(request.headers, { route: "/api/v1/approval-requests" });
3740
+ requireOperatorSession(request.headers, {
3741
+ route: "/api/v1/approval-requests"
3742
+ });
1922
3743
  const query = request.query;
1923
3744
  return { approvalRequests: listApprovalRequests(query?.status) };
1924
3745
  });
@@ -1926,7 +3747,9 @@ export async function buildServer(options = {}) {
1926
3747
  const context = requireOperatorSession(request.headers, { route: "/api/v1/approval-requests/:id/approve" });
1927
3748
  const { id } = request.params;
1928
3749
  const body = resolveApprovalRequestSchema.parse(request.body ?? {});
1929
- 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));
1930
3753
  if (!approvalRequest) {
1931
3754
  reply.code(404);
1932
3755
  return { error: "Approval request not found" };
@@ -1937,7 +3760,9 @@ export async function buildServer(options = {}) {
1937
3760
  const context = requireOperatorSession(request.headers, { route: "/api/v1/approval-requests/:id/reject" });
1938
3761
  const { id } = request.params;
1939
3762
  const body = resolveApprovalRequestSchema.parse(request.body ?? {});
1940
- 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));
1941
3766
  if (!approvalRequest) {
1942
3767
  reply.code(404);
1943
3768
  return { error: "Approval request not found" };
@@ -1958,16 +3783,24 @@ export async function buildServer(options = {}) {
1958
3783
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/agent-actions" });
1959
3784
  const input = createAgentActionSchema.parse(request.body ?? {});
1960
3785
  const idempotencyKey = parseIdempotencyKey(request.headers);
1961
- 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);
1962
3791
  reply.code(result.approvalRequest ? 202 : 201);
1963
3792
  return result;
1964
3793
  });
1965
3794
  app.get("/api/v1/rewards/rules", async (request) => {
1966
- requireOperatorSession(request.headers, { route: "/api/v1/rewards/rules" });
3795
+ requireOperatorSession(request.headers, {
3796
+ route: "/api/v1/rewards/rules"
3797
+ });
1967
3798
  return { rules: listRewardRules() };
1968
3799
  });
1969
3800
  app.get("/api/v1/rewards/rules/:id", async (request, reply) => {
1970
- requireOperatorSession(request.headers, { route: "/api/v1/rewards/rules/:id" });
3801
+ requireOperatorSession(request.headers, {
3802
+ route: "/api/v1/rewards/rules/:id"
3803
+ });
1971
3804
  const { id } = request.params;
1972
3805
  const rule = getRewardRuleById(id);
1973
3806
  if (!rule) {
@@ -1987,7 +3820,9 @@ export async function buildServer(options = {}) {
1987
3820
  return { rule };
1988
3821
  });
1989
3822
  app.get("/api/v1/rewards/ledger", async (request) => {
1990
- requireOperatorSession(request.headers, { route: "/api/v1/rewards/ledger" });
3823
+ requireOperatorSession(request.headers, {
3824
+ route: "/api/v1/rewards/ledger"
3825
+ });
1991
3826
  const query = rewardsLedgerQuerySchema.parse(request.query ?? {});
1992
3827
  return { ledger: listRewardLedger(query) };
1993
3828
  });
@@ -2000,7 +3835,10 @@ export async function buildServer(options = {}) {
2000
3835
  app.post("/api/v1/session-events", async (request, reply) => {
2001
3836
  const auth = requireAuthenticatedActor(request.headers, { route: "/api/v1/session-events" });
2002
3837
  const payload = createSessionEventSchema.parse(request.body ?? {});
2003
- const event = recordSessionEvent(payload, { actor: auth.actor, source: auth.source });
3838
+ const event = recordSessionEvent(payload, {
3839
+ actor: auth.actor,
3840
+ source: auth.source
3841
+ });
2004
3842
  reply.code(201);
2005
3843
  return event;
2006
3844
  });
@@ -2011,6 +3849,27 @@ export async function buildServer(options = {}) {
2011
3849
  app.get("/api/v1/reviews/weekly", async () => ({
2012
3850
  review: getWeeklyReviewPayload()
2013
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
+ });
2014
3873
  app.get("/api/v1/settings", async (request) => {
2015
3874
  requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/settings" });
2016
3875
  return { settings: getSettings() };
@@ -2025,6 +3884,295 @@ export async function buildServer(options = {}) {
2025
3884
  reply.code(201);
2026
3885
  return { project };
2027
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
+ });
2028
4176
  app.post("/api/v1/habits", async (request, reply) => {
2029
4177
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits" });
2030
4178
  const habit = createHabit(createHabitSchema.parse(request.body ?? {}), toActivityContext(auth));
@@ -2263,8 +4411,10 @@ export async function buildServer(options = {}) {
2263
4411
  const taskRuns = listTaskRuns({ taskId: id, limit: 10 });
2264
4412
  return taskContextPayloadSchema.parse({
2265
4413
  task,
2266
- goal: task.goalId ? getGoalById(task.goalId) ?? null : null,
2267
- 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,
2268
4418
  activeTaskRun: taskRuns.find((run) => run.status === "active") ?? null,
2269
4419
  taskRuns,
2270
4420
  activity: listActivityEventsForTask(id, 20),
@@ -2281,8 +4431,10 @@ export async function buildServer(options = {}) {
2281
4431
  const taskRuns = listTaskRuns({ taskId: id, limit: 10 });
2282
4432
  return taskContextPayloadSchema.parse({
2283
4433
  task,
2284
- goal: task.goalId ? getGoalById(task.goalId) ?? null : null,
2285
- 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,
2286
4438
  activeTaskRun: taskRuns.find((run) => run.status === "active") ?? null,
2287
4439
  taskRuns,
2288
4440
  activity: listActivityEventsForTask(id, 20),
@@ -2458,7 +4610,9 @@ export async function buildServer(options = {}) {
2458
4610
  const input = operatorLogWorkSchema.parse(request.body ?? {});
2459
4611
  if (input.taskId) {
2460
4612
  const task = updateTask(input.taskId, {
2461
- 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,
2462
4616
  description: typeof input.description === "string"
2463
4617
  ? input.description
2464
4618
  : input.summary.trim().length > 0
@@ -2504,6 +4658,72 @@ export async function buildServer(options = {}) {
2504
4658
  reply.code(201);
2505
4659
  return { task, xp: buildXpMetricsPayload() };
2506
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
+ });
2507
4727
  app.post("/api/v1/tasks/:id/uncomplete", async (request, reply) => {
2508
4728
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/tasks/:id/uncomplete" });
2509
4729
  const { id } = request.params;
@@ -2517,18 +4737,26 @@ export async function buildServer(options = {}) {
2517
4737
  });
2518
4738
  app.post("/api/v1/entities/create", async (request) => {
2519
4739
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/entities/create" });
2520
- 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;
2521
4743
  });
2522
4744
  app.post("/api/v1/entities/update", async (request) => {
2523
4745
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/entities/update" });
2524
- 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;
2525
4749
  });
2526
4750
  app.post("/api/v1/entities/delete", async (request) => {
2527
4751
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/entities/delete" });
2528
- 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;
2529
4755
  });
2530
4756
  app.post("/api/v1/entities/restore", async (request) => {
2531
- requireScopedAccess(request.headers, ["write"], { route: "/api/v1/entities/restore" });
4757
+ requireScopedAccess(request.headers, ["write"], {
4758
+ route: "/api/v1/entities/restore"
4759
+ });
2532
4760
  return restoreEntities(batchRestoreEntitiesSchema.parse(request.body ?? {}));
2533
4761
  });
2534
4762
  app.post("/api/v1/entities/search", async (request) => {
@@ -2537,7 +4765,9 @@ export async function buildServer(options = {}) {
2537
4765
  });
2538
4766
  app.post("/api/task-runs/recover", async (request, reply) => {
2539
4767
  markCompatibilityRoute(reply);
2540
- const payload = taskRunListQuerySchema.pick({ limit: true }).parse(request.body ?? {});
4768
+ const payload = taskRunListQuerySchema
4769
+ .pick({ limit: true })
4770
+ .parse(request.body ?? {});
2541
4771
  return { timedOutRuns: recoverTimedOutTaskRuns({ limit: payload.limit }) };
2542
4772
  });
2543
4773
  app.post("/api/task-runs/:id/heartbeat", async (request, reply) => {
@@ -2545,52 +4775,68 @@ export async function buildServer(options = {}) {
2545
4775
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/heartbeat" });
2546
4776
  const { id } = request.params;
2547
4777
  const input = taskRunHeartbeatSchema.parse(request.body ?? {});
2548
- return { taskRun: heartbeatTaskRun(id, input, new Date(), toActivityContext(auth)) };
4778
+ return {
4779
+ taskRun: heartbeatTaskRun(id, input, new Date(), toActivityContext(auth))
4780
+ };
2549
4781
  });
2550
4782
  app.post("/api/v1/task-runs/:id/heartbeat", async (request) => {
2551
4783
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/heartbeat" });
2552
4784
  const { id } = request.params;
2553
4785
  const input = taskRunHeartbeatSchema.parse(request.body ?? {});
2554
- return { taskRun: heartbeatTaskRun(id, input, new Date(), toActivityContext(auth)) };
4786
+ return {
4787
+ taskRun: heartbeatTaskRun(id, input, new Date(), toActivityContext(auth))
4788
+ };
2555
4789
  });
2556
4790
  app.post("/api/task-runs/:id/focus", async (request, reply) => {
2557
4791
  markCompatibilityRoute(reply);
2558
4792
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/focus" });
2559
4793
  const { id } = request.params;
2560
4794
  const input = taskRunFocusSchema.parse(request.body ?? {});
2561
- return { taskRun: focusTaskRun(id, input, new Date(), toActivityContext(auth)) };
4795
+ return {
4796
+ taskRun: focusTaskRun(id, input, new Date(), toActivityContext(auth))
4797
+ };
2562
4798
  });
2563
4799
  app.post("/api/v1/task-runs/:id/focus", async (request) => {
2564
4800
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/focus" });
2565
4801
  const { id } = request.params;
2566
4802
  const input = taskRunFocusSchema.parse(request.body ?? {});
2567
- return { taskRun: focusTaskRun(id, input, new Date(), toActivityContext(auth)) };
4803
+ return {
4804
+ taskRun: focusTaskRun(id, input, new Date(), toActivityContext(auth))
4805
+ };
2568
4806
  });
2569
4807
  app.post("/api/task-runs/:id/complete", async (request, reply) => {
2570
4808
  markCompatibilityRoute(reply);
2571
4809
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/complete" });
2572
4810
  const { id } = request.params;
2573
4811
  const input = taskRunFinishSchema.parse(request.body ?? {});
2574
- return { taskRun: completeTaskRun(id, input, new Date(), toActivityContext(auth)) };
4812
+ return {
4813
+ taskRun: completeTaskRun(id, input, new Date(), toActivityContext(auth))
4814
+ };
2575
4815
  });
2576
4816
  app.post("/api/v1/task-runs/:id/complete", async (request) => {
2577
4817
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/complete" });
2578
4818
  const { id } = request.params;
2579
4819
  const input = taskRunFinishSchema.parse(request.body ?? {});
2580
- return { taskRun: completeTaskRun(id, input, new Date(), toActivityContext(auth)) };
4820
+ return {
4821
+ taskRun: completeTaskRun(id, input, new Date(), toActivityContext(auth))
4822
+ };
2581
4823
  });
2582
4824
  app.post("/api/task-runs/:id/release", async (request, reply) => {
2583
4825
  markCompatibilityRoute(reply);
2584
4826
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/release" });
2585
4827
  const { id } = request.params;
2586
4828
  const input = taskRunFinishSchema.parse(request.body ?? {});
2587
- return { taskRun: releaseTaskRun(id, input, new Date(), toActivityContext(auth)) };
4829
+ return {
4830
+ taskRun: releaseTaskRun(id, input, new Date(), toActivityContext(auth))
4831
+ };
2588
4832
  });
2589
4833
  app.post("/api/v1/task-runs/:id/release", async (request) => {
2590
4834
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/release" });
2591
4835
  const { id } = request.params;
2592
4836
  const input = taskRunFinishSchema.parse(request.body ?? {});
2593
- return { taskRun: releaseTaskRun(id, input, new Date(), toActivityContext(auth)) };
4837
+ return {
4838
+ taskRun: releaseTaskRun(id, input, new Date(), toActivityContext(auth))
4839
+ };
2594
4840
  });
2595
4841
  app.post("/api/tags/suggestions", async (request, reply) => {
2596
4842
  markCompatibilityRoute(reply);