forge-openclaw-plugin 0.2.25 → 0.2.27

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 (237) hide show
  1. package/README.md +59 -3
  2. package/dist/assets/{board-VmF4FAfr.js → board-C6jCchjI.js} +3 -3
  3. package/dist/assets/{board-VmF4FAfr.js.map → board-C6jCchjI.js.map} +1 -1
  4. package/dist/assets/index-DVvS8iiU.css +1 -0
  5. package/dist/assets/index-zYB-9Dfo.js +85 -0
  6. package/dist/assets/index-zYB-9Dfo.js.map +1 -0
  7. package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js +2 -0
  8. package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js.map +1 -0
  9. package/dist/assets/{motion-DvkU14p-.js → motion-DFHrH2rd.js} +2 -2
  10. package/dist/assets/{motion-DvkU14p-.js.map → motion-DFHrH2rd.js.map} +1 -1
  11. package/dist/assets/{table-DgiPof9E.js → table-ZL7Di_u3.js} +2 -2
  12. package/dist/assets/{table-DgiPof9E.js.map → table-ZL7Di_u3.js.map} +1 -1
  13. package/dist/assets/{ui-nYfoC0Gq.js → ui-CKNPpz7q.js} +2 -2
  14. package/dist/assets/{ui-nYfoC0Gq.js.map → ui-CKNPpz7q.js.map} +1 -1
  15. package/dist/assets/vendor-DoNZuFhn.js +1247 -0
  16. package/dist/assets/vendor-DoNZuFhn.js.map +1 -0
  17. package/dist/index.html +7 -8
  18. package/dist/openclaw/local-runtime.d.ts +3 -1
  19. package/dist/openclaw/local-runtime.js +67 -15
  20. package/dist/openclaw/plugin-entry-shared.js +24 -2
  21. package/dist/openclaw/plugin-sdk-types.d.ts +17 -0
  22. package/dist/openclaw/routes.d.ts +27 -0
  23. package/dist/openclaw/routes.js +16 -12
  24. package/dist/openclaw/tools.js +0 -3
  25. package/dist/server/server/migrations/001_core.sql +411 -0
  26. package/dist/server/server/migrations/002_psyche.sql +392 -0
  27. package/dist/server/server/migrations/003_habits.sql +30 -0
  28. package/dist/server/server/migrations/004_habit_links.sql +8 -0
  29. package/dist/server/server/migrations/005_habit_psyche_links.sql +24 -0
  30. package/dist/server/server/migrations/006_work_adjustments.sql +14 -0
  31. package/dist/server/server/migrations/007_weekly_review_closures.sql +17 -0
  32. package/dist/server/server/migrations/008_calendar_execution.sql +147 -0
  33. package/dist/server/server/migrations/009_true_calendar_events.sql +195 -0
  34. package/dist/server/server/migrations/010_calendar_selection_state.sql +6 -0
  35. package/dist/server/server/migrations/011_calendar_timezone_backfill.sql +11 -0
  36. package/dist/server/server/migrations/012_work_block_ranges.sql +7 -0
  37. package/dist/server/server/migrations/013_microsoft_local_auth_settings.sql +8 -0
  38. package/dist/server/server/migrations/014_note_tags_and_ephemeral.sql +8 -0
  39. package/dist/server/server/migrations/015_multi_user_and_strategies.sql +244 -0
  40. package/dist/server/server/migrations/016_health_companion.sql +158 -0
  41. package/dist/server/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
  42. package/dist/server/server/migrations/017_preferences.sql +131 -0
  43. package/dist/server/server/migrations/018_preference_catalogs.sql +31 -0
  44. package/dist/server/server/migrations/019_wiki_memory.sql +255 -0
  45. package/dist/server/server/migrations/020_wiki_page_hierarchy.sql +11 -0
  46. package/dist/server/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
  47. package/dist/server/server/migrations/022_wiki_ingest_background.sql +85 -0
  48. package/dist/server/server/migrations/023_diagnostic_logs.sql +28 -0
  49. package/dist/server/server/migrations/024_questionnaires.sql +96 -0
  50. package/dist/server/server/migrations/025_ai_model_connections.sql +26 -0
  51. package/dist/server/server/migrations/026_custom_theme_settings.sql +2 -0
  52. package/dist/server/server/migrations/027_ai_processors.sql +31 -0
  53. package/dist/server/server/migrations/028_movement_domain.sql +136 -0
  54. package/dist/server/server/migrations/029_watch_micro_capture.sql +23 -0
  55. package/dist/server/server/migrations/030_surface_layouts.sql +5 -0
  56. package/dist/server/server/migrations/031_ai_processor_runtime_upgrades.sql +10 -0
  57. package/dist/server/server/migrations/032_ai_connectors.sql +44 -0
  58. package/dist/server/server/migrations/033_movement_trip_point_sync.sql +36 -0
  59. package/dist/server/server/migrations/034_movement_segment_sync.sql +49 -0
  60. package/dist/server/server/migrations/035_google_local_auth_settings.sql +2 -0
  61. package/dist/server/server/migrations/036_google_local_auth_client_secret.sql +2 -0
  62. package/dist/server/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
  63. package/dist/server/server/migrations/038_data_management_settings.sql +11 -0
  64. package/dist/server/server/migrations/039_life_force_and_action_points.sql +114 -0
  65. package/dist/server/server/migrations/040_screen_time_domain.sql +89 -0
  66. package/dist/server/server/migrations/041_companion_source_states.sql +21 -0
  67. package/dist/server/server/migrations/042_movement_boxes.sql +47 -0
  68. package/dist/server/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
  69. package/dist/server/{app.js → server/src/app.js} +2112 -414
  70. package/dist/server/server/src/connectors/box-registry.js +223 -0
  71. package/dist/server/server/src/data-management-types.js +107 -0
  72. package/dist/server/{db.js → server/src/db.js} +72 -4
  73. package/dist/server/server/src/debug.js +19 -0
  74. package/dist/server/{demo-data.js → server/src/demo-data.js} +2 -2
  75. package/dist/server/{health.js → server/src/health.js} +702 -18
  76. package/dist/server/{managers → server/src/managers}/platform/llm-manager.js +7 -4
  77. package/dist/server/server/src/managers/platform/mock-workbench-provider.js +149 -0
  78. package/dist/server/{managers → server/src/managers}/platform/secrets-manager.js +18 -1
  79. package/dist/server/{managers → server/src/managers}/runtime.js +9 -0
  80. package/dist/server/{movement.js → server/src/movement.js} +1971 -112
  81. package/dist/server/{openapi.js → server/src/openapi.js} +491 -3
  82. package/dist/server/{psyche-types.js → server/src/psyche-types.js} +9 -1
  83. package/dist/server/{repositories → server/src/repositories}/activity-events.js +8 -0
  84. package/dist/server/{repositories → server/src/repositories}/ai-connectors.js +758 -47
  85. package/dist/server/{repositories → server/src/repositories}/calendar.js +1 -1
  86. package/dist/server/{repositories → server/src/repositories}/habits.js +37 -1
  87. package/dist/server/{repositories → server/src/repositories}/model-settings.js +13 -3
  88. package/dist/server/{repositories → server/src/repositories}/notes.js +3 -0
  89. package/dist/server/{repositories → server/src/repositories}/settings.js +431 -21
  90. package/dist/server/{repositories → server/src/repositories}/tasks.js +170 -10
  91. package/dist/server/server/src/runtime-data-root.js +82 -0
  92. package/dist/server/server/src/screen-time.js +802 -0
  93. package/dist/server/{services → server/src/services}/calendar-runtime.js +775 -58
  94. package/dist/server/server/src/services/data-management.js +788 -0
  95. package/dist/server/{services → server/src/services}/entity-crud.js +205 -2
  96. package/dist/server/server/src/services/google-calendar-oauth-config.js +176 -0
  97. package/dist/server/server/src/services/knowledge-graph.js +1455 -0
  98. package/dist/server/server/src/services/life-force-model.js +197 -0
  99. package/dist/server/server/src/services/life-force.js +1270 -0
  100. package/dist/server/server/src/services/psyche-observation-calendar.js +413 -0
  101. package/dist/server/{types.js → server/src/types.js} +420 -29
  102. package/dist/server/server/src/web.js +332 -0
  103. package/dist/server/src/components/customization/utility-widgets.js +439 -0
  104. package/dist/server/src/components/ui/info-tooltip.js +25 -0
  105. package/dist/server/src/components/workbench-boxes/calendar/calendar-boxes.js +78 -0
  106. package/dist/server/src/components/workbench-boxes/goals/goals-boxes.js +62 -0
  107. package/dist/server/src/components/workbench-boxes/habits/habits-boxes.js +62 -0
  108. package/dist/server/src/components/workbench-boxes/health/health-boxes.js +147 -0
  109. package/dist/server/src/components/workbench-boxes/insights/insights-boxes.js +50 -0
  110. package/dist/server/src/components/workbench-boxes/kanban/kanban-boxes.js +136 -0
  111. package/dist/server/src/components/workbench-boxes/movement/movement-boxes.js +47 -0
  112. package/dist/server/src/components/workbench-boxes/notes/notes-boxes.js +132 -0
  113. package/dist/server/src/components/workbench-boxes/overview/overview-boxes.js +65 -0
  114. package/dist/server/src/components/workbench-boxes/preferences/preferences-boxes.js +78 -0
  115. package/dist/server/src/components/workbench-boxes/projects/projects-boxes.js +62 -0
  116. package/dist/server/src/components/workbench-boxes/psyche/psyche-boxes.js +88 -0
  117. package/dist/server/src/components/workbench-boxes/questionnaires/questionnaires-boxes.js +61 -0
  118. package/dist/server/src/components/workbench-boxes/review/review-boxes.js +53 -0
  119. package/dist/server/src/components/workbench-boxes/shared/define-workbench-box.js +6 -0
  120. package/dist/server/src/components/workbench-boxes/shared/generic-node-view.js +49 -0
  121. package/dist/server/src/components/workbench-boxes/strategies/strategies-boxes.js +62 -0
  122. package/dist/server/src/components/workbench-boxes/tasks/tasks-boxes.js +76 -0
  123. package/dist/server/src/components/workbench-boxes/today/today-boxes.js +78 -0
  124. package/dist/server/src/components/workbench-boxes/wiki/wiki-boxes.js +60 -0
  125. package/dist/server/src/lib/api-error.js +37 -0
  126. package/dist/server/src/lib/api.js +2118 -0
  127. package/dist/server/src/lib/calendar-name-deduper.js +144 -0
  128. package/dist/server/src/lib/data-management-types.js +1 -0
  129. package/dist/server/src/lib/diagnostics.js +67 -0
  130. package/dist/server/src/lib/entity-visuals.js +279 -0
  131. package/dist/server/src/lib/knowledge-graph-types.js +276 -0
  132. package/dist/server/src/lib/knowledge-graph.js +470 -0
  133. package/dist/server/src/lib/psyche-types.js +1 -0
  134. package/dist/server/src/lib/questionnaire-types.js +1 -0
  135. package/dist/server/src/lib/runtime-paths.js +24 -0
  136. package/dist/server/src/lib/schemas.js +238 -0
  137. package/dist/server/src/lib/snapshot-normalizer.js +416 -0
  138. package/dist/server/src/lib/theme-system.js +319 -0
  139. package/dist/server/src/lib/types.js +1 -0
  140. package/dist/server/src/lib/utils.js +22 -0
  141. package/dist/server/src/lib/workbench/boxes.js +16 -0
  142. package/dist/server/src/lib/workbench/contracts.js +229 -0
  143. package/dist/server/src/lib/workbench/nodes.js +215 -0
  144. package/dist/server/src/lib/workbench/registry.js +120 -0
  145. package/dist/server/src/lib/workbench/runtime.js +397 -0
  146. package/dist/server/src/lib/workbench/tool-catalog.js +68 -0
  147. package/openclaw.plugin.json +1 -1
  148. package/package.json +1 -1
  149. package/server/index.js +68 -0
  150. package/server/migrations/035_google_local_auth_settings.sql +2 -0
  151. package/server/migrations/036_google_local_auth_client_secret.sql +2 -0
  152. package/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
  153. package/server/migrations/038_data_management_settings.sql +11 -0
  154. package/server/migrations/039_life_force_and_action_points.sql +114 -0
  155. package/server/migrations/040_screen_time_domain.sql +89 -0
  156. package/server/migrations/041_companion_source_states.sql +21 -0
  157. package/server/migrations/042_movement_boxes.sql +47 -0
  158. package/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
  159. package/skills/forge-openclaw/SKILL.md +27 -11
  160. package/skills/forge-openclaw/entity_conversation_playbooks.md +411 -46
  161. package/skills/forge-openclaw/psyche_entity_playbooks.md +195 -20
  162. package/dist/assets/index-CFCKDIMH.js +0 -67
  163. package/dist/assets/index-CFCKDIMH.js.map +0 -1
  164. package/dist/assets/index-ZPY6U1TU.css +0 -1
  165. package/dist/assets/vendor-D9PTEPSB.js +0 -824
  166. package/dist/assets/vendor-D9PTEPSB.js.map +0 -1
  167. package/dist/assets/viz-Cqb6s--o.js +0 -34
  168. package/dist/assets/viz-Cqb6s--o.js.map +0 -1
  169. package/dist/server/connectors/box-registry.js +0 -257
  170. package/dist/server/services/psyche-observation-calendar.js +0 -46
  171. package/dist/server/web.js +0 -98
  172. /package/dist/server/{discovery-advertiser.js → server/src/discovery-advertiser.js} +0 -0
  173. /package/dist/server/{e2e-server.js → server/src/e2e-server.js} +0 -0
  174. /package/dist/server/{errors.js → server/src/errors.js} +0 -0
  175. /package/dist/server/{index.js → server/src/index.js} +0 -0
  176. /package/dist/server/{managers → server/src/managers}/base.js +0 -0
  177. /package/dist/server/{managers → server/src/managers}/contracts.js +0 -0
  178. /package/dist/server/{managers → server/src/managers}/platform/api-gateway-manager.js +0 -0
  179. /package/dist/server/{managers → server/src/managers}/platform/audit-manager.js +0 -0
  180. /package/dist/server/{managers → server/src/managers}/platform/authentication-manager.js +0 -0
  181. /package/dist/server/{managers → server/src/managers}/platform/authorization-manager.js +0 -0
  182. /package/dist/server/{managers → server/src/managers}/platform/background-job-manager.js +0 -0
  183. /package/dist/server/{managers → server/src/managers}/platform/configuration-manager.js +0 -0
  184. /package/dist/server/{managers → server/src/managers}/platform/database-manager.js +0 -0
  185. /package/dist/server/{managers → server/src/managers}/platform/event-bus-manager.js +0 -0
  186. /package/dist/server/{managers → server/src/managers}/platform/external-service-manager.js +0 -0
  187. /package/dist/server/{managers → server/src/managers}/platform/health-manager.js +0 -0
  188. /package/dist/server/{managers → server/src/managers}/platform/migration-manager.js +0 -0
  189. /package/dist/server/{managers → server/src/managers}/platform/openai-responses-provider.js +0 -0
  190. /package/dist/server/{managers → server/src/managers}/platform/search-index-manager.js +0 -0
  191. /package/dist/server/{managers → server/src/managers}/platform/session-manager.js +0 -0
  192. /package/dist/server/{managers → server/src/managers}/platform/storage-manager.js +0 -0
  193. /package/dist/server/{managers → server/src/managers}/platform/token-manager.js +0 -0
  194. /package/dist/server/{managers → server/src/managers}/platform/transaction-manager.js +0 -0
  195. /package/dist/server/{managers → server/src/managers}/platform/trusted-network.js +0 -0
  196. /package/dist/server/{managers → server/src/managers}/type-guards.js +0 -0
  197. /package/dist/server/{preferences-seeds.js → server/src/preferences-seeds.js} +0 -0
  198. /package/dist/server/{preferences-types.js → server/src/preferences-types.js} +0 -0
  199. /package/dist/server/{questionnaire-flow.js → server/src/questionnaire-flow.js} +0 -0
  200. /package/dist/server/{questionnaire-seeds.js → server/src/questionnaire-seeds.js} +0 -0
  201. /package/dist/server/{questionnaire-types.js → server/src/questionnaire-types.js} +0 -0
  202. /package/dist/server/{repositories → server/src/repositories}/ai-processors.js +0 -0
  203. /package/dist/server/{repositories → server/src/repositories}/collaboration.js +0 -0
  204. /package/dist/server/{repositories → server/src/repositories}/deleted-entities.js +0 -0
  205. /package/dist/server/{repositories → server/src/repositories}/diagnostic-logs.js +0 -0
  206. /package/dist/server/{repositories → server/src/repositories}/domains.js +0 -0
  207. /package/dist/server/{repositories → server/src/repositories}/entity-ownership.js +0 -0
  208. /package/dist/server/{repositories → server/src/repositories}/event-log.js +0 -0
  209. /package/dist/server/{repositories → server/src/repositories}/goals.js +0 -0
  210. /package/dist/server/{repositories → server/src/repositories}/preferences.js +0 -0
  211. /package/dist/server/{repositories → server/src/repositories}/projects.js +0 -0
  212. /package/dist/server/{repositories → server/src/repositories}/psyche.js +0 -0
  213. /package/dist/server/{repositories → server/src/repositories}/questionnaires.js +0 -0
  214. /package/dist/server/{repositories → server/src/repositories}/rewards.js +0 -0
  215. /package/dist/server/{repositories → server/src/repositories}/strategies.js +0 -0
  216. /package/dist/server/{repositories → server/src/repositories}/surface-layouts.js +0 -0
  217. /package/dist/server/{repositories → server/src/repositories}/tags.js +0 -0
  218. /package/dist/server/{repositories → server/src/repositories}/task-runs.js +0 -0
  219. /package/dist/server/{repositories → server/src/repositories}/users.js +0 -0
  220. /package/dist/server/{repositories → server/src/repositories}/weekly-reviews.js +0 -0
  221. /package/dist/server/{repositories → server/src/repositories}/wiki-memory.js +0 -0
  222. /package/dist/server/{repositories → server/src/repositories}/work-adjustments.js +0 -0
  223. /package/dist/server/{seed-demo.js → server/src/seed-demo.js} +0 -0
  224. /package/dist/server/{services → server/src/services}/context.js +0 -0
  225. /package/dist/server/{services → server/src/services}/dashboard.js +0 -0
  226. /package/dist/server/{services → server/src/services}/gamification.js +0 -0
  227. /package/dist/server/{services → server/src/services}/insights.js +0 -0
  228. /package/dist/server/{services → server/src/services}/openai-codex-oauth.js +0 -0
  229. /package/dist/server/{services → server/src/services}/projects.js +0 -0
  230. /package/dist/server/{services → server/src/services}/psyche.js +0 -0
  231. /package/dist/server/{services → server/src/services}/relations.js +0 -0
  232. /package/dist/server/{services → server/src/services}/reviews.js +0 -0
  233. /package/dist/server/{services → server/src/services}/run-recovery.js +0 -0
  234. /package/dist/server/{services → server/src/services}/tagging.js +0 -0
  235. /package/dist/server/{services → server/src/services}/task-run-watchdog.js +0 -0
  236. /package/dist/server/{services → server/src/services}/work-time.js +0 -0
  237. /package/dist/server/{watch-mobile.js → server/src/watch-mobile.js} +0 -0
@@ -2,61 +2,66 @@ import Fastify from "fastify";
2
2
  import cors from "@fastify/cors";
3
3
  import multipart from "@fastify/multipart";
4
4
  import { CronExpressionParser } from "cron-parser";
5
- import { ZodError } from "zod";
6
- import { configureDatabase, configureDatabaseSeeding, runInTransaction } from "./db.js";
5
+ import { z, ZodError } from "zod";
6
+ import { configureDatabase, configureDatabaseSeeding, getEffectiveDataRoot, resolveDataDir, resolveDatabasePathForDataRoot, runInTransaction } from "./db.js";
7
7
  import { HttpError, isHttpError } from "./errors.js";
8
8
  import { listActivityEvents, listActivityEventsForTask, recordActivityEvent, removeActivityEvent } from "./repositories/activity-events.js";
9
9
  import { approveApprovalRequest, createAgentAction, createInsight, createInsightFeedback, deleteInsight, getInsightById, listAgentActions, listApprovalRequests, listInsights, rejectApprovalRequest, updateInsight } from "./repositories/collaboration.js";
10
- import { createAiConnector, deleteAiConnector, getAiConnectorById, getAiConnectorBySlug, getAiConnectorConversationForConnector, listAiConnectorRuns, listAiConnectors, runAiConnector, updateAiConnector } from "./repositories/ai-connectors.js";
10
+ import { createAiConnector, deleteAiConnector, getAiConnectorById, getAiConnectorRunById, getAiConnectorRunNodeResult, getAiConnectorRunNodeResults, getLatestAiConnectorNodeOutput, getAiConnectorBySlug, getAiConnectorConversationForConnector, listAiConnectorRuns, listAiConnectors, runAiConnector, updateAiConnector } from "./repositories/ai-connectors.js";
11
11
  import { createAiProcessor, createAiProcessorLink, deleteAiProcessor, deleteAiProcessorLink, getAiProcessorById, getAiProcessorBySlug, listAiProcessors, getSurfaceProcessorGraph, runAiProcessor, updateAiProcessor } from "./repositories/ai-processors.js";
12
12
  import { listEventLog } from "./repositories/event-log.js";
13
13
  import { createDiagnosticMessage, DIAGNOSTIC_LOG_RETENTION_SWEEP_INTERVAL_MS, enforceDiagnosticLogRetention, listDiagnosticLogs, normalizeDiagnosticSource, recordDiagnosticLog, serializeDiagnosticError } from "./repositories/diagnostic-logs.js";
14
14
  import { createGoal, getGoalById, listGoals, updateGoal } from "./repositories/goals.js";
15
15
  import { getSurfaceLayout, resetSurfaceLayout, saveSurfaceLayout } from "./repositories/surface-layouts.js";
16
- import { listForgeBoxCatalog } from "./connectors/box-registry.js";
16
+ import { buildConnectorOutputCatalogEntry, listForgeBoxCatalog } from "./connectors/box-registry.js";
17
17
  import { createHabit, createHabitCheckIn, deleteHabitCheckIn, getHabitById, listHabits, updateHabit } from "./repositories/habits.js";
18
18
  import { listDomains } from "./repositories/domains.js";
19
19
  import { buildNotesSummaryByEntity, createNote, getNoteById, listNotes, updateNote } from "./repositories/notes.js";
20
20
  import { createWikiIngestJobSchema, createUploadedWikiIngestJob, createWikiSpace, createWikiSpaceSchema, deleteWikiIngestJob, deleteWikiProfile, getWikiHealth, getWikiIngestJob, getWikiHomePageDetail, getWikiPageDetail, getWikiPageDetailBySlug, getWikiSettingsPayload, ingestWikiSource, listWikiIngestJobs, listWikiLlmProfiles, listWikiPageTree, listWikiPages, listWikiSpaces, processWikiIngestJob, reindexWikiEmbeddings, reindexWikiEmbeddingsSchema, rerunWikiIngestJob, reviewWikiIngestJob, reviewWikiIngestJobSchema, searchWikiPages, syncWikiVaultFromDisk, syncWikiVaultSchema, testWikiLlmProfileSchema, upsertWikiEmbeddingProfile, upsertWikiEmbeddingProfileSchema, upsertWikiLlmProfile, upsertWikiLlmProfileSchema, wikiSearchQuerySchema } from "./repositories/wiki-memory.js";
21
21
  import { filterOwnedEntities, setEntityOwner } from "./repositories/entity-ownership.js";
22
22
  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";
23
- import { cloneQuestionnaireInstrument, completeQuestionnaireRun, createQuestionnaireInstrument, ensureQuestionnaireDraftVersion, getQuestionnaireInstrumentDetail, getQuestionnaireRunDetail, listQuestionnaireInstruments, publishQuestionnaireDraftVersion, startQuestionnaireRun, updateQuestionnaireDraftVersion, updateQuestionnaireRun } from "./repositories/questionnaires.js";
23
+ import { cloneQuestionnaireInstrument, completeQuestionnaireRun, createQuestionnaireInstrument, deleteQuestionnaireInstrument, ensureQuestionnaireDraftVersion, getQuestionnaireInstrumentDetail, getQuestionnaireRunDetail, listQuestionnaireInstruments, publishQuestionnaireDraftVersion, startQuestionnaireRun, updateQuestionnaireInstrument, updateQuestionnaireInstrumentSchema, updateQuestionnaireDraftVersion, updateQuestionnaireRun } from "./repositories/questionnaires.js";
24
24
  import { createProject, updateProject } from "./repositories/projects.js";
25
- import { createPreferenceCatalog, createPreferenceCatalogItem, createPreferenceContext, createPreferenceItem, createPreferenceItemFromEntity, deletePreferenceCatalog, deletePreferenceCatalogItem, getPreferenceWorkspace, mergePreferenceContexts, startPreferenceGame, submitAbsoluteSignal, submitPairwiseJudgment, updatePreferenceCatalog, updatePreferenceCatalogItem, updatePreferenceContext, updatePreferenceItem, updatePreferenceScore } from "./repositories/preferences.js";
25
+ import { createPreferenceCatalog, createPreferenceCatalogItem, createPreferenceContext, createPreferenceItem, createPreferenceItemFromEntity, deletePreferenceCatalog, deletePreferenceCatalogItem, deletePreferenceContext, deletePreferenceItem, getPreferenceCatalogById, getPreferenceCatalogItemById, getPreferenceContextById, getPreferenceItemById, getPreferenceWorkspace, listPreferenceCatalogItems, listPreferenceCatalogs, listPreferenceContexts, listPreferenceItems, mergePreferenceContexts, startPreferenceGame, submitAbsoluteSignal, submitPairwiseJudgment, updatePreferenceCatalog, updatePreferenceCatalogItem, updatePreferenceContext, updatePreferenceItem, updatePreferenceScore } from "./repositories/preferences.js";
26
26
  import { createStrategy, getStrategyById, listStrategies, updateStrategy } from "./repositories/strategies.js";
27
+ import { buildKnowledgeGraph, buildKnowledgeGraphFocus } from "./services/knowledge-graph.js";
27
28
  import { createManualRewardGrant, getDailyAmbientXp, getRewardRuleById, listRewardLedger, listRewardRules, recordWorkAdjustmentReward, recordSessionEvent, updateRewardRule } from "./repositories/rewards.js";
28
- import { listAgentIdentities, getSettings, isPsycheAuthRequired, updateSettings, verifyAgentToken } from "./repositories/settings.js";
29
+ import { getSettingsFileStatus, listAgentIdentities, getSettings, isPsycheAuthRequired, mirrorSettingsFileFromCurrentState, updateSettings, verifyAgentToken } from "./repositories/settings.js";
29
30
  import { deleteAiModelConnection, getAiModelConnectionById, readModelConnectionCredential, upsertAiModelConnection } from "./repositories/model-settings.js";
30
31
  import { createTag, getTagById, listTags, updateTag } from "./repositories/tags.js";
31
32
  import { createUser, ensureSystemUsers, getDefaultUser, getUserById, listUserAccessGrants, listUserOwnershipSummaries, listUserXpSummaries, listUsers, resolveUserForMutation, updateUserAccessGrant, updateUser } from "./repositories/users.js";
32
33
  import { claimTaskRun, completeTaskRun, focusTaskRun, heartbeatTaskRun, listTaskRuns, recoverTimedOutTaskRuns, releaseTaskRun } from "./repositories/task-runs.js";
33
- import { createTask, createTaskWithIdempotency, getTaskById, listTasks, uncompleteTask, updateTask } from "./repositories/tasks.js";
34
+ import { createTask, createTaskWithIdempotency, getTaskById, listTasks, splitTask, uncompleteTask, updateTask } from "./repositories/tasks.js";
34
35
  import { createWorkAdjustment } from "./repositories/work-adjustments.js";
35
- import { createCalendarEvent, createTaskTimebox, createWorkBlockTemplate, deleteCalendarEvent, deleteTaskTimebox, deleteWorkBlockTemplate, getCalendarConnectionById, getCalendarEventById, listCalendars, listCalendarEvents, listTaskTimeboxes, suggestTaskTimeboxes, listWorkBlockInstances, listWorkBlockTemplates, updateCalendarEvent, updateTaskTimebox, updateWorkBlockTemplate } from "./repositories/calendar.js";
36
+ import { createCalendarEvent, createTaskTimebox, createWorkBlockTemplate, deleteCalendarEvent, deleteTaskTimebox, deleteWorkBlockTemplate, getCalendarConnectionById, getCalendarEventById, getTaskTimeboxById, getWorkBlockTemplateById, listCalendars, listCalendarEvents, listTaskTimeboxes, suggestTaskTimeboxes, listWorkBlockInstances, listWorkBlockTemplates, updateCalendarEvent, updateTaskTimebox, updateWorkBlockTemplate } from "./repositories/calendar.js";
36
37
  import { getDashboard } from "./services/dashboard.js";
37
38
  import { getOverviewContext, getRiskContext, getTodayContext } from "./services/context.js";
38
39
  import { buildGamificationOverview, buildGamificationProfile, buildXpMomentumPulse } from "./services/gamification.js";
39
40
  import { getInsightsPayload } from "./services/insights.js";
41
+ import { buildLifeForcePayload, createFatigueSignal, listLifeForceTemplates, resolveLifeForceUser, updateLifeForceProfile, updateLifeForceTemplate } from "./services/life-force.js";
40
42
  import { createEntities, deleteEntities, deleteEntity, getSettingsBinPayload, restoreEntities, searchEntities, updateEntities } from "./services/entity-crud.js";
41
43
  import { getPsycheOverview } from "./services/psyche.js";
42
- import { getPsycheObservationCalendar } from "./services/psyche-observation-calendar.js";
44
+ import { exportPsycheObservationCalendar, getPsycheObservationCalendar } from "./services/psyche-observation-calendar.js";
43
45
  import { getProjectBoard, getProjectSummary, listProjectSummaries } from "./services/projects.js";
46
+ import { createDataBackup, exportData, getDataManagementState, maybeRunAutomaticBackup, restoreDataBackup, scanForDataRecoveryCandidates, switchDataRoot, updateDataManagementSettings } from "./services/data-management.js";
44
47
  import { getWeeklyReviewPayload } from "./services/reviews.js";
45
48
  import { finalizeWeeklyReviewClosure } from "./repositories/weekly-reviews.js";
46
49
  import { createTaskRunWatchdog } from "./services/task-run-watchdog.js";
47
50
  import { suggestTags } from "./services/tagging.js";
48
- import { CalendarConnectionConflictError, completeMicrosoftCalendarOauth, createCalendarConnection, deleteCalendarEventProjection, discoverCalendarConnection, discoverExistingCalendarConnection, getMicrosoftCalendarOauthSession, listConnectedCalendarConnections, removeCalendarConnection, pushCalendarEventUpdate, readCalendarOverview, syncCalendarConnection, startMicrosoftCalendarOauth, testMicrosoftCalendarOauthConfiguration, listCalendarProviderMetadata, updateCalendarConnectionSelection } from "./services/calendar-runtime.js";
51
+ import { CalendarConnectionConflictError, completeGoogleCalendarOauth, completeMicrosoftCalendarOauth, createCalendarConnection, deleteCalendarEventProjection, discoverCalendarConnection, discoverExistingCalendarConnection, getGoogleCalendarOauthSession, getMicrosoftCalendarOauthSession, listConnectedCalendarConnections, removeCalendarConnection, pushCalendarEventUpdate, readCalendarOverview, syncCalendarConnection, startGoogleCalendarOauth, startMicrosoftCalendarOauth, testMicrosoftCalendarOauthConfiguration, listCalendarProviderMetadata, updateCalendarConnectionSelection } from "./services/calendar-runtime.js";
49
52
  import { consumeOpenAiCodexOauthCredentials, getOpenAiCodexOauthSession, startOpenAiCodexOauthSession, submitOpenAiCodexOauthManualInput } from "./services/openai-codex-oauth.js";
50
53
  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";
51
54
  import { createQuestionnaireInstrumentSchema, publishQuestionnaireVersionSchema, startQuestionnaireRunSchema, updateQuestionnaireRunSchema, updateQuestionnaireVersionSchema } from "./questionnaire-types.js";
52
55
  import { createPreferenceCatalogItemSchema, createPreferenceCatalogSchema, createPreferenceContextSchema, createPreferenceItemSchema, enqueueEntityPreferenceItemSchema, mergePreferenceContextsSchema, preferenceWorkspaceQuerySchema, startPreferenceGameSchema, submitAbsoluteSignalSchema, submitPairwiseJudgmentSchema, updatePreferenceCatalogItemSchema, updatePreferenceCatalogSchema, updatePreferenceContextSchema, updatePreferenceItemSchema, updatePreferenceScoreSchema } from "./preferences-types.js";
53
- import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, createAiConnectorSchema, createAiProcessorLinkSchema, createAiProcessorSchema, runAiConnectorSchema, writeSurfaceLayoutSchema, upsertAiModelConnectionSchema, testAiModelConnectionSchema, submitOpenAiCodexOauthManualCodeSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createStrategySchema, createUserSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createCalendarEventSchema, createHabitCheckInSchema, createCalendarConnectionSchema, createDiagnosticLogSchema, discoverCalendarConnectionSchema, startMicrosoftCalendarOauthSchema, testMicrosoftCalendarOauthConfigurationSchema, createHabitSchema, createTaskTimeboxSchema, createWorkBlockTemplateSchema, createSessionEventSchema, createWorkAdjustmentSchema, createTagSchema, calendarOverviewQuerySchema, notesListQuerySchema, updateTagSchema, createTaskSchema, diagnosticLogListQuerySchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateStrategySchema, updateUserSchema, updateCalendarConnectionSchema, updateCalendarEventSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskTimeboxSchema, updateTaskSchema, updateUserAccessGrantSchema, updateWorkBlockTemplateSchema, updateAiConnectorSchema, updateAiProcessorSchema, runAiProcessorSchema, workAdjustmentResultSchema, finalizeWeeklyReviewResultSchema, goalListQuerySchema, recommendTaskTimeboxesSchema, strategyListQuerySchema } from "./types.js";
56
+ import { createDataBackupSchema, dataExportQuerySchema, restoreDataBackupSchema, switchDataRootSchema, updateDataManagementSettingsSchema } from "./data-management-types.js";
57
+ import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, createAiConnectorSchema, createAiProcessorLinkSchema, createAiProcessorSchema, runAiConnectorSchema, writeSurfaceLayoutSchema, upsertAiModelConnectionSchema, testAiModelConnectionSchema, submitOpenAiCodexOauthManualCodeSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createStrategySchema, createUserSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createCalendarEventSchema, createHabitCheckInSchema, createCalendarConnectionSchema, createDiagnosticLogSchema, discoverCalendarConnectionSchema, startGoogleCalendarOauthSchema, startMicrosoftCalendarOauthSchema, testMicrosoftCalendarOauthConfigurationSchema, createHabitSchema, createTaskTimeboxSchema, createWorkBlockTemplateSchema, createSessionEventSchema, createWorkAdjustmentSchema, createTagSchema, calendarOverviewQuerySchema, psycheObservationCalendarExportQuerySchema, notesListQuerySchema, updateTagSchema, createTaskSchema, diagnosticLogListQuerySchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskSplitCreateSchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateStrategySchema, updateUserSchema, updateCalendarConnectionSchema, updateCalendarEventSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskTimeboxSchema, updateTaskSchema, lifeForceProfilePatchSchema, lifeForceTemplateUpdateSchema, fatigueSignalCreateSchema, updateUserAccessGrantSchema, updateWorkBlockTemplateSchema, updateAiConnectorSchema, updateAiProcessorSchema, runAiProcessorSchema, workAdjustmentResultSchema, finalizeWeeklyReviewResultSchema, goalListQuerySchema, recommendTaskTimeboxesSchema, strategyListQuerySchema } from "./types.js";
54
58
  import { buildOpenApiDocument } from "./openapi.js";
55
59
  import { registerWebRoutes } from "./web.js";
56
60
  import { createManagerRuntime } from "./managers/runtime.js";
57
61
  import { isManagerError } from "./managers/type-guards.js";
58
- import { createCompanionPairingSession, createCompanionPairingSessionSchema, getCompanionOverview, getFitnessViewData, getSleepViewData, ingestMobileHealthSync, mobileHealthSyncSchema, requireValidPairing, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
59
- import { createMovementPlace, getMovementAllTimeSummary, getMovementDayDetail, getMovementMobileBootstrap, getMovementTimeline, getMovementSelectionAggregate, getMovementSettings, getMovementTripDetail, getMovementMonthSummary, listMovementPlaces, movementMobileBootstrapSchema, movementMobilePlaceMutationSchema, movementMobileStayPatchSchema, movementMobileTimelineSchema, movementMobileTripPatchSchema, movementPlaceMutationSchema, movementPlacePatchSchema, movementSelectionAggregateSchema, movementStayPatchSchema, movementSettingsPatchSchema, movementTimelineQuerySchema, movementTripPointPatchSchema, movementTripPatchSchema, deleteMovementTripPoint, deleteMovementStay, deleteMovementTrip, updateMovementPlace, updateMovementStay, updateMovementSettings, updateMovementTrip, updateMovementTripPoint } from "./movement.js";
62
+ import { createCompanionPairingSession, createCompanionPairingSessionSchema, createSleepSession, createSleepSessionSchema, createWorkoutSession, createWorkoutSessionSchema, deleteSleepSession, deleteWorkoutSession, getCompanionPairingSessionById, getCompanionOverview, getFitnessViewData, getSleepSessionById, getSleepViewData, getWorkoutSessionById, ingestMobileHealthSync, mobileHealthSyncSchema, patchCompanionPairingSourceState, patchCompanionPairingSourceStateSchema, companionSourceKeySchema, requireValidPairing, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, updateMobileCompanionSourceState, updateMobileCompanionSourceStateSchema, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
63
+ import { analyzeMovementUserBoxPreflight, createMovementUserBox, createMovementPlace, deleteMovementUserBox, getMovementAllTimeSummary, getMovementDayDetail, getMovementMobileBootstrap, getMovementTimeline, getMovementSelectionAggregate, getMovementSettings, getMovementTripDetail, getMovementMonthSummary, invalidateAutomaticMovementBox, listMovementPlaces, movementAutomaticBoxInvalidateSchema, movementMobileBootstrapSchema, movementMobilePlaceMutationSchema, movementMobileUserBoxCreateSchema, movementMobileUserBoxPreflightSchema, movementMobileUserBoxPatchSchema, movementMobileAutomaticBoxInvalidateSchema, movementMobileTimelineSchema, movementPlaceMutationSchema, movementPlacePatchSchema, movementSelectionAggregateSchema, movementStayPatchSchema, movementTripPatchSchema, movementUserBoxCreateSchema, movementUserBoxPreflightSchema, movementUserBoxPatchSchema, movementSettingsPatchSchema, movementTimelineQuerySchema, movementTripPointPatchSchema, deleteMovementStay, deleteMovementTrip, deleteMovementTripPoint, updateMovementPlace, updateMovementSettings, updateMovementStay, updateMovementTrip, updateMovementUserBox, updateMovementTripPoint, resolveMovementTimelineSegmentForBox } from "./movement.js";
64
+ import { getScreenTimeAllTimeSummary, getScreenTimeDayDetail, getScreenTimeMonthSummary, getScreenTimeSettings, screenTimeSettingsPatchSchema, updateScreenTimeSettings } from "./screen-time.js";
60
65
  import { assertWatchReady, buildWatchBootstrap, ingestWatchCaptureBatch, mobileWatchBootstrapSchema, mobileWatchCaptureBatchSchema, mobileWatchHabitCheckInSchema } from "./watch-mobile.js";
61
66
  const COMPATIBILITY_SUNSET = "transitional-node";
62
67
  function markCompatibilityRoute(reply) {
@@ -162,7 +167,7 @@ function getRequestOrigin(request) {
162
167
  request.hostname;
163
168
  return `${protocol}://${host}`;
164
169
  }
165
- const AGENT_ONBOARDING_ENTITY_CATALOG = [
170
+ const AGENT_ONBOARDING_ENTITY_CATALOG_BASE = [
166
171
  {
167
172
  entityType: "goal",
168
173
  purpose: "A long-horizon outcome or direction. Goals anchor projects and tasks.",
@@ -1888,250 +1893,1138 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
1888
1893
  ]
1889
1894
  }
1890
1895
  ];
1891
- const AGENT_ONBOARDING_CONVERSATION_RULES = [
1892
- "Ask only for what is missing or unclear instead of walking the user through every optional field.",
1893
- "Use a progression of concrete example or intent, working name, purpose or meaning, placement in Forge, operational details, and linked context.",
1894
- "Ask one to three focused questions at a time. One is usually best when the user is uncertain or emotionally loaded.",
1895
- "If the user already answered the normal opening question, do not repeat it. Move to the next missing clarification.",
1896
- "Do not over-therapize logistical entities. For tasks, calendar events, work blocks, timeboxes, and task runs, one brief confirming sentence plus one question is usually enough.",
1897
- "Before saving, briefly summarize the working formulation in the user's own language when that would reduce ambiguity.",
1898
- "When updating an entity, start with what is changing, what should stay true, and what prompted the update now."
1899
- ];
1900
- const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
1901
- {
1902
- focus: "goal",
1903
- openingQuestion: "What direction here feels important enough that you want to keep it in view?",
1904
- coachingGoal: "Clarify the direction and why it matters, not just produce a title.",
1905
- askSequence: [
1906
- "Ask what direction or outcome the user wants to keep in view.",
1907
- "Ask why it matters now.",
1908
- "Distinguish the goal from a project or task.",
1909
- "Clarify horizon and status only after the meaning is clear."
1910
- ]
1911
- },
1912
- {
1913
- focus: "project",
1914
- openingQuestion: "If this became a real project, what would you be trying to make true?",
1915
- coachingGoal: "Turn an intention into a bounded workstream with a clear outcome.",
1916
- askSequence: [
1917
- "Ask what this piece of work is trying to make true.",
1918
- "Ask what outcome would make the project feel real or complete for now.",
1919
- "Ask which goal it belongs under.",
1920
- "Land on a working name once the scope is clear.",
1921
- "Clarify status, owner, and notes only after the scope is clear."
1922
- ]
1923
- },
1924
- {
1925
- focus: "strategy",
1926
- openingQuestion: "What future state is this strategy supposed to make real?",
1927
- coachingGoal: "Turn a vague plan into a deliberate sequence toward a real end state.",
1928
- askSequence: [
1929
- "Ask what end state the strategy is trying to land.",
1930
- "Ask which goals or projects are the true targets.",
1931
- "Ask what the major steps or nodes are.",
1932
- "Ask about order, dependencies, and anything that must not be skipped."
1933
- ]
1934
- },
1935
- {
1936
- focus: "task",
1937
- openingQuestion: "What is the next concrete move you want to remember or do?",
1938
- coachingGoal: "Identify the next concrete move, not just capture a vague obligation.",
1939
- askSequence: [
1940
- "Ask what the next concrete action is.",
1941
- "Ask where it belongs: project, goal, both, or standalone.",
1942
- "Ask what would make it easier to do: due date, priority, owner, or brief context."
1943
- ]
1944
- },
1945
- {
1946
- focus: "habit",
1947
- openingQuestion: "What recurring move are you trying to strengthen or loosen?",
1948
- coachingGoal: "Define the recurring behavior and cadence clearly enough for honest later check-ins.",
1949
- askSequence: [
1950
- "Ask what the recurring behavior is in plain language.",
1951
- "Ask whether doing it is aligned or a slip.",
1952
- "Ask about cadence and what counts as an honest check-in in practice.",
1953
- "Ask about links only if they will help later review."
1954
- ]
1955
- },
1956
- {
1957
- focus: "note",
1958
- openingQuestion: "What feels important to preserve from this?",
1959
- coachingGoal: "Preserve the useful context and link it to the right places without turning the note into a dump.",
1960
- askSequence: [
1961
- "Ask what the note needs to preserve.",
1962
- "Ask what entities it should stay attached to.",
1963
- "Ask whether it should be durable or temporary.",
1964
- "Ask about tags or author only if they help retrieval or handoff."
1965
- ]
1966
- },
1967
- {
1968
- focus: "insight",
1969
- openingQuestion: "What observation or recommendation do you want Forge to remember?",
1970
- coachingGoal: "Capture one grounded observation or recommendation clearly enough that it remains useful later.",
1971
- askSequence: [
1972
- "Ask what pattern, tension, or observation should be remembered.",
1973
- "Ask what entity or timeframe it belongs to, if any.",
1974
- "Ask what recommendation, caution, or invitation should remain explicit."
1975
- ]
1976
- },
1977
- {
1978
- focus: "calendar_event",
1979
- openingQuestion: "What is the event, and when should it happen in your local time?",
1980
- coachingGoal: "Make the event legible as a real commitment in time, with the right timezone and links.",
1981
- askSequence: [
1982
- "Ask what the event is.",
1983
- "Ask when it starts and ends in local time.",
1984
- "Ask where it belongs or what it supports.",
1985
- "Ask whether it should stay Forge-only only if that choice matters."
1986
- ]
1987
- },
1988
- {
1989
- focus: "work_block_template",
1990
- openingQuestion: "What recurring block do you want to set up, and when should it repeat?",
1991
- coachingGoal: "Define a reusable availability rule rather than a one-off event.",
1992
- askSequence: [
1993
- "Ask what kind of block it is and what it should be called.",
1994
- "Ask on which days and at what local times it should repeat.",
1995
- "Ask whether it allows or blocks work.",
1996
- "Ask whether it has a start or end date."
1997
- ]
1998
- },
1999
- {
2000
- focus: "task_timebox",
2001
- openingQuestion: "Which task are you trying to make time for, and when should the slot be?",
2002
- coachingGoal: "Reserve real time for one task without confusing planned work with completed work.",
2003
- askSequence: [
2004
- "Ask which task the slot belongs to.",
2005
- "Ask when the slot should start and end.",
2006
- "Ask about source or override reason only when that context matters."
2007
- ]
2008
- },
2009
- {
2010
- focus: "event_type",
2011
- openingQuestion: "When this kind of moment happens, what would you want to call it so future reports stay consistent?",
2012
- coachingGoal: "Create a reusable incident category that will actually help future reports stay consistent.",
2013
- askSequence: [
2014
- "Ask what category the label should capture.",
2015
- "Ask how narrow or broad it should be.",
2016
- "Ask for a short description only if the label could be ambiguous later."
2017
- ]
2018
- },
2019
- {
2020
- focus: "emotion_definition",
2021
- openingQuestion: "What emotion do you want Forge to help you name clearly and reuse later?",
2022
- coachingGoal: "Create a reusable emotion label with enough clarity to use consistently later.",
2023
- askSequence: [
2024
- "Ask what emotion label the user wants to preserve.",
2025
- "Ask what distinguishes it from nearby emotions.",
2026
- "Ask for a broader category only if it will help later browsing or reporting."
2027
- ]
1896
+ const AGENT_ONBOARDING_BATCH_ROUTE_BASES = {
1897
+ goal: "/api/v1/goals",
1898
+ project: "/api/v1/projects",
1899
+ task: "/api/v1/tasks",
1900
+ strategy: "/api/v1/strategies",
1901
+ habit: "/api/v1/habits",
1902
+ tag: "/api/v1/tags",
1903
+ note: "/api/v1/notes",
1904
+ insight: "/api/v1/insights",
1905
+ calendar_event: "/api/v1/calendar/events",
1906
+ work_block_template: "/api/v1/calendar/work-block-templates",
1907
+ task_timebox: "/api/v1/calendar/timeboxes",
1908
+ sleep_session: "/api/v1/health/sleep",
1909
+ workout_session: "/api/v1/health/workouts",
1910
+ psyche_value: "/api/v1/psyche/values",
1911
+ behavior_pattern: "/api/v1/psyche/patterns",
1912
+ behavior: "/api/v1/psyche/behaviors",
1913
+ belief_entry: "/api/v1/psyche/beliefs",
1914
+ mode_profile: "/api/v1/psyche/modes",
1915
+ mode_guide_session: "/api/v1/psyche/mode-guides",
1916
+ event_type: "/api/v1/psyche/event-types",
1917
+ emotion_definition: "/api/v1/psyche/emotions",
1918
+ trigger_report: "/api/v1/psyche/reports",
1919
+ preference_catalog: "/api/v1/preferences/catalogs",
1920
+ preference_catalog_item: "/api/v1/preferences/catalog-items",
1921
+ preference_context: "/api/v1/preferences/contexts",
1922
+ preference_item: "/api/v1/preferences/items",
1923
+ questionnaire_instrument: "/api/v1/psyche/questionnaires"
1924
+ };
1925
+ function classifyOnboardingEntity(entityType) {
1926
+ if (entityType in AGENT_ONBOARDING_BATCH_ROUTE_BASES) {
1927
+ return "batch_crud_entity";
2028
1928
  }
2029
- ];
2030
- const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
2031
- {
2032
- focus: "psyche_value",
2033
- useWhen: "Use for a lived direction, quality of being, or way of showing up that matters to the user and should guide actions rather than just describe an outcome.",
2034
- coachingGoal: "Clarify the value as a chosen direction, distinguish it from a goal, and gather one concrete way the user wants to embody it now.",
2035
- askSequence: [
2036
- "Start with what matters and why it matters now.",
2037
- "Ask for one concrete example of what living this value would look like in ordinary life.",
2038
- "Separate the value direction from any specific outcome or achievement goal.",
2039
- "Notice tensions, barriers, or situations where the value gets lost.",
2040
- "Name one small committed action that would move toward the value."
2041
- ],
2042
- requiredForCreate: ["title"],
2043
- highValueOptionalFields: [
2044
- "description",
2045
- "valuedDirection",
2046
- "whyItMatters",
2047
- "committedActions",
2048
- "linkedGoalIds",
2049
- "linkedProjectIds",
2050
- "linkedTaskIds"
2051
- ],
2052
- exampleQuestions: [
2053
- "What feels deeply important about this to you?",
2054
- "If you were living this value a little more this week, what would someone be able to see?",
2055
- "What goal or area of life does this value belong to most clearly?",
2056
- "When this value is hard to live, what tends to get in the way?",
2057
- "What is one small action that would express it in practice?"
1929
+ if (entityType === "wiki_page" || entityType === "calendar_connection") {
1930
+ return "specialized_crud_entity";
1931
+ }
1932
+ if (entityType === "task_run" ||
1933
+ entityType === "questionnaire_run" ||
1934
+ entityType === "preference_judgment" ||
1935
+ entityType === "preference_signal" ||
1936
+ entityType === "work_adjustment") {
1937
+ return "action_workflow_entity";
1938
+ }
1939
+ return "read_model_only_surface";
1940
+ }
1941
+ function buildPreferredMutationPath(entityType) {
1942
+ if (entityType in AGENT_ONBOARDING_BATCH_ROUTE_BASES) {
1943
+ return "/api/v1/entities/create | /api/v1/entities/update | /api/v1/entities/delete | /api/v1/entities/search";
1944
+ }
1945
+ switch (entityType) {
1946
+ case "wiki_page":
1947
+ return "Use /api/v1/wiki/pages with POST or PATCH for page CRUD.";
1948
+ case "calendar_connection":
1949
+ return "Use /api/v1/calendar/connections plus provider-specific setup flows.";
1950
+ case "task_run":
1951
+ return "Use the task-run action routes to start, heartbeat, focus, complete, or release live work.";
1952
+ case "questionnaire_run":
1953
+ return "Use the questionnaire-run action routes to start, patch answers, and complete the run.";
1954
+ case "preference_judgment":
1955
+ return "Use /api/v1/preferences/judgments to record one pairwise comparison.";
1956
+ case "preference_signal":
1957
+ return "Use /api/v1/preferences/signals to record one direct signal such as favorite or veto.";
1958
+ case "work_adjustment":
1959
+ return "Use /api/v1/work-adjustments to apply an explicit operator adjustment.";
1960
+ case "self_observation":
1961
+ return "Read the calendar surface; mutate it by creating or updating note-backed observations with frontmatter.observedAt.";
1962
+ case "sleep_overview":
1963
+ return "Read-only surface. Use batch CRUD for sleep_session records or the review enrichment route for reflective notes.";
1964
+ case "sports_overview":
1965
+ return "Read-only surface. Use batch CRUD for workout_session records or the review enrichment route for reflective notes.";
1966
+ default:
1967
+ return "Read-only surface.";
1968
+ }
1969
+ }
1970
+ function buildPreferredReadPath(entityType) {
1971
+ if (entityType in AGENT_ONBOARDING_BATCH_ROUTE_BASES) {
1972
+ return AGENT_ONBOARDING_BATCH_ROUTE_BASES[entityType];
1973
+ }
1974
+ switch (entityType) {
1975
+ case "wiki_page":
1976
+ return "/api/v1/wiki/pages/:id";
1977
+ case "calendar_connection":
1978
+ return "/api/v1/calendar/connections";
1979
+ case "task_run":
1980
+ return "/api/v1/operator/context";
1981
+ case "questionnaire_run":
1982
+ return "/api/v1/psyche/questionnaire-runs/:id";
1983
+ case "preference_judgment":
1984
+ case "preference_signal":
1985
+ return "/api/v1/preferences/workspace";
1986
+ case "work_adjustment":
1987
+ return "/api/v1/operator/context";
1988
+ case "self_observation":
1989
+ return "/api/v1/psyche/self-observation/calendar";
1990
+ case "sleep_overview":
1991
+ return "/api/v1/health/sleep";
1992
+ case "sports_overview":
1993
+ return "/api/v1/health/fitness";
1994
+ default:
1995
+ return null;
1996
+ }
1997
+ }
1998
+ function enrichOnboardingEntityGuide(entry) {
1999
+ const classification = classifyOnboardingEntity(entry.entityType);
2000
+ return {
2001
+ ...entry,
2002
+ classification,
2003
+ routeBase: classification === "batch_crud_entity"
2004
+ ? AGENT_ONBOARDING_BATCH_ROUTE_BASES[entry.entityType]
2005
+ : null,
2006
+ preferredMutationPath: buildPreferredMutationPath(entry.entityType),
2007
+ preferredReadPath: buildPreferredReadPath(entry.entityType),
2008
+ preferredMutationTool: classification === "batch_crud_entity"
2009
+ ? "forge_create_entities | forge_update_entities | forge_delete_entities | forge_search_entities"
2010
+ : null
2011
+ };
2012
+ }
2013
+ const AGENT_ONBOARDING_ENTITY_CATALOG = [
2014
+ ...AGENT_ONBOARDING_ENTITY_CATALOG_BASE.map(enrichOnboardingEntityGuide),
2015
+ enrichOnboardingEntityGuide({
2016
+ entityType: "tag",
2017
+ purpose: "A shared classification label used across Forge entities and notes.",
2018
+ minimumCreateFields: ["label"],
2019
+ relationshipRules: [
2020
+ "Tags are simple reusable labels, not a substitute for richer entity links.",
2021
+ "They use batch CRUD like other simple entities."
2058
2022
  ],
2059
- notes: [
2060
- "Use an ACT-style values clarification stance: values are directions to live toward, not boxes to complete.",
2061
- "Ask one or two questions at a time, reflect back the user's language, and only then move toward naming committed actions or linked work items.",
2062
- "If the user says they want to understand it first, start with one orienting question before offering a formulation or save suggestion."
2023
+ searchHints: ["Search by label before creating a near-duplicate tag."],
2024
+ examples: ['{"label":"Deep work","kind":"execution"}'],
2025
+ fieldGuide: [
2026
+ {
2027
+ name: "label",
2028
+ type: "string",
2029
+ required: true,
2030
+ description: "Human-readable tag label."
2031
+ },
2032
+ {
2033
+ name: "kind",
2034
+ type: "value|category|execution",
2035
+ required: false,
2036
+ description: "Optional tag family.",
2037
+ enumValues: ["value", "category", "execution"],
2038
+ defaultValue: "category"
2039
+ }
2063
2040
  ]
2064
- },
2065
- {
2066
- focus: "behavior_pattern",
2067
- useWhen: "Use for a recurring loop that shows up across multiple situations and can be described as cue -> response -> payoff -> cost -> preferred response.",
2068
- coachingGoal: "Help the user build a CBT-style functional analysis with active listening instead of just naming the problem vaguely.",
2069
- askSequence: [
2070
- "Start from one recent concrete example before generalizing the loop.",
2071
- "Identify the typical cue, vulnerability, or context that makes the loop more likely.",
2072
- "Reflect back the sequence of thoughts, feelings, body state, and visible behavior once it starts.",
2073
- "Clarify the short-term payoff, protection, or escape function.",
2074
- "Clarify the long-term cost to the self, relationships, work, or values.",
2075
- "Ask what a slightly more workable response would look like.",
2076
- "Notice adjacent beliefs, schema themes, modes, or values that should be linked or saved separately."
2077
- ],
2078
- requiredForCreate: ["title"],
2079
- highValueOptionalFields: [
2080
- "description",
2081
- "targetBehavior",
2082
- "cueContexts",
2083
- "shortTermPayoff",
2084
- "longTermCost",
2085
- "preferredResponse",
2086
- "linkedBeliefIds",
2087
- "linkedModeIds",
2088
- "linkedValueIds"
2089
- ],
2090
- exampleQuestions: [
2091
- "Can we slow this down using one recent example first?",
2092
- "What usually sets this loop off, and what was going on just before it started?",
2093
- "What do you notice in your thoughts, body, and actions once it gets going?",
2094
- "What does that move do for you immediately?",
2095
- "What does it cost you later?",
2096
- "What belief, rule, or vulnerable part seems to get activated inside this loop?",
2097
- "If this loop loosened a little, what response would you want to make instead?"
2041
+ }),
2042
+ enrichOnboardingEntityGuide({
2043
+ entityType: "sleep_session",
2044
+ purpose: "A first-class health record for one night with timing, derived sleep scores, optional stage detail, and reflective links back into Forge.",
2045
+ minimumCreateFields: ["startedAt", "endedAt"],
2046
+ relationshipRules: [
2047
+ "Use batch CRUD for ordinary sleep_session create, update, delete, and search work.",
2048
+ "The direct PATCH route is still available when enriching an existing night with reflective notes after review.",
2049
+ "Sleep deletions are immediate and do not go through the settings bin."
2098
2050
  ],
2099
- notes: [
2100
- "A pattern is usually the best Psyche container for functional analysis.",
2101
- "If the user is describing one specific episode rather than a repeated loop, prefer a trigger report.",
2102
- "Reflect before the next question, and avoid interrogating through the schema fields in order.",
2103
- "If the user asks to understand the loop first, do not lead with a finished working diagnosis or title before asking at least one clarifying question.",
2104
- "Before you ask how to change the loop, ask what it is protecting, preventing, or managing for the user."
2105
- ]
2106
- },
2107
- {
2108
- focus: "behavior",
2109
- useWhen: "Use for one recurring move, coping action, or regulating action that the user wants to understand more clearly and possibly link to a broader pattern.",
2110
- coachingGoal: "Describe the behavior in plain language, understand its function, classify whether it moves away, toward, or back into repair, and identify a more workable move when relevant.",
2111
- askSequence: [
2112
- "Start with a recent example of the behavior in context.",
2113
- "Name what the user actually does or tends to do.",
2114
- "Clarify what cues, urges, or situations pull the behavior online.",
2115
- "Clarify the short-term payoff or relief.",
2116
- "Clarify the long-term cost or price.",
2117
- "Decide whether the behavior is away, committed, or recovery.",
2118
- "Identify a replacement move or repair plan if the user wants one."
2051
+ searchHints: [
2052
+ "Search by linked entities or date window before creating a duplicate manual night."
2119
2053
  ],
2120
- requiredForCreate: ["kind", "title"],
2121
- highValueOptionalFields: [
2122
- "description",
2123
- "commonCues",
2124
- "urgeStory",
2125
- "shortTermPayoff",
2126
- "longTermCost",
2127
- "replacementMove",
2128
- "repairPlan",
2129
- "linkedPatternIds",
2130
- "linkedValueIds",
2131
- "linkedSchemaIds",
2132
- "linkedModeIds"
2054
+ examples: [
2055
+ '{"startedAt":"2026-04-10T22:45:00.000Z","endedAt":"2026-04-11T06:45:00.000Z","qualitySummary":"Slept cleanly after a light evening.","links":[{"entityType":"habit","entityId":"habit_sleep_hygiene","relationshipType":"supports"}]}'
2133
2056
  ],
2134
- exampleQuestions: [
2057
+ fieldGuide: [
2058
+ {
2059
+ name: "startedAt",
2060
+ type: "ISO datetime",
2061
+ required: true,
2062
+ description: "Sleep start timestamp."
2063
+ },
2064
+ {
2065
+ name: "endedAt",
2066
+ type: "ISO datetime",
2067
+ required: true,
2068
+ description: "Sleep end timestamp."
2069
+ },
2070
+ {
2071
+ name: "timeInBedSeconds",
2072
+ type: "integer",
2073
+ required: false,
2074
+ description: "Defaults from startedAt and endedAt when omitted."
2075
+ },
2076
+ {
2077
+ name: "asleepSeconds",
2078
+ type: "integer",
2079
+ required: false,
2080
+ description: "Defaults to timeInBedSeconds when omitted."
2081
+ },
2082
+ {
2083
+ name: "awakeSeconds",
2084
+ type: "integer",
2085
+ required: false,
2086
+ description: "Defaults to the residual between timeInBedSeconds and asleepSeconds."
2087
+ },
2088
+ {
2089
+ name: "stageBreakdown",
2090
+ type: "array",
2091
+ required: false,
2092
+ description: "Optional list of { stage, seconds } items.",
2093
+ defaultValue: []
2094
+ },
2095
+ {
2096
+ name: "recoveryMetrics",
2097
+ type: "object",
2098
+ required: false,
2099
+ description: "Optional metric bag attached to the night.",
2100
+ defaultValue: {}
2101
+ },
2102
+ {
2103
+ name: "qualitySummary",
2104
+ type: "string",
2105
+ required: false,
2106
+ description: "Optional reflection summary.",
2107
+ defaultValue: ""
2108
+ },
2109
+ {
2110
+ name: "notes",
2111
+ type: "string",
2112
+ required: false,
2113
+ description: "Optional longer reflective note.",
2114
+ defaultValue: ""
2115
+ },
2116
+ {
2117
+ name: "tags",
2118
+ type: "string[]",
2119
+ required: false,
2120
+ description: "Optional review tags.",
2121
+ defaultValue: []
2122
+ },
2123
+ {
2124
+ name: "links",
2125
+ type: "array",
2126
+ required: false,
2127
+ description: "Linked Forge entities for context or support.",
2128
+ defaultValue: []
2129
+ }
2130
+ ]
2131
+ }),
2132
+ enrichOnboardingEntityGuide({
2133
+ entityType: "workout_session",
2134
+ purpose: "A first-class sports record with workout type, timing, optional effort or biometric detail, and linked Forge context.",
2135
+ minimumCreateFields: ["workoutType", "startedAt", "endedAt"],
2136
+ relationshipRules: [
2137
+ "Use batch CRUD for ordinary workout_session create, update, delete, and search work.",
2138
+ "The direct PATCH route remains useful for reflective enrichment after reviewing an existing imported or habit-generated workout.",
2139
+ "Workout deletions are immediate and do not go through the settings bin."
2140
+ ],
2141
+ searchHints: [
2142
+ "Search by workoutType, linked entity, or nearby timestamps before creating another manual workout."
2143
+ ],
2144
+ examples: [
2145
+ '{"workoutType":"walk","startedAt":"2026-04-11T10:00:00.000Z","endedAt":"2026-04-11T10:45:00.000Z","subjectiveEffort":6,"meaningText":"Reset after a long planning block."}'
2146
+ ],
2147
+ fieldGuide: [
2148
+ {
2149
+ name: "workoutType",
2150
+ type: "string",
2151
+ required: true,
2152
+ description: "Canonical workout label such as walk, run, ride, or mobility."
2153
+ },
2154
+ {
2155
+ name: "startedAt",
2156
+ type: "ISO datetime",
2157
+ required: true,
2158
+ description: "Workout start timestamp."
2159
+ },
2160
+ {
2161
+ name: "endedAt",
2162
+ type: "ISO datetime",
2163
+ required: true,
2164
+ description: "Workout end timestamp."
2165
+ },
2166
+ {
2167
+ name: "activeEnergyKcal",
2168
+ type: "number|null",
2169
+ required: false,
2170
+ description: "Optional active calories.",
2171
+ defaultValue: null,
2172
+ nullable: true
2173
+ },
2174
+ {
2175
+ name: "totalEnergyKcal",
2176
+ type: "number|null",
2177
+ required: false,
2178
+ description: "Optional total calories.",
2179
+ defaultValue: null,
2180
+ nullable: true
2181
+ },
2182
+ {
2183
+ name: "distanceMeters",
2184
+ type: "number|null",
2185
+ required: false,
2186
+ description: "Optional distance.",
2187
+ defaultValue: null,
2188
+ nullable: true
2189
+ },
2190
+ {
2191
+ name: "exerciseMinutes",
2192
+ type: "number|null",
2193
+ required: false,
2194
+ description: "Optional exercise minutes.",
2195
+ defaultValue: null,
2196
+ nullable: true
2197
+ },
2198
+ {
2199
+ name: "subjectiveEffort",
2200
+ type: "integer|null",
2201
+ required: false,
2202
+ description: "Optional subjective effort 1-10.",
2203
+ defaultValue: null,
2204
+ nullable: true
2205
+ },
2206
+ {
2207
+ name: "meaningText",
2208
+ type: "string",
2209
+ required: false,
2210
+ description: "Optional reflective meaning or context.",
2211
+ defaultValue: ""
2212
+ },
2213
+ {
2214
+ name: "tags",
2215
+ type: "string[]",
2216
+ required: false,
2217
+ description: "Optional workout tags.",
2218
+ defaultValue: []
2219
+ },
2220
+ {
2221
+ name: "links",
2222
+ type: "array",
2223
+ required: false,
2224
+ description: "Linked Forge entities for context or support.",
2225
+ defaultValue: []
2226
+ }
2227
+ ]
2228
+ }),
2229
+ enrichOnboardingEntityGuide({
2230
+ entityType: "preference_catalog",
2231
+ purpose: "A reusable concept list inside one preference domain, used to seed or organize comparison candidates.",
2232
+ minimumCreateFields: ["userId", "domain", "title"],
2233
+ relationshipRules: [
2234
+ "Preference catalogs are simple entities and should default to batch CRUD.",
2235
+ "Catalog items belong to one preference_catalog through catalogId."
2236
+ ],
2237
+ searchHints: [
2238
+ "Search by title and domain before creating another concept list."
2239
+ ],
2240
+ examples: [
2241
+ '{"userId":"user_operator","domain":"food","title":"Cafe shortlist"}'
2242
+ ],
2243
+ fieldGuide: [
2244
+ {
2245
+ name: "userId",
2246
+ type: "string",
2247
+ required: true,
2248
+ description: "Owner user id."
2249
+ },
2250
+ {
2251
+ name: "domain",
2252
+ type: "string",
2253
+ required: true,
2254
+ description: "Preference domain such as food, places, tools, or custom."
2255
+ },
2256
+ {
2257
+ name: "title",
2258
+ type: "string",
2259
+ required: true,
2260
+ description: "Catalog display title."
2261
+ },
2262
+ {
2263
+ name: "description",
2264
+ type: "string",
2265
+ required: false,
2266
+ description: "Optional catalog summary.",
2267
+ defaultValue: ""
2268
+ },
2269
+ {
2270
+ name: "slug",
2271
+ type: "string",
2272
+ required: false,
2273
+ description: "Optional stable slug.",
2274
+ defaultValue: ""
2275
+ }
2276
+ ]
2277
+ }),
2278
+ enrichOnboardingEntityGuide({
2279
+ entityType: "preference_catalog_item",
2280
+ purpose: "One comparable candidate inside a preference catalog.",
2281
+ minimumCreateFields: ["catalogId", "label"],
2282
+ relationshipRules: [
2283
+ "Catalog items belong to a preference_catalog and use batch CRUD.",
2284
+ "They are concept seeds, not judgments or inferred scores."
2285
+ ],
2286
+ searchHints: [
2287
+ "Search inside the catalog before creating another near-duplicate concept item."
2288
+ ],
2289
+ examples: ['{"catalogId":"preference_catalog_123","label":"Flat white"}'],
2290
+ fieldGuide: [
2291
+ {
2292
+ name: "catalogId",
2293
+ type: "string",
2294
+ required: true,
2295
+ description: "Parent catalog id."
2296
+ },
2297
+ {
2298
+ name: "label",
2299
+ type: "string",
2300
+ required: true,
2301
+ description: "Candidate label."
2302
+ },
2303
+ {
2304
+ name: "description",
2305
+ type: "string",
2306
+ required: false,
2307
+ description: "Optional description.",
2308
+ defaultValue: ""
2309
+ },
2310
+ {
2311
+ name: "tags",
2312
+ type: "string[]",
2313
+ required: false,
2314
+ description: "Optional tags.",
2315
+ defaultValue: []
2316
+ },
2317
+ {
2318
+ name: "featureWeights",
2319
+ type: "object",
2320
+ required: false,
2321
+ description: "Optional interpretable feature weight hints.",
2322
+ defaultValue: {}
2323
+ }
2324
+ ]
2325
+ }),
2326
+ enrichOnboardingEntityGuide({
2327
+ entityType: "preference_context",
2328
+ purpose: "A named preference mode such as Work, Personal, or Discovery under one user and domain.",
2329
+ minimumCreateFields: ["userId", "domain", "name"],
2330
+ relationshipRules: [
2331
+ "Preference contexts are simple entities and should default to batch CRUD.",
2332
+ "Use the merge action only when the operator explicitly wants context consolidation."
2333
+ ],
2334
+ searchHints: ["Search by name and domain before creating another context."],
2335
+ examples: [
2336
+ '{"userId":"user_operator","domain":"food","name":"Work breakfasts","shareMode":"blended"}'
2337
+ ],
2338
+ fieldGuide: [
2339
+ {
2340
+ name: "userId",
2341
+ type: "string",
2342
+ required: true,
2343
+ description: "Owner user id."
2344
+ },
2345
+ {
2346
+ name: "domain",
2347
+ type: "string",
2348
+ required: true,
2349
+ description: "Preference domain."
2350
+ },
2351
+ {
2352
+ name: "name",
2353
+ type: "string",
2354
+ required: true,
2355
+ description: "Context display name."
2356
+ },
2357
+ {
2358
+ name: "description",
2359
+ type: "string",
2360
+ required: false,
2361
+ description: "Optional summary.",
2362
+ defaultValue: ""
2363
+ },
2364
+ {
2365
+ name: "shareMode",
2366
+ type: "shared|isolated|blended",
2367
+ required: false,
2368
+ description: "How this context mixes evidence with others.",
2369
+ enumValues: ["shared", "isolated", "blended"],
2370
+ defaultValue: "blended"
2371
+ },
2372
+ {
2373
+ name: "active",
2374
+ type: "boolean",
2375
+ required: false,
2376
+ description: "Whether the context is active.",
2377
+ defaultValue: true
2378
+ }
2379
+ ]
2380
+ }),
2381
+ enrichOnboardingEntityGuide({
2382
+ entityType: "preference_item",
2383
+ purpose: "One modeled preference candidate that may stand alone or point back to another Forge entity.",
2384
+ minimumCreateFields: ["userId", "domain", "label"],
2385
+ relationshipRules: [
2386
+ "Preference items are simple entities and should default to batch CRUD.",
2387
+ "They can optionally point back to another Forge entity through sourceEntityType and sourceEntityId."
2388
+ ],
2389
+ searchHints: [
2390
+ "Search by label, domain, or linked source entity before creating another preference item."
2391
+ ],
2392
+ examples: [
2393
+ '{"userId":"user_operator","domain":"tools","label":"Mechanical keyboard"}'
2394
+ ],
2395
+ fieldGuide: [
2396
+ {
2397
+ name: "userId",
2398
+ type: "string",
2399
+ required: true,
2400
+ description: "Owner user id."
2401
+ },
2402
+ {
2403
+ name: "domain",
2404
+ type: "string",
2405
+ required: true,
2406
+ description: "Preference domain."
2407
+ },
2408
+ {
2409
+ name: "label",
2410
+ type: "string",
2411
+ required: true,
2412
+ description: "Item display label."
2413
+ },
2414
+ {
2415
+ name: "description",
2416
+ type: "string",
2417
+ required: false,
2418
+ description: "Optional description.",
2419
+ defaultValue: ""
2420
+ },
2421
+ {
2422
+ name: "sourceEntityType",
2423
+ type: "string|null",
2424
+ required: false,
2425
+ description: "Optional linked Forge entity type.",
2426
+ defaultValue: null,
2427
+ nullable: true
2428
+ },
2429
+ {
2430
+ name: "sourceEntityId",
2431
+ type: "string|null",
2432
+ required: false,
2433
+ description: "Optional linked Forge entity id.",
2434
+ defaultValue: null,
2435
+ nullable: true
2436
+ },
2437
+ {
2438
+ name: "tags",
2439
+ type: "string[]",
2440
+ required: false,
2441
+ description: "Optional tags.",
2442
+ defaultValue: []
2443
+ }
2444
+ ]
2445
+ }),
2446
+ enrichOnboardingEntityGuide({
2447
+ entityType: "questionnaire_instrument",
2448
+ purpose: "A reusable Psyche questionnaire instrument with versions, scoring rules, and provenance.",
2449
+ minimumCreateFields: [
2450
+ "title",
2451
+ "sourceClass",
2452
+ "availability",
2453
+ "isSelfReport",
2454
+ "versionLabel",
2455
+ "definition",
2456
+ "scoring",
2457
+ "provenance"
2458
+ ],
2459
+ relationshipRules: [
2460
+ "Questionnaire instruments now default to batch CRUD for normal create, update, delete, and search work.",
2461
+ "Clone, ensure draft, and publish remain specialized actions because they operate on instrument version state."
2462
+ ],
2463
+ searchHints: [
2464
+ "Search by title or key before creating a new custom instrument."
2465
+ ],
2466
+ examples: [
2467
+ '{"title":"Tiny weekly check-in","sourceClass":"secondary_verified","availability":"custom","isSelfReport":true,"versionLabel":"Draft 1","definition":{"locale":"en","instructions":"Rate how present this feels today.","completionNote":"","presentationMode":"single_question","responseStyle":"four_point_frequency","itemIds":[],"items":[],"sections":[],"pageSize":null},"scoring":{"scores":[]},"provenance":{"retrievalDate":"2026-04-06","sourceClass":"secondary_verified","scoringNotes":"","sources":[]}}'
2468
+ ],
2469
+ fieldGuide: [
2470
+ {
2471
+ name: "title",
2472
+ type: "string",
2473
+ required: true,
2474
+ description: "Instrument title."
2475
+ },
2476
+ {
2477
+ name: "sourceClass",
2478
+ type: "string",
2479
+ required: true,
2480
+ description: "Evidence or provenance class."
2481
+ },
2482
+ {
2483
+ name: "availability",
2484
+ type: "string",
2485
+ required: true,
2486
+ description: "System or custom availability mode."
2487
+ },
2488
+ {
2489
+ name: "isSelfReport",
2490
+ type: "boolean",
2491
+ required: true,
2492
+ description: "Whether the instrument is self-report."
2493
+ },
2494
+ {
2495
+ name: "versionLabel",
2496
+ type: "string",
2497
+ required: true,
2498
+ description: "Initial draft version label on create."
2499
+ },
2500
+ {
2501
+ name: "definition",
2502
+ type: "object",
2503
+ required: true,
2504
+ description: "Questionnaire definition payload."
2505
+ },
2506
+ {
2507
+ name: "scoring",
2508
+ type: "object",
2509
+ required: true,
2510
+ description: "Scoring payload."
2511
+ },
2512
+ {
2513
+ name: "provenance",
2514
+ type: "object",
2515
+ required: true,
2516
+ description: "Provenance payload."
2517
+ }
2518
+ ]
2519
+ }),
2520
+ enrichOnboardingEntityGuide({
2521
+ entityType: "task_run",
2522
+ purpose: "A live timed work session attached to a task.",
2523
+ minimumCreateFields: [],
2524
+ relationshipRules: [
2525
+ "Task runs are action-heavy records. Do not model them as ordinary CRUD entities.",
2526
+ "Start, focus, heartbeat, complete, or release them through the dedicated task-run routes."
2527
+ ],
2528
+ searchHints: [
2529
+ "Read operator context before starting or altering live work."
2530
+ ],
2531
+ fieldGuide: []
2532
+ }),
2533
+ enrichOnboardingEntityGuide({
2534
+ entityType: "questionnaire_run",
2535
+ purpose: "One user-owned answer session against a questionnaire instrument version.",
2536
+ minimumCreateFields: [],
2537
+ relationshipRules: [
2538
+ "Questionnaire runs are action-heavy records with a lifecycle of start, patch answers, and complete.",
2539
+ "Use the run routes instead of batch CRUD."
2540
+ ],
2541
+ searchHints: [
2542
+ "Read the run detail when continuing or reviewing an in-flight answer session."
2543
+ ],
2544
+ fieldGuide: []
2545
+ }),
2546
+ enrichOnboardingEntityGuide({
2547
+ entityType: "calendar_connection",
2548
+ purpose: "A stored external calendar provider connection and its selected calendars.",
2549
+ minimumCreateFields: [],
2550
+ relationshipRules: [
2551
+ "Calendar connections use specialized setup and sync flows rather than batch CRUD.",
2552
+ "Provider auth and writable Forge-calendar selection are part of the same specialized surface."
2553
+ ],
2554
+ searchHints: [
2555
+ "Read the calendar overview before changing connections or sync state."
2556
+ ],
2557
+ fieldGuide: []
2558
+ }),
2559
+ enrichOnboardingEntityGuide({
2560
+ entityType: "wiki_page",
2561
+ purpose: "A file-backed Forge wiki page or evidence page.",
2562
+ minimumCreateFields: ["title", "contentMarkdown"],
2563
+ relationshipRules: [
2564
+ "Wiki pages live on the wiki surface and use specialized page upsert routes rather than batch CRUD.",
2565
+ "Entity links remain explicit inside the page link model."
2566
+ ],
2567
+ searchHints: [
2568
+ "Search or list wiki pages before creating another page with the same topic."
2569
+ ],
2570
+ fieldGuide: [
2571
+ {
2572
+ name: "title",
2573
+ type: "string",
2574
+ required: true,
2575
+ description: "Page title."
2576
+ },
2577
+ {
2578
+ name: "contentMarkdown",
2579
+ type: "string",
2580
+ required: true,
2581
+ description: "Markdown body."
2582
+ }
2583
+ ]
2584
+ }),
2585
+ enrichOnboardingEntityGuide({
2586
+ entityType: "self_observation",
2587
+ purpose: "The note-backed Psyche self-observation calendar surface for observed events and reflections.",
2588
+ minimumCreateFields: [],
2589
+ relationshipRules: [
2590
+ "This is a read model, not a standalone CRUD entity.",
2591
+ "Mutate it by creating or updating a note with frontmatter.observedAt."
2592
+ ],
2593
+ searchHints: [
2594
+ "Read the self-observation calendar before proposing new reflected notes or edits."
2595
+ ],
2596
+ fieldGuide: []
2597
+ }),
2598
+ enrichOnboardingEntityGuide({
2599
+ entityType: "sleep_overview",
2600
+ purpose: "The read-model sleep workspace that summarizes recent sleep sessions, trends, and stage averages.",
2601
+ minimumCreateFields: [],
2602
+ relationshipRules: [
2603
+ "Use this surface for review.",
2604
+ "Create, update, delete, or search the underlying sleep_session records through batch CRUD by default."
2605
+ ],
2606
+ searchHints: [
2607
+ "Read this surface before suggesting reflective edits or health-planning follow-up."
2608
+ ],
2609
+ fieldGuide: []
2610
+ }),
2611
+ enrichOnboardingEntityGuide({
2612
+ entityType: "sports_overview",
2613
+ purpose: "The read-model sports workspace that summarizes recent workout sessions and training load.",
2614
+ minimumCreateFields: [],
2615
+ relationshipRules: [
2616
+ "Use this surface for review.",
2617
+ "Create, update, delete, or search the underlying workout_session records through batch CRUD by default."
2618
+ ],
2619
+ searchHints: [
2620
+ "Read this surface before suggesting workout reflections or recovery follow-up."
2621
+ ],
2622
+ fieldGuide: []
2623
+ })
2624
+ ];
2625
+ const AGENT_ONBOARDING_CONVERSATION_RULES = [
2626
+ "Ask only for what is missing or unclear instead of walking the user through every optional field.",
2627
+ "Start by saying what seems to matter here or what the record is becoming, then ask the next useful question.",
2628
+ "Before each question, decide the one missing thing you are trying to clarify and why it matters for the record.",
2629
+ "Use a progression of concrete example or intent, working name, purpose or meaning, placement in Forge, operational details, and linked context.",
2630
+ "Ask one to three focused questions at a time. One is usually best when the user is uncertain or emotionally loaded.",
2631
+ "One focused question is the default. Only stack a second question when both serve the same clarification job and the user is steady enough for it.",
2632
+ "If the user already answered the normal opening question, do not repeat it. Move to the next missing clarification.",
2633
+ "Do not over-therapize logistical entities. For tasks, calendar events, work blocks, timeboxes, and task runs, one brief confirming sentence plus one question is usually enough.",
2634
+ "After each substantive answer, briefly say what is becoming clearer and ask only for the next thing that still changes the record shape or usefulness.",
2635
+ "For reusable records such as tags, event types, emotion definitions, preference contexts, or questionnaires, ask what distinction or decision the record should help with before you ask for wording.",
2636
+ "When useful, help the user name, define, and connect the record in that order: offer a working label, clarify what belongs inside it, then ask about links only after the record itself feels steady.",
2637
+ "When the meaning is clearer than the wording, offer a tentative title or formulation yourself and invite correction instead of forcing the user to wordsmith alone.",
2638
+ "Before saving, briefly summarize the working formulation in the user's own language when that would reduce ambiguity.",
2639
+ "Once the record is clear enough to name, stop exploring broadly and ask only for the last structural detail that still matters.",
2640
+ "If the record is already clear enough to save, save it instead of performing a ceremonial extra question.",
2641
+ "If the user accepts the wording or record shape, move to the write instead of reopening the intake.",
2642
+ "When updating an entity, start with what is changing, what should stay true, and what prompted the update now."
2643
+ ];
2644
+ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
2645
+ {
2646
+ focus: "goal",
2647
+ openingQuestion: "What direction are you trying to keep hold of here?",
2648
+ coachingGoal: "Clarify the direction and why it matters, not just produce a title.",
2649
+ askSequence: [
2650
+ "Ask what direction or outcome the user wants to keep in view.",
2651
+ "Reflect the deeper stake in plain language before moving on.",
2652
+ "Ask why it matters now.",
2653
+ "Distinguish the goal from a project or task.",
2654
+ "Clarify horizon and status only after the meaning is clear."
2655
+ ]
2656
+ },
2657
+ {
2658
+ focus: "project",
2659
+ openingQuestion: "If this became a real project, what would you be trying to make true in your life or work?",
2660
+ coachingGoal: "Turn an intention into a bounded workstream with a clear outcome.",
2661
+ askSequence: [
2662
+ "Ask what this piece of work is trying to make true.",
2663
+ "Reflect the emerging boundary so the user can hear what is in scope.",
2664
+ "Ask what outcome would make the project feel real or complete for now.",
2665
+ "Ask which goal it belongs under.",
2666
+ "Land on a working name once the scope is clear.",
2667
+ "Clarify status, owner, and notes only after the scope is clear."
2668
+ ]
2669
+ },
2670
+ {
2671
+ focus: "strategy",
2672
+ openingQuestion: "What future state are you actually trying to arrive at with this strategy?",
2673
+ coachingGoal: "Turn a vague plan into a deliberate sequence toward a real end state.",
2674
+ askSequence: [
2675
+ "Ask what end state the strategy is trying to land.",
2676
+ "Reflect the destination in plain language so the user can correct it early.",
2677
+ "Ask which goals or projects are the true targets.",
2678
+ "Ask what the major steps or nodes are.",
2679
+ "Ask about order, dependencies, and anything that must not be skipped."
2680
+ ]
2681
+ },
2682
+ {
2683
+ focus: "task",
2684
+ openingQuestion: "What is the next concrete move here?",
2685
+ coachingGoal: "Identify the next concrete move, not just capture a vague obligation.",
2686
+ askSequence: [
2687
+ "Ask what the next concrete action is.",
2688
+ "Ask where it belongs: project, goal, both, or standalone.",
2689
+ "Ask what would make it easier to do: due date, priority, owner, or brief context."
2690
+ ]
2691
+ },
2692
+ {
2693
+ focus: "habit",
2694
+ openingQuestion: "What recurring move are you trying to strengthen or interrupt?",
2695
+ coachingGoal: "Define the recurring behavior and cadence clearly enough for honest later check-ins.",
2696
+ askSequence: [
2697
+ "Ask what the recurring behavior is in plain language.",
2698
+ "Ask whether doing it is aligned or a slip.",
2699
+ "Ask about cadence and what counts as an honest check-in in practice.",
2700
+ "Ask about links only if they will help later review."
2701
+ ]
2702
+ },
2703
+ {
2704
+ focus: "tag",
2705
+ openingQuestion: "What do you want this tag to help you notice or find again later?",
2706
+ coachingGoal: "Create a label that helps later retrieval or grouping instead of another vague bucket.",
2707
+ askSequence: [
2708
+ "Ask what the tag should help the user notice, group, or find later.",
2709
+ "Ask what kinds of records should belong under it and what should stay outside it.",
2710
+ "Offer a concise label if the grouping meaning is clearer than the wording.",
2711
+ "Ask about color, kind, or parent grouping only if that changes how the tag will be used."
2712
+ ]
2713
+ },
2714
+ {
2715
+ focus: "note",
2716
+ openingQuestion: "What about this feels worth preserving in a note?",
2717
+ coachingGoal: "Preserve the useful context and link it to the right places without turning the note into a dump.",
2718
+ askSequence: [
2719
+ "Ask what the note needs to preserve.",
2720
+ "Ask what entities it should stay attached to.",
2721
+ "Ask whether it should be durable or temporary.",
2722
+ "Ask about tags or author only if they help retrieval or handoff."
2723
+ ]
2724
+ },
2725
+ {
2726
+ focus: "wiki_page",
2727
+ openingQuestion: "What should this page become the main reference for?",
2728
+ coachingGoal: "Create a durable reference page with a clear scope instead of dumping raw notes into the wiki.",
2729
+ askSequence: [
2730
+ "Ask what topic this page should become the canonical place for.",
2731
+ "Ask whether it is a durable wiki page or supporting evidence.",
2732
+ "Ask what future lookup, decision, or collaboration this page should support.",
2733
+ "Ask about linked entities, aliases, or tags only if they will make the page more navigable later."
2734
+ ]
2735
+ },
2736
+ {
2737
+ focus: "insight",
2738
+ openingQuestion: "What is the clearest thing you want future-you or the agent to remember from this?",
2739
+ coachingGoal: "Capture one grounded observation or recommendation clearly enough that it remains useful later.",
2740
+ askSequence: [
2741
+ "Ask what pattern, tension, or observation should be remembered.",
2742
+ "Ask what entity or timeframe it belongs to, if any.",
2743
+ "Ask what recommendation, caution, or invitation should remain explicit."
2744
+ ]
2745
+ },
2746
+ {
2747
+ focus: "calendar_event",
2748
+ openingQuestion: "What is the event, and when should it happen in your local time?",
2749
+ coachingGoal: "Make the event legible as a real commitment in time, with the right timezone and links.",
2750
+ askSequence: [
2751
+ "Ask what the event is.",
2752
+ "Ask when it starts and ends in local time.",
2753
+ "Ask where it belongs or what it supports.",
2754
+ "Ask whether it should stay Forge-only only if that choice matters."
2755
+ ]
2756
+ },
2757
+ {
2758
+ focus: "work_block_template",
2759
+ openingQuestion: "What recurring block do you want to set up, and when should it repeat?",
2760
+ coachingGoal: "Define a reusable availability rule rather than a one-off event.",
2761
+ askSequence: [
2762
+ "Ask what kind of block it is and what it should be called.",
2763
+ "Ask on which days and at what local times it should repeat.",
2764
+ "Ask whether it allows or blocks work.",
2765
+ "Ask whether it has a start or end date."
2766
+ ]
2767
+ },
2768
+ {
2769
+ focus: "task_timebox",
2770
+ openingQuestion: "Which task are you trying to make time for, and when should the slot be?",
2771
+ coachingGoal: "Reserve real time for one task without confusing planned work with completed work.",
2772
+ askSequence: [
2773
+ "Ask which task the slot belongs to.",
2774
+ "Ask when the slot should start and end.",
2775
+ "Ask about source or override reason only when that context matters."
2776
+ ]
2777
+ },
2778
+ {
2779
+ focus: "calendar_connection",
2780
+ openingQuestion: "Which calendar provider are you trying to connect, and what do you want Forge to do with it?",
2781
+ coachingGoal: "Connect the right provider deliberately without turning setup into a credential dump.",
2782
+ askSequence: [
2783
+ "Ask which provider the user wants to connect and what they want Forge to do with it.",
2784
+ "Ask whether the goal is read-only visibility, writable planning, or both.",
2785
+ "Ask only for the next provider-specific step that still matters, such as auth flow, label, or calendar selection.",
2786
+ "Move into the actual connection flow once the setup goal is clear."
2787
+ ]
2788
+ },
2789
+ {
2790
+ focus: "task_run",
2791
+ openingQuestion: "Which task should I start?",
2792
+ coachingGoal: "Start truthful live work with as little friction as possible while still knowing what is being worked on and by whom.",
2793
+ askSequence: [
2794
+ "Confirm the task.",
2795
+ "Confirm the actor only if it is not already obvious.",
2796
+ "Ask whether the run should be planned or unlimited only if that changes the action.",
2797
+ "Start the run instead of turning it into a longer intake."
2798
+ ]
2799
+ },
2800
+ {
2801
+ focus: "self_observation",
2802
+ openingQuestion: "What did you notice most clearly in that moment?",
2803
+ coachingGoal: "Capture one observation clearly enough that it can support later reflection without pretending it is already a full interpretation.",
2804
+ askSequence: [
2805
+ "Ask what was observed.",
2806
+ "Reflect the moment without pretending it is already a finished interpretation.",
2807
+ "Ask when it happened or became noticeable unless timing is already clear.",
2808
+ "Ask what it may connect to: pattern, belief, value, mode, task, project, or note.",
2809
+ "Ask for tags or extra context only if that will help later review."
2810
+ ]
2811
+ },
2812
+ {
2813
+ focus: "sleep_session",
2814
+ openingQuestion: "What about this night feels important enough to remember or connect?",
2815
+ coachingGoal: "Enrich one night's record with reflective context instead of treating it like a generic note.",
2816
+ askSequence: [
2817
+ "Ask what about the night feels worth capturing.",
2818
+ "Ask whether the main point is quality, pattern, context, meaning, or links.",
2819
+ "Ask what goal, project, task, habit, or Psyche record it should stay connected to.",
2820
+ "Ask about tags only if they will help later review."
2821
+ ]
2822
+ },
2823
+ {
2824
+ focus: "workout_session",
2825
+ openingQuestion: "What about this workout feels most worth remembering or connecting?",
2826
+ coachingGoal: "Enrich one workout with subjective effort, mood, meaning, or linked context.",
2827
+ askSequence: [
2828
+ "Ask what about the session the user wants to preserve.",
2829
+ "Ask whether the key layer is effort, mood, meaning, social context, or links.",
2830
+ "Ask what it connects to in Forge if links matter.",
2831
+ "Ask about tags only if they help later retrieval."
2832
+ ]
2833
+ },
2834
+ {
2835
+ focus: "preference_catalog",
2836
+ openingQuestion: "What decision or taste question should this catalog help with?",
2837
+ coachingGoal: "Define a useful comparison pool rather than a list with no decision purpose.",
2838
+ askSequence: [
2839
+ "Ask what preference question this catalog is meant to support.",
2840
+ "Ask what domain or concept area it belongs to.",
2841
+ "Ask what kinds of items should be included or excluded.",
2842
+ "Offer a working catalog name once the purpose is clear."
2843
+ ]
2844
+ },
2845
+ {
2846
+ focus: "preference_catalog_item",
2847
+ openingQuestion: "What makes this option meaningfully worth comparing?",
2848
+ coachingGoal: "Add one candidate in a way that will make later comparisons feel clear and fair.",
2849
+ askSequence: [
2850
+ "Ask what makes this item worth including in the catalog.",
2851
+ "Ask what catalog or domain it belongs to if that is still unclear.",
2852
+ "Ask for a short clarifying description only if the label would be ambiguous later.",
2853
+ "Ask about aliases or tags only if they help retrieval."
2854
+ ]
2855
+ },
2856
+ {
2857
+ focus: "preference_context",
2858
+ openingQuestion: "In what situation should Forge treat your preferences differently here?",
2859
+ coachingGoal: "Define a real operating mode for preferences instead of a decorative label.",
2860
+ askSequence: [
2861
+ "Ask what situation or mode this context is meant to represent.",
2862
+ "Ask what decisions or comparisons should feel different inside that context.",
2863
+ "Ask what should count inside that context and what should stay outside it.",
2864
+ "Ask whether it should be active, default, or kept separate from other evidence.",
2865
+ "Offer a concise name if the mode is clearer than the wording."
2866
+ ]
2867
+ },
2868
+ {
2869
+ focus: "preference_item",
2870
+ openingQuestion: "What preference are you trying to make clearer by saving this item?",
2871
+ coachingGoal: "Save one concrete preference candidate or signal without losing the context that makes it meaningful.",
2872
+ askSequence: [
2873
+ "Ask what preference or taste question this item belongs to.",
2874
+ "Ask what domain or context it should live in.",
2875
+ "Ask whether the user is saving a comparison candidate or a direct signal such as favorite, veto, or compare-later.",
2876
+ "Ask what makes the item distinct enough to compare usefully only if it is still a comparison candidate."
2877
+ ]
2878
+ },
2879
+ {
2880
+ focus: "questionnaire_instrument",
2881
+ openingQuestion: "What would this questionnaire help someone notice or track?",
2882
+ coachingGoal: "Clarify whether the user is authoring a reusable questionnaire and what the instrument is for.",
2883
+ askSequence: [
2884
+ "Ask what the questionnaire is meant to measure or surface.",
2885
+ "Ask who it is for and when it should be used.",
2886
+ "Ask what kind of honest moment or decision it should help someone answer before getting into item wording.",
2887
+ "Reflect the practical use case back in plain language.",
2888
+ "Move to draft creation once the purpose is clear."
2889
+ ]
2890
+ },
2891
+ {
2892
+ focus: "questionnaire_run",
2893
+ openingQuestion: "Do you want to start, continue, review, or finish a questionnaire run?",
2894
+ coachingGoal: "Clarify whether the user wants to start, continue, or complete one answer session.",
2895
+ askSequence: [
2896
+ "Ask which questionnaire run this is about.",
2897
+ "Ask whether the user wants to start, continue, review, or complete it.",
2898
+ "If answering is still in progress, ask only for the next answer or note that matters."
2899
+ ]
2900
+ },
2901
+ {
2902
+ focus: "event_type",
2903
+ openingQuestion: "What kind of moment keeps happening that you want future reports to name the same way each time?",
2904
+ coachingGoal: "Create a reusable incident category that will actually help future reports stay consistent.",
2905
+ askSequence: [
2906
+ "Ask what kind of moment or incident this label should capture in lived terms.",
2907
+ "Ask how narrow or broad it should be.",
2908
+ "Ask what would count as inside versus outside the category if that boundary is still fuzzy.",
2909
+ "Ask for a short description only if the label could be ambiguous later."
2910
+ ]
2911
+ },
2912
+ {
2913
+ focus: "emotion_definition",
2914
+ openingQuestion: "When this feeling is present, what tells you it is this feeling and not a nearby one?",
2915
+ coachingGoal: "Create a reusable emotion label with enough clarity to use consistently later.",
2916
+ askSequence: [
2917
+ "Ask what this feeling is like in lived terms when the user says it.",
2918
+ "Ask what distinguishes it from nearby emotions if that matters.",
2919
+ "Ask for a broader category only if it will help later browsing or reporting."
2920
+ ]
2921
+ }
2922
+ ];
2923
+ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
2924
+ {
2925
+ focus: "psyche_value",
2926
+ useWhen: "Use for a lived direction, quality of being, or way of showing up that matters to the user and should guide actions rather than just describe an outcome.",
2927
+ coachingGoal: "Clarify the value as a chosen direction, distinguish it from a goal, and gather one concrete way the user wants to embody it now.",
2928
+ askSequence: [
2929
+ "Start with what matters and why it matters now.",
2930
+ "Ask for one concrete example of what living this value would look like in ordinary life.",
2931
+ "Separate the value direction from any specific outcome or achievement goal.",
2932
+ "Notice tensions, barriers, or situations where the value gets lost.",
2933
+ "Name one small committed action that would move toward the value."
2934
+ ],
2935
+ requiredForCreate: ["title"],
2936
+ highValueOptionalFields: [
2937
+ "description",
2938
+ "valuedDirection",
2939
+ "whyItMatters",
2940
+ "committedActions",
2941
+ "linkedGoalIds",
2942
+ "linkedProjectIds",
2943
+ "linkedTaskIds"
2944
+ ],
2945
+ exampleQuestions: [
2946
+ "What feels deeply important about this to you?",
2947
+ "If you were living this value a little more this week, what would someone be able to see?",
2948
+ "What goal or area of life does this value belong to most clearly?",
2949
+ "When this value is hard to live, what tends to get in the way?",
2950
+ "What is one small action that would express it in practice?"
2951
+ ],
2952
+ notes: [
2953
+ "Use an ACT-style values clarification stance: values are directions to live toward, not boxes to complete.",
2954
+ "Ask one or two questions at a time, reflect back the user's language, and only then move toward naming committed actions or linked work items.",
2955
+ "If the user says they want to understand it first, start with one orienting question before offering a formulation or save suggestion."
2956
+ ]
2957
+ },
2958
+ {
2959
+ focus: "behavior_pattern",
2960
+ useWhen: "Use for a recurring loop that shows up across multiple situations and can be described as cue -> response -> payoff -> cost -> preferred response.",
2961
+ coachingGoal: "Help the user build a CBT-style functional analysis with active listening instead of just naming the problem vaguely.",
2962
+ askSequence: [
2963
+ "Start from one recent concrete example before generalizing the loop.",
2964
+ "Identify the typical cue, vulnerability, or context that makes the loop more likely.",
2965
+ "Reflect back the sequence of thoughts, feelings, body state, and visible behavior once it starts.",
2966
+ "Clarify the short-term payoff, protection, or escape function.",
2967
+ "Clarify the long-term cost to the self, relationships, work, or values.",
2968
+ "Ask what a slightly more workable response would look like.",
2969
+ "Notice adjacent beliefs, schema themes, modes, or values that should be linked or saved separately."
2970
+ ],
2971
+ requiredForCreate: ["title"],
2972
+ highValueOptionalFields: [
2973
+ "description",
2974
+ "targetBehavior",
2975
+ "cueContexts",
2976
+ "shortTermPayoff",
2977
+ "longTermCost",
2978
+ "preferredResponse",
2979
+ "linkedBeliefIds",
2980
+ "linkedModeIds",
2981
+ "linkedValueIds"
2982
+ ],
2983
+ exampleQuestions: [
2984
+ "Can we slow this down using one recent example first?",
2985
+ "What usually sets this loop off, and what was going on just before it started?",
2986
+ "What do you notice in your thoughts, body, and actions once it gets going?",
2987
+ "What does that move do for you immediately?",
2988
+ "What does it cost you later?",
2989
+ "What belief, rule, or vulnerable part seems to get activated inside this loop?",
2990
+ "If this loop loosened a little, what response would you want to make instead?"
2991
+ ],
2992
+ notes: [
2993
+ "A pattern is usually the best Psyche container for functional analysis.",
2994
+ "If the user is describing one specific episode rather than a repeated loop, prefer a trigger report.",
2995
+ "Reflect before the next question, and avoid interrogating through the schema fields in order.",
2996
+ "If the user asks to understand the loop first, do not lead with a finished working diagnosis or title before asking at least one clarifying question.",
2997
+ "Before you ask how to change the loop, ask what it is protecting, preventing, or managing for the user."
2998
+ ]
2999
+ },
3000
+ {
3001
+ focus: "behavior",
3002
+ useWhen: "Use for one recurring move, coping action, or regulating action that the user wants to understand more clearly and possibly link to a broader pattern.",
3003
+ coachingGoal: "Describe the behavior in plain language, understand its function, classify whether it moves away, toward, or back into repair, and identify a more workable move when relevant.",
3004
+ askSequence: [
3005
+ "Start with a recent example of the behavior in context.",
3006
+ "Name what the user actually does or tends to do.",
3007
+ "Clarify what cues, urges, or situations pull the behavior online.",
3008
+ "Clarify the short-term payoff or relief.",
3009
+ "Clarify the long-term cost or price.",
3010
+ "Decide whether the behavior is away, committed, or recovery.",
3011
+ "Identify a replacement move or repair plan if the user wants one."
3012
+ ],
3013
+ requiredForCreate: ["kind", "title"],
3014
+ highValueOptionalFields: [
3015
+ "description",
3016
+ "commonCues",
3017
+ "urgeStory",
3018
+ "shortTermPayoff",
3019
+ "longTermCost",
3020
+ "replacementMove",
3021
+ "repairPlan",
3022
+ "linkedPatternIds",
3023
+ "linkedValueIds",
3024
+ "linkedSchemaIds",
3025
+ "linkedModeIds"
3026
+ ],
3027
+ exampleQuestions: [
2135
3028
  "What does this behavior actually look like when it happens?",
2136
3029
  "What usually pulls you toward it?",
2137
3030
  "What does it do for you in the moment?",
@@ -2331,7 +3224,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
2331
3224
  {
2332
3225
  toolName: "forge_create_entities",
2333
3226
  summary: "Create one or more entities in one ordered batch.",
2334
- whenToUse: "Use after explicit save intent and after duplicate checks when needed.",
3227
+ whenToUse: "Use after explicit save intent and after duplicate checks when needed. This is the default create path for simple Forge entities; do not spray one-off direct mutation routes when the batch contract already covers the record.",
2335
3228
  inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, clientRef?: string, data: object }> }",
2336
3229
  requiredFields: [
2337
3230
  "operations",
@@ -2342,7 +3235,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
2342
3235
  "entityType alone is never enough; full data is required.",
2343
3236
  "Batch multiple related creates together when they come from one user ask.",
2344
3237
  "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.",
2345
- "The same batch create route also handles calendar_event, work_block_template, task_timebox, preference_catalog, preference_catalog_item, preference_context, preference_item, and questionnaire_instrument.",
3238
+ "The same batch create route also handles calendar_event, work_block_template, task_timebox, sleep_session, workout_session, preference_catalog, preference_catalog_item, preference_context, preference_item, and questionnaire_instrument.",
2346
3239
  "Calendar-event creates still trigger downstream projection sync when a writable provider calendar is selected."
2347
3240
  ],
2348
3241
  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"}]}'
@@ -2350,7 +3243,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
2350
3243
  {
2351
3244
  toolName: "forge_update_entities",
2352
3245
  summary: "Patch one or more entities in one ordered batch.",
2353
- whenToUse: "Use when ids are known and the user explicitly wants a change persisted.",
3246
+ whenToUse: "Use when ids are known and the user explicitly wants a change persisted. This is the default update path for simple Forge entities, including manual health-session CRUD.",
2354
3247
  inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, id: string, clientRef?: string, patch: object }> }",
2355
3248
  requiredFields: [
2356
3249
  "operations",
@@ -2363,7 +3256,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
2363
3256
  "Project lifecycle is status-driven: patch project.status to active, paused, or completed instead of looking for separate suspend, restart, or finish routes.",
2364
3257
  "Setting project.status to completed finishes the project and auto-completes linked unfinished tasks through the normal task completion path.",
2365
3258
  "Task and project scheduling rules stay on these same entity patches. Update task.schedulingRules, task.plannedDurationSeconds, or project.schedulingRules here.",
2366
- "Use this same route to move or relink calendar_event records, edit work_block_template or task_timebox records, and do normal field updates on preference_catalog, preference_catalog_item, preference_context, preference_item, and questionnaire_instrument."
3259
+ "Use this same route to move or relink calendar_event records, edit work_block_template, task_timebox, sleep_session, or workout_session records, and do normal field updates on preference_catalog, preference_catalog_item, preference_context, preference_item, and questionnaire_instrument."
2367
3260
  ],
2368
3261
  example: '{"operations":[{"entityType":"project","id":"project_123","patch":{"status":"completed"},"clientRef":"project-finish-1"}]}'
2369
3262
  },
@@ -2381,7 +3274,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
2381
3274
  "Delete defaults to soft.",
2382
3275
  "Use mode=hard only for explicit permanent removal.",
2383
3276
  "Restoration is only possible after soft delete.",
2384
- "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."
3277
+ "calendar_event, work_block_template, task_timebox, sleep_session, and workout_session are immediate deletions: calendar events delete remote projections too, and these records do not go through the settings bin."
2385
3278
  ],
2386
3279
  example: '{"operations":[{"entityType":"task","id":"task_123","mode":"soft","reason":"Merged into another task"}]}'
2387
3280
  },
@@ -2532,7 +3425,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
2532
3425
  {
2533
3426
  toolName: "forge_update_sleep_session",
2534
3427
  summary: "Patch one sleep session with reflective notes, tags, or linked Forge context.",
2535
- whenToUse: "Use after reviewing a specific night when the operator wants richer context stored on that sleep record.",
3428
+ whenToUse: "Use after reviewing a specific night when the operator wants richer context stored on that sleep record. Do not use this as the primary CRUD path when batch entity mutation already fits the job.",
2536
3429
  inputShape: "{ sleepId: string, qualitySummary?: string, notes?: string, tags?: string[], links?: Array<{ entityType, entityId, relationshipType? }> }",
2537
3430
  requiredFields: ["sleepId"],
2538
3431
  notes: [
@@ -2544,7 +3437,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
2544
3437
  {
2545
3438
  toolName: "forge_update_workout_session",
2546
3439
  summary: "Patch one workout session with subjective effort, mood, meaning, tags, or linked Forge context.",
2547
- whenToUse: "Use after reviewing one sports session when the operator wants the workout record to carry narrative or planning context.",
3440
+ whenToUse: "Use after reviewing one sports session when the operator wants the workout record to carry narrative or planning context. Do not use this as the primary CRUD path when batch entity mutation already fits the job.",
2548
3441
  inputShape: "{ workoutId: string, subjectiveEffort?: integer|null, moodBefore?: string, moodAfter?: string, meaningText?: string, plannedContext?: string, socialContext?: string, tags?: string[], links?: Array<{ entityType, entityId, relationshipType? }> }",
2549
3442
  requiredFields: ["workoutId"],
2550
3443
  notes: [
@@ -2569,10 +3462,10 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
2569
3462
  toolName: "forge_connect_calendar_provider",
2570
3463
  summary: "Create a Forge calendar connection for Google, Apple, Exchange Online, or custom CalDAV.",
2571
3464
  whenToUse: "Use only when the operator explicitly wants Forge connected to an external calendar provider.",
2572
- 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 }',
3465
+ inputShape: '{ provider: "google"|"apple"|"caldav"|"microsoft", label: string, username?: string, password?: string, serverUrl?: string, authSessionId?: string, selectedCalendarUrls: string[], forgeCalendarUrl?: string, createForgeCalendar?: boolean }',
2573
3466
  requiredFields: ["provider", "label", "provider-specific credentials"],
2574
3467
  notes: [
2575
- "Google uses OAuth client credentials plus a refresh token.",
3468
+ "Google now uses an interactive localhost Authorization Code + PKCE flow. The user signs in interactively on the same machine running Forge, Forge exchanges the authorization code on the backend, and forge_connect_calendar_provider should only be used after a completed Google authSessionId exists.",
2576
3469
  "Apple starts from https://caldav.icloud.com and autodiscovers the principal plus calendars after authentication.",
2577
3470
  "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.",
2578
3471
  "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.",
@@ -2864,7 +3757,9 @@ function buildAgentOnboardingPayload(request) {
2864
3757
  "preference_catalog_item",
2865
3758
  "preference_context",
2866
3759
  "preference_item",
2867
- "questionnaire_instrument"
3760
+ "questionnaire_instrument",
3761
+ "sleep_session",
3762
+ "workout_session"
2868
3763
  ],
2869
3764
  batchRoutes: {
2870
3765
  search: "/api/v1/entities/search",
@@ -2873,6 +3768,19 @@ function buildAgentOnboardingPayload(request) {
2873
3768
  delete: "/api/v1/entities/delete",
2874
3769
  restore: "/api/v1/entities/restore"
2875
3770
  },
3771
+ specializedCrudEntities: {
3772
+ wiki_page: {
3773
+ create: "/api/v1/wiki/pages",
3774
+ update: "/api/v1/wiki/pages/:id",
3775
+ read: "/api/v1/wiki/pages/:id"
3776
+ },
3777
+ calendar_connection: {
3778
+ list: "/api/v1/calendar/connections",
3779
+ create: "/api/v1/calendar/connections",
3780
+ update: "/api/v1/calendar/connections/:id",
3781
+ delete: "/api/v1/calendar/connections/:id"
3782
+ }
3783
+ },
2876
3784
  actionEntities: {
2877
3785
  task_run: {
2878
3786
  readModel: "/api/v1/operator/context",
@@ -2914,16 +3822,16 @@ function buildAgentOnboardingPayload(request) {
2914
3822
  },
2915
3823
  selfObservation: {
2916
3824
  read: "/api/v1/psyche/self-observation/calendar",
2917
- writeModel: "Create or update an observed note with frontmatter.observedAt. Manual reflections usually carry the Self-observation tag, while movement sync can also publish rolling observed notes tagged movement."
2918
- },
2919
- sleep_session: {
2920
- read: "/api/v1/health/sleep",
2921
- update: "/api/v1/health/sleep/:id"
2922
- },
2923
- workout_session: {
2924
- read: "/api/v1/health/fitness",
2925
- update: "/api/v1/health/workouts/:id"
3825
+ writeModel: "Create or update an observed note with frontmatter.observedAt. Manual reflections usually carry the Self-observation tag, while movement sync can also publish rolling observed notes tagged movement."
2926
3826
  }
3827
+ },
3828
+ readModelOnlySurfaces: {
3829
+ sleepOverview: "/api/v1/health/sleep",
3830
+ sportsOverview: "/api/v1/health/fitness",
3831
+ selfObservation: "/api/v1/psyche/self-observation/calendar",
3832
+ calendarOverview: "/api/v1/calendar/overview",
3833
+ operatorOverview: "/api/v1/operator/overview",
3834
+ operatorContext: "/api/v1/operator/context"
2927
3835
  }
2928
3836
  },
2929
3837
  multiUserModel: {
@@ -3072,11 +3980,11 @@ function buildAgentOnboardingPayload(request) {
3072
3980
  saveSuggestionPlacement: "end_of_message",
3073
3981
  saveSuggestionTone: "gentle_optional",
3074
3982
  maxQuestionsPerTurn: 1,
3075
- psycheExplorationRule: "When a Psyche entity needs understanding first, begin with one exploratory question before any working formulation, replacement belief, suggested title, or save pitch. Keep the opening reflection to one or two short sentences, stay in plain prose instead of bullets or numbered lists, keep that first reply short, do not mention Forge search or save structure yet, avoid colons or list-shaped phrasing, and wait for the user's answer before offering a fuller formulation.",
3076
- psycheOpeningQuestionRule: "Prefer a concrete opening question tied to the entity: ask when the value mattered, what happened the last time the pattern appeared, what felt threatened before the behavior, what the feared outcome is inside the belief, what the mode is protecting, what the part says to do, or where the shift began in the incident.",
3983
+ psycheExplorationRule: "When a Psyche entity needs understanding first, begin with one exploratory question before any working formulation, replacement belief, suggested title, or save pitch. Keep the opening reflection to one or two short sentences, stay in plain prose instead of bullets or numbered lists, keep that first reply short, do not mention Forge search or save structure yet, avoid colons or list-shaped phrasing, prefer what/when/how over why until the experience is grounded, wait for the user's answer before offering a fuller formulation, ask permission before moving from charged exploration into naming or challenge when needed, do not widen into adjacent entities until the current one has a working sentence the user recognizes, and once the lived experience is coherent stop deepening and help the user name it cleanly. If the user accepts the wording, move toward the save instead of reopening deeper exploration.",
3984
+ psycheOpeningQuestionRule: "Prefer a concrete opening question tied to the entity: ask when the value mattered, what happened the last time the pattern appeared, what cue or body signal came first before the behavior, what the belief starts saying about self or outcome, what feels most at risk inside the mode, what the part is trying to get the user to do or stop doing, or where the shift began in the incident. Reflect briefly before the question, choose one follow-up lane at a time, say what is becoming clearer before the next deeper question, and if several Psyche entities are visible hold the adjacent ones lightly until the main container is clear.",
3077
3985
  duplicateCheckRoute: "/api/v1/entities/search",
3078
3986
  uiSuggestionRule: "offer_visual_ui_when_review_or_editing_would_be_easier",
3079
- browserFallbackRule: "Do not open the Forge UI or a browser just to create or update normal entities when the batch entity tools can do the job.",
3987
+ browserFallbackRule: "Do not open the Forge UI or a browser just to create or update normal entities when the batch entity tools can do the job. Batch CRUD is the default for simple entities; avoid spamming the agent with a large one-route-per-entity mental model.",
3080
3988
  writeConsentRule: "If an entity is only implied, keep helping in the main conversation and offer Forge lightly at the end. Only write after explicit save intent or after the user accepts the Forge save offer."
3081
3989
  },
3082
3990
  mutationGuidance: {
@@ -3089,12 +3997,12 @@ function buildAgentOnboardingPayload(request) {
3089
3997
  },
3090
3998
  deleteDefault: "soft",
3091
3999
  hardDeleteRequiresExplicitMode: true,
3092
- 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.",
3093
- 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.",
3094
- 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.",
4000
+ restoreSummary: "Restore soft-deleted entities through the restore route or the settings bin. Immediate-delete entities such as calendar_event, work_block_template, task_timebox, sleep_session, and workout_session do not enter the bin.",
4001
+ entityDeleteSummary: "Entity DELETE routes default to soft delete. Pass mode=hard only when permanent removal is intended. Immediate-delete entities skip the bin, and calendar-event deletes still remove remote projections downstream.",
4002
+ batchingRule: "forge_create_entities, forge_update_entities, forge_delete_entities, and forge_restore_entities all accept operations as arrays. Batch CRUD is the default for simple entities, so batch multiple related mutations together instead of reaching for a long list of entity-specific routes.",
3095
4003
  searchRule: "forge_search_entities accepts searches as an array. Search before create or update when duplicate risk exists.",
3096
- 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.",
3097
- 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.",
4004
+ createRule: "Each create operation must include entityType and full data. entityType alone is not enough. This includes calendar_event, work_block_template, task_timebox, sleep_session, workout_session, preference CRUD entities, and questionnaire_instrument alongside the usual planning and Psyche entities.",
4005
+ 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, and manual health-session field edits belong on the batch route by default rather than on the reflective review helpers.",
3098
4006
  createExample: '{"operations":[{"entityType":"goal","data":{"title":"Create meaningfully"},"clientRef":"goal-create-1"},{"entityType":"goal","data":{"title":"Build a beautiful family"},"clientRef":"goal-create-2"}]}',
3099
4007
  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"}]}'
3100
4008
  }
@@ -3309,6 +4217,7 @@ function buildV1Context(userIds) {
3309
4217
  const tasks = filterOwnedEntities("task", listTasks(), userIds);
3310
4218
  const habits = filterOwnedEntities("habit", listHabits(), userIds);
3311
4219
  const users = listUsers();
4220
+ const dashboard = getDashboard({ userIds });
3312
4221
  const selectedUsers = userIds && userIds.length > 0
3313
4222
  ? users.filter((user) => userIds.includes(user.id))
3314
4223
  : users;
@@ -3321,7 +4230,7 @@ function buildV1Context(userIds) {
3321
4230
  mode: "transitional-node"
3322
4231
  },
3323
4232
  metrics: buildGamificationProfile(goals, tasks, habits),
3324
- dashboard: getDashboard({ userIds }),
4233
+ dashboard,
3325
4234
  overview: getOverviewContext(new Date(), { userIds }),
3326
4235
  today: getTodayContext(new Date(), { userIds }),
3327
4236
  risk: getRiskContext(new Date(), { userIds }),
@@ -3337,7 +4246,8 @@ function buildV1Context(userIds) {
3337
4246
  selectedUsers
3338
4247
  },
3339
4248
  activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
3340
- activity: getDashboard({ userIds }).recentActivity
4249
+ activity: dashboard.recentActivity,
4250
+ lifeForce: buildLifeForcePayload(new Date(), userIds)
3341
4251
  };
3342
4252
  }
3343
4253
  function buildXpMetricsPayload() {
@@ -3614,6 +4524,7 @@ export async function buildServer(options = {}) {
3614
4524
  configureDatabaseSeeding(options.seedDemoData ?? false);
3615
4525
  await managers.migration.initialize();
3616
4526
  ensureSystemUsers();
4527
+ getSettings();
3617
4528
  const app = Fastify({
3618
4529
  logger: false,
3619
4530
  rewriteUrl: (request) => rewriteMountPath(request.url ?? "/")
@@ -3654,7 +4565,8 @@ export async function buildServer(options = {}) {
3654
4565
  }
3655
4566
  try {
3656
4567
  const interval = CronExpressionParser.parse(processor.cronExpression, {
3657
- currentDate: processor.lastRunAt && Number.isFinite(Date.parse(processor.lastRunAt))
4568
+ currentDate: processor.lastRunAt &&
4569
+ Number.isFinite(Date.parse(processor.lastRunAt))
3658
4570
  ? processor.lastRunAt
3659
4571
  : new Date(now.getTime() - 60_000).toISOString()
3660
4572
  });
@@ -3676,9 +4588,19 @@ export async function buildServer(options = {}) {
3676
4588
  }
3677
4589
  }, 30_000);
3678
4590
  cronSchedulerTimer.unref?.();
4591
+ const dataBackupTimer = setInterval(() => {
4592
+ void maybeRunAutomaticBackup().catch(() => {
4593
+ // Automatic backup sweeps should never crash the runtime loop.
4594
+ });
4595
+ }, 5 * 60 * 1000);
4596
+ dataBackupTimer.unref?.();
4597
+ void maybeRunAutomaticBackup().catch(() => {
4598
+ // Ignore startup backup failures; the Data settings surface exposes recovery.
4599
+ });
3679
4600
  app.addHook("onClose", async () => {
3680
4601
  clearInterval(diagnosticRetentionTimer);
3681
4602
  clearInterval(cronSchedulerTimer);
4603
+ clearInterval(dataBackupTimer);
3682
4604
  taskRunWatchdog?.stop();
3683
4605
  await managers.backgroundJobs.stop();
3684
4606
  });
@@ -3706,8 +4628,7 @@ export async function buildServer(options = {}) {
3706
4628
  url.startsWith("/api/v1/events/meta"));
3707
4629
  };
3708
4630
  app.addHook("onRequest", async (request) => {
3709
- request.diagnosticStartedAt =
3710
- process.hrtime.bigint();
4631
+ request.diagnosticStartedAt = process.hrtime.bigint();
3711
4632
  });
3712
4633
  app.addHook("onResponse", async (request, reply) => {
3713
4634
  const routeUrl = request.routeOptions.url || request.url;
@@ -3984,12 +4905,59 @@ export async function buildServer(options = {}) {
3984
4905
  ? {
3985
4906
  runtime: {
3986
4907
  pid: process.pid,
3987
- storageRoot: runtimeConfig.dataRoot ?? process.cwd(),
4908
+ storageRoot: getEffectiveDataRoot(),
3988
4909
  basePath: runtimeConfig.basePath
3989
4910
  }
3990
4911
  }
3991
4912
  : {})
3992
4913
  }));
4914
+ app.get("/api/v1/doctor", async (request) => {
4915
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/doctor" });
4916
+ const settings = getSettings();
4917
+ const settingsFile = getSettingsFileStatus();
4918
+ const runtime = {
4919
+ pid: process.pid,
4920
+ storageRoot: getEffectiveDataRoot(),
4921
+ dataDir: resolveDataDir(),
4922
+ databasePath: resolveDatabasePathForDataRoot(),
4923
+ basePath: runtimeConfig.basePath,
4924
+ devWebOrigin: process.env.FORGE_DEV_WEB_ORIGIN?.trim() || null
4925
+ };
4926
+ const health = buildHealthPayload(taskRunWatchdog, {
4927
+ apiVersion: "v1",
4928
+ backend: "forge-node-runtime",
4929
+ runtime
4930
+ });
4931
+ const warnings = [];
4932
+ if (!settingsFile.valid) {
4933
+ warnings.push(`forge.json is invalid at ${settingsFile.path}. Forge ignored file precedence until the JSON is repaired or rewritten.`);
4934
+ }
4935
+ if (settingsFile.syncState === "applied_file_overrides") {
4936
+ warnings.push("forge.json overrode one or more persisted database settings on this run.");
4937
+ }
4938
+ if (health.ok === false) {
4939
+ warnings.push("The task-run watchdog reported degraded health.");
4940
+ }
4941
+ return {
4942
+ doctor: {
4943
+ ok: health.ok && settingsFile.valid,
4944
+ now: new Date().toISOString(),
4945
+ runtime,
4946
+ health,
4947
+ settingsFile,
4948
+ settingsSummary: {
4949
+ themePreference: settings.themePreference,
4950
+ localePreference: settings.localePreference,
4951
+ operatorName: settings.profile.operatorName,
4952
+ maxActiveTasks: settings.execution.maxActiveTasks,
4953
+ timeAccountingMode: settings.execution.timeAccountingMode,
4954
+ psycheAuthRequired: settings.security.psycheAuthRequired,
4955
+ webAppUrl: `http://127.0.0.1:${runtimeConfig.port}${runtimeConfig.basePath}`
4956
+ },
4957
+ warnings
4958
+ }
4959
+ };
4960
+ });
3993
4961
  app.get("/api/v1/auth/operator-session", async (request, reply) => ({
3994
4962
  session: managers.session.ensureLocalOperatorSession(request.headers, reply)
3995
4963
  }));
@@ -3998,15 +4966,116 @@ export async function buildServer(options = {}) {
3998
4966
  }));
3999
4967
  app.get("/api/v1/openapi.json", async () => buildOpenApiDocument());
4000
4968
  app.get("/api/v1/context", async (request) => buildV1Context(resolveScopedUserIds(request.query)));
4969
+ app.get("/api/v1/life-force", async (request) => ({
4970
+ lifeForce: buildLifeForcePayload(new Date(), resolveScopedUserIds(request.query)),
4971
+ templates: listLifeForceTemplates(resolveLifeForceUser(resolveScopedUserIds(request.query)).id)
4972
+ }));
4973
+ app.patch("/api/v1/life-force/profile", async (request) => {
4974
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/life-force/profile" });
4975
+ const userId = resolveLifeForceUser(resolveScopedUserIds(request.query)).id;
4976
+ return {
4977
+ lifeForce: updateLifeForceProfile(userId, lifeForceProfilePatchSchema.parse(request.body ?? {})),
4978
+ actor: auth.session?.actorLabel ?? auth.actor ?? "Forge"
4979
+ };
4980
+ });
4981
+ app.put("/api/v1/life-force/templates/:weekday", async (request) => {
4982
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/life-force/templates/:weekday" });
4983
+ const weekday = Number(request.params.weekday);
4984
+ return {
4985
+ weekday,
4986
+ points: updateLifeForceTemplate(resolveLifeForceUser(resolveScopedUserIds(request.query)).id, weekday, lifeForceTemplateUpdateSchema.parse(request.body ?? {})),
4987
+ actor: auth.session?.actorLabel ?? auth.actor ?? "Forge"
4988
+ };
4989
+ });
4990
+ app.post("/api/v1/life-force/fatigue-signals", async (request) => {
4991
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/life-force/fatigue-signals" });
4992
+ return {
4993
+ lifeForce: createFatigueSignal(resolveLifeForceUser(resolveScopedUserIds(request.query)).id, fatigueSignalCreateSchema.parse(request.body ?? {})),
4994
+ actor: auth.session?.actorLabel ?? auth.actor ?? "Forge"
4995
+ };
4996
+ });
4997
+ app.get("/api/v1/knowledge-graph", async (request) => {
4998
+ const query = request.query;
4999
+ const readString = (value) => typeof value === "string" ? value.trim() : "";
5000
+ const readList = (key) => {
5001
+ const value = query[key];
5002
+ const values = Array.isArray(value) ? value : [value];
5003
+ return values
5004
+ .flatMap((entry) => (typeof entry === "string" ? entry.split(",") : []))
5005
+ .map((entry) => entry.trim())
5006
+ .filter(Boolean);
5007
+ };
5008
+ const limitRaw = readString(query.limit);
5009
+ const limit = limitRaw.length > 0 && Number.isFinite(Number(limitRaw))
5010
+ ? Math.max(1, Math.min(2000, Math.round(Number(limitRaw))))
5011
+ : null;
5012
+ return {
5013
+ graph: buildKnowledgeGraph(resolveScopedUserIds(query), {
5014
+ q: readString(query.q) || null,
5015
+ entityKinds: readList("entityKind"),
5016
+ relationKinds: readList("relationKind"),
5017
+ tags: readList("tag"),
5018
+ owners: readList("owner"),
5019
+ updatedFrom: readString(query.updatedFrom) || null,
5020
+ updatedTo: readString(query.updatedTo) || null,
5021
+ limit,
5022
+ focusNodeId: readString(query.focusNodeId) || null
5023
+ })
5024
+ };
5025
+ });
5026
+ app.get("/api/v1/knowledge-graph/focus", async (request, reply) => {
5027
+ const query = request.query;
5028
+ const entityType = typeof query.entityType === "string" ? query.entityType.trim() : "";
5029
+ const entityId = typeof query.entityId === "string" ? query.entityId.trim() : "";
5030
+ if (!entityType || !entityId) {
5031
+ reply.code(400);
5032
+ return {
5033
+ error: "entityType and entityId are required."
5034
+ };
5035
+ }
5036
+ return {
5037
+ focus: buildKnowledgeGraphFocus(entityType, entityId, resolveScopedUserIds(query))
5038
+ };
5039
+ });
4001
5040
  app.get("/api/v1/health/overview", async (request) => ({
4002
5041
  overview: getCompanionOverview(resolveScopedUserIds(request.query))
4003
5042
  }));
4004
5043
  app.get("/api/v1/health/sleep", async (request) => ({
4005
5044
  sleep: getSleepViewData(resolveScopedUserIds(request.query))
4006
5045
  }));
5046
+ app.post("/api/v1/health/sleep", async (request, reply) => {
5047
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/health/sleep" });
5048
+ const sleep = createSleepSession(createSleepSessionSchema.parse(request.body ?? {}), toActivityContext(auth));
5049
+ reply.code(201);
5050
+ return { sleep };
5051
+ });
5052
+ app.get("/api/v1/health/sleep/:id", async (request, reply) => {
5053
+ const { id } = request.params;
5054
+ const sleep = getSleepSessionById(id);
5055
+ if (!sleep) {
5056
+ reply.code(404);
5057
+ return { error: "Sleep session not found" };
5058
+ }
5059
+ return { sleep };
5060
+ });
4007
5061
  app.get("/api/v1/health/fitness", async (request) => ({
4008
5062
  fitness: getFitnessViewData(resolveScopedUserIds(request.query))
4009
5063
  }));
5064
+ app.post("/api/v1/health/workouts", async (request, reply) => {
5065
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/health/workouts" });
5066
+ const workout = createWorkoutSession(createWorkoutSessionSchema.parse(request.body ?? {}), toActivityContext(auth));
5067
+ reply.code(201);
5068
+ return { workout };
5069
+ });
5070
+ app.get("/api/v1/health/workouts/:id", async (request, reply) => {
5071
+ const { id } = request.params;
5072
+ const workout = getWorkoutSessionById(id);
5073
+ if (!workout) {
5074
+ reply.code(404);
5075
+ return { error: "Workout session not found" };
5076
+ }
5077
+ return { workout };
5078
+ });
4010
5079
  app.get("/api/v1/movement/day", async (request) => {
4011
5080
  const query = request.query;
4012
5081
  return {
@@ -4028,6 +5097,40 @@ export async function buildServer(options = {}) {
4028
5097
  app.get("/api/v1/movement/all-time", async (request) => ({
4029
5098
  movement: getMovementAllTimeSummary(resolveScopedUserIds(request.query))
4030
5099
  }));
5100
+ app.get("/api/v1/screen-time/day", async (request) => {
5101
+ const query = request.query;
5102
+ return {
5103
+ screenTime: getScreenTimeDayDetail({
5104
+ date: typeof query.date === "string" ? query.date : undefined,
5105
+ userIds: resolveScopedUserIds(query)
5106
+ })
5107
+ };
5108
+ });
5109
+ app.get("/api/v1/screen-time/month", async (request) => {
5110
+ const query = request.query;
5111
+ return {
5112
+ screenTime: getScreenTimeMonthSummary({
5113
+ month: typeof query.month === "string" ? query.month : undefined,
5114
+ userIds: resolveScopedUserIds(query)
5115
+ })
5116
+ };
5117
+ });
5118
+ app.get("/api/v1/screen-time/all-time", async (request) => ({
5119
+ screenTime: getScreenTimeAllTimeSummary(resolveScopedUserIds(request.query))
5120
+ }));
5121
+ app.get("/api/v1/screen-time/settings", async (request) => ({
5122
+ settings: getScreenTimeSettings(resolveScopedUserIds(request.query))
5123
+ }));
5124
+ app.patch("/api/v1/screen-time/settings", async (request) => {
5125
+ requireScopedAccess(request.headers, ["write"], {
5126
+ route: "/api/v1/screen-time/settings"
5127
+ });
5128
+ const userId = resolveScopedUserIds(request.query)?.[0] ??
5129
+ getDefaultUser().id;
5130
+ return {
5131
+ settings: updateScreenTimeSettings(userId, screenTimeSettingsPatchSchema.parse(request.body ?? {}))
5132
+ };
5133
+ });
4031
5134
  app.get("/api/v1/movement/timeline", async (request) => {
4032
5135
  const parsed = movementTimelineQuerySchema.parse(request.query ?? {});
4033
5136
  return {
@@ -4035,7 +5138,8 @@ export async function buildServer(options = {}) {
4035
5138
  ...parsed,
4036
5139
  userIds: parsed.userIds.length > 0
4037
5140
  ? parsed.userIds
4038
- : (resolveScopedUserIds(request.query) ?? [])
5141
+ : (resolveScopedUserIds(request.query) ??
5142
+ [])
4039
5143
  })
4040
5144
  };
4041
5145
  });
@@ -4076,6 +5180,71 @@ export async function buildServer(options = {}) {
4076
5180
  }
4077
5181
  return { place };
4078
5182
  });
5183
+ app.post("/api/v1/movement/user-boxes", async (request, reply) => {
5184
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/user-boxes" });
5185
+ const userId = resolveScopedUserIds(request.query)?.[0] ??
5186
+ getDefaultUser().id;
5187
+ reply.code(201);
5188
+ const created = createMovementUserBox({
5189
+ ...movementUserBoxCreateSchema.parse(request.body ?? {}),
5190
+ userId
5191
+ }, toActivityContext(auth));
5192
+ return {
5193
+ box: resolveMovementTimelineSegmentForBox(userId, created.id) ?? created
5194
+ };
5195
+ });
5196
+ app.post("/api/v1/movement/user-boxes/preflight", async (request) => {
5197
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/user-boxes/preflight" });
5198
+ const userId = resolveScopedUserIds(request.query)?.[0] ??
5199
+ getDefaultUser().id;
5200
+ return {
5201
+ preflight: analyzeMovementUserBoxPreflight({
5202
+ ...movementUserBoxPreflightSchema.parse(request.body ?? {}),
5203
+ userId
5204
+ })
5205
+ };
5206
+ });
5207
+ app.patch("/api/v1/movement/user-boxes/:id", async (request, reply) => {
5208
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/user-boxes/:id" });
5209
+ const userId = resolveScopedUserIds(request.query)?.[0] ??
5210
+ getDefaultUser().id;
5211
+ const { id } = request.params;
5212
+ const box = updateMovementUserBox(id, movementUserBoxPatchSchema.parse(request.body ?? {}), toActivityContext(auth), { userId });
5213
+ if (!box) {
5214
+ reply.code(404);
5215
+ return { error: "Movement user box not found" };
5216
+ }
5217
+ return {
5218
+ box: resolveMovementTimelineSegmentForBox(userId, box.id) ?? box
5219
+ };
5220
+ });
5221
+ app.delete("/api/v1/movement/user-boxes/:id", async (request, reply) => {
5222
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/user-boxes/:id" });
5223
+ const userId = resolveScopedUserIds(request.query)?.[0] ??
5224
+ getDefaultUser().id;
5225
+ const { id } = request.params;
5226
+ const result = deleteMovementUserBox(id, toActivityContext(auth), { userId });
5227
+ if (!result) {
5228
+ reply.code(404);
5229
+ return { error: "Movement user box not found" };
5230
+ }
5231
+ return result;
5232
+ });
5233
+ app.post("/api/v1/movement/automatic-boxes/:id/invalidate", async (request, reply) => {
5234
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/automatic-boxes/:id/invalidate" });
5235
+ const userId = resolveScopedUserIds(request.query)?.[0] ??
5236
+ getDefaultUser().id;
5237
+ const { id } = request.params;
5238
+ const result = invalidateAutomaticMovementBox(id, movementAutomaticBoxInvalidateSchema.parse(request.body ?? {}), toActivityContext(auth), { userId });
5239
+ if (!result) {
5240
+ reply.code(404);
5241
+ return { error: "Automatic movement box not found" };
5242
+ }
5243
+ reply.code(201);
5244
+ return {
5245
+ box: resolveMovementTimelineSegmentForBox(userId, result.box.id) ?? result.box
5246
+ };
5247
+ });
4079
5248
  app.patch("/api/v1/movement/stays/:id", async (request, reply) => {
4080
5249
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/stays/:id" });
4081
5250
  const { id } = request.params;
@@ -4173,6 +5342,23 @@ export async function buildServer(options = {}) {
4173
5342
  }
4174
5343
  return { session };
4175
5344
  });
5345
+ app.patch("/api/v1/health/pairing-sessions/:id/sources/:source", async (request, reply) => {
5346
+ requireOperatorSession(request.headers, {
5347
+ route: "/api/v1/health/pairing-sessions/:id/sources/:source"
5348
+ });
5349
+ const params = z
5350
+ .object({
5351
+ id: z.string().trim().min(1),
5352
+ source: companionSourceKeySchema
5353
+ })
5354
+ .parse(request.params ?? {});
5355
+ const session = patchCompanionPairingSourceState(params.id, params.source, patchCompanionPairingSourceStateSchema.parse(request.body ?? {}));
5356
+ if (!session) {
5357
+ reply.code(404);
5358
+ return { error: "Companion pairing session not found" };
5359
+ }
5360
+ return { session };
5361
+ });
4176
5362
  app.post("/api/v1/health/pairing-sessions/revoke-all", async (request) => {
4177
5363
  const auth = requireOperatorSession(request.headers, {
4178
5364
  route: "/api/v1/health/pairing-sessions/revoke-all"
@@ -4189,9 +5375,13 @@ export async function buildServer(options = {}) {
4189
5375
  const parsed = movementMobileBootstrapSchema.parse(request.body ?? {});
4190
5376
  const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
4191
5377
  return {
5378
+ pairingSession: getCompanionPairingSessionById(pairing.id),
4192
5379
  movement: getMovementMobileBootstrap(pairing)
4193
5380
  };
4194
5381
  });
5382
+ app.post("/api/v1/mobile/source-state", async (request) => ({
5383
+ pairingSession: updateMobileCompanionSourceState(updateMobileCompanionSourceStateSchema.parse(request.body ?? {}))
5384
+ }));
4195
5385
  app.post("/api/v1/mobile/movement/places", async (request, reply) => {
4196
5386
  const parsed = movementMobilePlaceMutationSchema.parse(request.body ?? {});
4197
5387
  const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
@@ -4218,33 +5408,90 @@ export async function buildServer(options = {}) {
4218
5408
  })
4219
5409
  };
4220
5410
  });
4221
- app.patch("/api/v1/mobile/movement/stays/:id", async (request, reply) => {
4222
- const parsed = movementMobileStayPatchSchema.parse(request.body ?? {});
5411
+ app.post("/api/v1/mobile/movement/user-boxes", async (request, reply) => {
5412
+ const parsed = movementMobileUserBoxCreateSchema.parse(request.body ?? {});
5413
+ const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
5414
+ reply.code(201);
5415
+ const created = createMovementUserBox({
5416
+ ...parsed.box,
5417
+ userId: pairing.user_id
5418
+ }, {
5419
+ actor: "Forge Companion",
5420
+ source: "system"
5421
+ });
5422
+ return {
5423
+ box: resolveMovementTimelineSegmentForBox(pairing.user_id, created.id) ?? created
5424
+ };
5425
+ });
5426
+ app.post("/api/v1/mobile/movement/user-boxes/preflight", async (request) => {
5427
+ const parsed = movementMobileUserBoxPreflightSchema.parse(request.body ?? {});
5428
+ const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
5429
+ return {
5430
+ preflight: analyzeMovementUserBoxPreflight({
5431
+ ...parsed.draft,
5432
+ userId: pairing.user_id
5433
+ })
5434
+ };
5435
+ });
5436
+ app.patch("/api/v1/mobile/movement/user-boxes/:id", async (request, reply) => {
5437
+ const parsed = movementMobileUserBoxPatchSchema.parse(request.body ?? {});
4223
5438
  const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
4224
5439
  const { id } = request.params;
4225
- const stay = updateMovementStay(id, parsed.patch, {
5440
+ const box = updateMovementUserBox(id, parsed.patch, {
4226
5441
  actor: "Forge Companion",
4227
5442
  source: "system"
4228
5443
  }, { userId: pairing.user_id });
4229
- if (!stay) {
5444
+ if (!box) {
4230
5445
  reply.code(404);
4231
- return { error: "Movement stay not found" };
5446
+ return { error: "Movement user box not found" };
4232
5447
  }
4233
- return { stay };
5448
+ return {
5449
+ box: resolveMovementTimelineSegmentForBox(pairing.user_id, box.id) ?? box
5450
+ };
4234
5451
  });
4235
- app.patch("/api/v1/mobile/movement/trips/:id", async (request, reply) => {
4236
- const parsed = movementMobileTripPatchSchema.parse(request.body ?? {});
5452
+ app.delete("/api/v1/mobile/movement/user-boxes/:id", async (request, reply) => {
5453
+ const parsed = movementMobileBootstrapSchema.parse(request.body ?? {});
4237
5454
  const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
4238
5455
  const { id } = request.params;
4239
- const trip = updateMovementTrip(id, parsed.patch, {
5456
+ const result = deleteMovementUserBox(id, {
4240
5457
  actor: "Forge Companion",
4241
5458
  source: "system"
4242
5459
  }, { userId: pairing.user_id });
4243
- if (!trip) {
5460
+ if (!result) {
4244
5461
  reply.code(404);
4245
- return { error: "Movement trip not found" };
5462
+ return { error: "Movement user box not found" };
4246
5463
  }
4247
- return { trip };
5464
+ return result;
5465
+ });
5466
+ app.post("/api/v1/mobile/movement/automatic-boxes/:id/invalidate", async (request, reply) => {
5467
+ const parsed = movementMobileAutomaticBoxInvalidateSchema.parse(request.body ?? {});
5468
+ const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
5469
+ const { id } = request.params;
5470
+ const result = invalidateAutomaticMovementBox(id, parsed.invalidate, {
5471
+ actor: "Forge Companion",
5472
+ source: "system"
5473
+ }, { userId: pairing.user_id });
5474
+ if (!result) {
5475
+ reply.code(404);
5476
+ return { error: "Automatic movement box not found" };
5477
+ }
5478
+ reply.code(201);
5479
+ return {
5480
+ box: resolveMovementTimelineSegmentForBox(pairing.user_id, result.box.id) ??
5481
+ result.box
5482
+ };
5483
+ });
5484
+ app.patch("/api/v1/mobile/movement/stays/:id", async (request, reply) => {
5485
+ reply.code(409);
5486
+ return {
5487
+ error: "Recorded stays are immutable in product UI. Create or edit a user-defined movement box instead."
5488
+ };
5489
+ });
5490
+ app.patch("/api/v1/mobile/movement/trips/:id", async (request, reply) => {
5491
+ reply.code(409);
5492
+ return {
5493
+ error: "Recorded moves are immutable in product UI. Create or edit a user-defined movement box instead."
5494
+ };
4248
5495
  });
4249
5496
  app.post("/api/v1/mobile/watch/bootstrap", async (request) => {
4250
5497
  const parsed = mobileWatchBootstrapSchema.parse(request.body ?? {});
@@ -4298,6 +5545,16 @@ export async function buildServer(options = {}) {
4298
5545
  }
4299
5546
  return { workout };
4300
5547
  });
5548
+ app.delete("/api/v1/health/workouts/:id", async (request, reply) => {
5549
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/health/workouts/:id" });
5550
+ const { id } = request.params;
5551
+ const workout = deleteWorkoutSession(id, toActivityContext(auth));
5552
+ if (!workout) {
5553
+ reply.code(404);
5554
+ return { error: "Workout session not found" };
5555
+ }
5556
+ return { workout };
5557
+ });
4301
5558
  app.patch("/api/v1/health/sleep/:id", async (request, reply) => {
4302
5559
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/health/sleep/:id" });
4303
5560
  const { id } = request.params;
@@ -4308,6 +5565,16 @@ export async function buildServer(options = {}) {
4308
5565
  }
4309
5566
  return { sleep };
4310
5567
  });
5568
+ app.delete("/api/v1/health/sleep/:id", async (request, reply) => {
5569
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/health/sleep/:id" });
5570
+ const { id } = request.params;
5571
+ const sleep = deleteSleepSession(id, toActivityContext(auth));
5572
+ if (!sleep) {
5573
+ reply.code(404);
5574
+ return { error: "Sleep session not found" };
5575
+ }
5576
+ return { sleep };
5577
+ });
4311
5578
  app.get("/api/v1/operator/context", async (request) => {
4312
5579
  requireOperatorSession(request.headers, {
4313
5580
  route: "/api/v1/operator/context"
@@ -4355,6 +5622,26 @@ export async function buildServer(options = {}) {
4355
5622
  const userIds = resolveScopedUserIds(request.query);
4356
5623
  return getQuestionnaireInstrumentDetail(id, { userIds });
4357
5624
  });
5625
+ app.patch("/api/v1/psyche/questionnaires/:id", async (request, reply) => {
5626
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/questionnaires/:id" });
5627
+ const { id } = request.params;
5628
+ const instrument = updateQuestionnaireInstrument(id, updateQuestionnaireInstrumentSchema.parse(request.body ?? {}), toActivityContext(auth));
5629
+ if (!instrument) {
5630
+ reply.code(404);
5631
+ return { error: "Questionnaire instrument not found" };
5632
+ }
5633
+ return { instrument };
5634
+ });
5635
+ app.delete("/api/v1/psyche/questionnaires/:id", async (request, reply) => {
5636
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/questionnaires/:id" });
5637
+ const { id } = request.params;
5638
+ const instrument = deleteQuestionnaireInstrument(id, toActivityContext(auth));
5639
+ if (!instrument) {
5640
+ reply.code(404);
5641
+ return { error: "Questionnaire instrument not found" };
5642
+ }
5643
+ return { instrument };
5644
+ });
4358
5645
  app.post("/api/v1/psyche/questionnaires/:id/clone", async (request, reply) => {
4359
5646
  const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/questionnaires/:id/clone" });
4360
5647
  const { id } = request.params;
@@ -4420,6 +5707,29 @@ export async function buildServer(options = {}) {
4420
5707
  })
4421
5708
  };
4422
5709
  });
5710
+ app.get("/api/v1/psyche/self-observation/calendar/export", async (request, reply) => {
5711
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/self-observation/calendar/export" });
5712
+ const query = psycheObservationCalendarExportQuerySchema.parse(request.query ?? {});
5713
+ const now = new Date();
5714
+ const from = query.from ??
5715
+ new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
5716
+ const to = query.to ??
5717
+ new Date(now.getTime() + 21 * 24 * 60 * 60 * 1000).toISOString();
5718
+ const exported = exportPsycheObservationCalendar({
5719
+ from,
5720
+ to,
5721
+ userIds: query.userIds,
5722
+ format: query.format,
5723
+ tags: query.tags,
5724
+ includeObservations: query.includeObservations,
5725
+ includeActivity: query.includeActivity,
5726
+ onlyHumanOwned: query.onlyHumanOwned,
5727
+ search: query.search
5728
+ });
5729
+ reply.header("Content-Disposition", `attachment; filename="${exported.fileName}"`);
5730
+ reply.type(exported.mimeType);
5731
+ return reply.send(exported.body);
5732
+ });
4423
5733
  app.get("/api/v1/psyche/values", async (request) => {
4424
5734
  requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/values" });
4425
5735
  const userIds = resolveScopedUserIds(request.query);
@@ -5329,6 +6639,7 @@ export async function buildServer(options = {}) {
5329
6639
  plannedDurationSeconds: null,
5330
6640
  schedulingRules: null,
5331
6641
  tagIds: readStringArrayField(suggestedFields, "tagIds"),
6642
+ actionCostBand: "standard",
5332
6643
  notes: []
5333
6644
  }, toActivityContext(auth));
5334
6645
  return { entityType: "task", entityId: task.id };
@@ -5557,7 +6868,9 @@ export async function buildServer(options = {}) {
5557
6868
  return job;
5558
6869
  });
5559
6870
  app.post("/api/v1/wiki/ingest-jobs/:id/rerun", async (request, reply) => {
5560
- requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/rerun" });
6871
+ requireScopedAccess(request.headers, ["write"], {
6872
+ route: "/api/v1/wiki/ingest-jobs/:id/rerun"
6873
+ });
5561
6874
  const { id } = request.params;
5562
6875
  try {
5563
6876
  const result = await rerunWikiIngestJob(id, {
@@ -5584,7 +6897,9 @@ export async function buildServer(options = {}) {
5584
6897
  }
5585
6898
  });
5586
6899
  app.post("/api/v1/wiki/ingest-jobs/:id/resume", async (request, reply) => {
5587
- requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/resume" });
6900
+ requireScopedAccess(request.headers, ["write"], {
6901
+ route: "/api/v1/wiki/ingest-jobs/:id/resume"
6902
+ });
5588
6903
  const { id } = request.params;
5589
6904
  const job = getWikiIngestJob(id);
5590
6905
  if (!job) {
@@ -5614,7 +6929,9 @@ export async function buildServer(options = {}) {
5614
6929
  };
5615
6930
  });
5616
6931
  app.delete("/api/v1/wiki/ingest-jobs/:id", async (request, reply) => {
5617
- requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id" });
6932
+ requireScopedAccess(request.headers, ["write"], {
6933
+ route: "/api/v1/wiki/ingest-jobs/:id"
6934
+ });
5618
6935
  const { id } = request.params;
5619
6936
  try {
5620
6937
  const deleted = deleteWikiIngestJob(id);
@@ -5711,6 +7028,37 @@ export async function buildServer(options = {}) {
5711
7028
  providers: listCalendarProviderMetadata(),
5712
7029
  connections: listConnectedCalendarConnections()
5713
7030
  }));
7031
+ app.post("/api/v1/calendar/oauth/google/start", async (request) => {
7032
+ requireScopedAccess(request.headers, ["write"], {
7033
+ route: "/api/v1/calendar/oauth/google/start"
7034
+ });
7035
+ return await startGoogleCalendarOauth(startGoogleCalendarOauthSchema.parse(request.body ?? {}), {
7036
+ browserOrigin: typeof request.body
7037
+ ?.browserOrigin === "string"
7038
+ ? request.body.browserOrigin
7039
+ : null,
7040
+ openerOrigin: typeof request.headers.origin === "string"
7041
+ ? request.headers.origin
7042
+ : typeof request.headers.referer === "string"
7043
+ ? request.headers.referer
7044
+ : null,
7045
+ requestBaseOrigin: getRequestOrigin(request)
7046
+ });
7047
+ });
7048
+ app.get("/api/v1/calendar/oauth/google/session/:id", async (request, reply) => {
7049
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/oauth/google/session/:id" });
7050
+ try {
7051
+ return getGoogleCalendarOauthSession(request.params.id);
7052
+ }
7053
+ catch (error) {
7054
+ if (error instanceof Error &&
7055
+ error.message.startsWith("Unknown Google calendar auth session")) {
7056
+ reply.code(404);
7057
+ return { error: "Google calendar auth session not found" };
7058
+ }
7059
+ throw error;
7060
+ }
7061
+ });
5714
7062
  app.post("/api/v1/calendar/oauth/microsoft/start", async (request) => {
5715
7063
  requireScopedAccess(request.headers, ["write"], {
5716
7064
  route: "/api/v1/calendar/oauth/microsoft/start"
@@ -5739,9 +7087,57 @@ export async function buildServer(options = {}) {
5739
7087
  throw error;
5740
7088
  }
5741
7089
  });
5742
- app.get("/api/v1/calendar/oauth/microsoft/callback", async (request, reply) => {
7090
+ app.get("/api/v1/calendar/oauth/microsoft/callback", async (request, reply) => {
7091
+ const query = request.query;
7092
+ const result = await completeMicrosoftCalendarOauth({
7093
+ state: query.state ?? null,
7094
+ code: query.code ?? null,
7095
+ error: query.error ?? null,
7096
+ errorDescription: query.error_description ?? null
7097
+ });
7098
+ const session = result.session;
7099
+ const escapedOrigin = JSON.stringify(result.openerOrigin || "*");
7100
+ const escapedMessage = JSON.stringify({
7101
+ type: "forge:microsoft-calendar-auth",
7102
+ sessionId: session.sessionId,
7103
+ status: session.status
7104
+ });
7105
+ const body = `<!doctype html>
7106
+ <html lang="en">
7107
+ <head>
7108
+ <meta charset="utf-8" />
7109
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7110
+ <title>Forge Microsoft sign-in</title>
7111
+ <style>
7112
+ body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;background:#0b1320;color:#f8fafc;display:grid;place-items:center;min-height:100vh}
7113
+ 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))}
7114
+ h1{margin:0 0 .75rem;font-size:1.15rem}
7115
+ p{margin:0;color:rgba(248,250,252,.72);line-height:1.6}
7116
+ </style>
7117
+ </head>
7118
+ <body>
7119
+ <main>
7120
+ <h1>${session.status === "authorized" ? "Microsoft account connected" : "Microsoft sign-in needs attention"}</h1>
7121
+ <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>
7122
+ </main>
7123
+ <script>
7124
+ const message = ${escapedMessage};
7125
+ const targetOrigin = ${escapedOrigin};
7126
+ try {
7127
+ if (window.opener && !window.opener.closed) {
7128
+ window.opener.postMessage(message, targetOrigin);
7129
+ }
7130
+ } catch {}
7131
+ setTimeout(() => window.close(), 180);
7132
+ </script>
7133
+ </body>
7134
+ </html>`;
7135
+ reply.type("text/html; charset=utf-8");
7136
+ return body;
7137
+ });
7138
+ app.get("/api/v1/calendar/oauth/google/callback", async (request, reply) => {
5743
7139
  const query = request.query;
5744
- const result = await completeMicrosoftCalendarOauth({
7140
+ const result = await completeGoogleCalendarOauth({
5745
7141
  state: query.state ?? null,
5746
7142
  code: query.code ?? null,
5747
7143
  error: query.error ?? null,
@@ -5750,7 +7146,7 @@ export async function buildServer(options = {}) {
5750
7146
  const session = result.session;
5751
7147
  const escapedOrigin = JSON.stringify(result.openerOrigin || "*");
5752
7148
  const escapedMessage = JSON.stringify({
5753
- type: "forge:microsoft-calendar-auth",
7149
+ type: "forge:google-calendar-auth",
5754
7150
  sessionId: session.sessionId,
5755
7151
  status: session.status
5756
7152
  });
@@ -5759,18 +7155,18 @@ export async function buildServer(options = {}) {
5759
7155
  <head>
5760
7156
  <meta charset="utf-8" />
5761
7157
  <meta name="viewport" content="width=device-width, initial-scale=1" />
5762
- <title>Forge Microsoft sign-in</title>
7158
+ <title>Forge Google sign-in</title>
5763
7159
  <style>
5764
7160
  body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;background:#0b1320;color:#f8fafc;display:grid;place-items:center;min-height:100vh}
5765
- 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))}
7161
+ main{max-width:30rem;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))}
5766
7162
  h1{margin:0 0 .75rem;font-size:1.15rem}
5767
7163
  p{margin:0;color:rgba(248,250,252,.72);line-height:1.6}
5768
7164
  </style>
5769
7165
  </head>
5770
7166
  <body>
5771
7167
  <main>
5772
- <h1>${session.status === "authorized" ? "Microsoft account connected" : "Microsoft sign-in needs attention"}</h1>
5773
- <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>
7168
+ <h1>${session.status === "authorized" ? "Google account connected" : "Google sign-in needs attention"}</h1>
7169
+ <p>${session.status === "authorized" ? "Forge received your Google account and sent the result back to the calendar setup flow. You can close this window." : (session.error ?? "Forge could not complete Google sign-in. You can close this window and try again from Settings.")}</p>
5774
7170
  </main>
5775
7171
  <script>
5776
7172
  const message = ${escapedMessage};
@@ -5916,6 +7312,9 @@ export async function buildServer(options = {}) {
5916
7312
  workspace: startPreferenceGame(startPreferenceGameSchema.parse(request.body ?? {}))
5917
7313
  };
5918
7314
  });
7315
+ app.get("/api/v1/preferences/catalogs", async () => ({
7316
+ catalogs: listPreferenceCatalogs()
7317
+ }));
5919
7318
  app.post("/api/v1/preferences/catalogs", async (request, reply) => {
5920
7319
  requireScopedAccess(request.headers, ["write"], {
5921
7320
  route: "/api/v1/preferences/catalogs"
@@ -5924,6 +7323,15 @@ export async function buildServer(options = {}) {
5924
7323
  reply.code(201);
5925
7324
  return { catalog };
5926
7325
  });
7326
+ app.get("/api/v1/preferences/catalogs/:id", async (request, reply) => {
7327
+ const { id } = request.params;
7328
+ const catalog = getPreferenceCatalogById(id);
7329
+ if (!catalog) {
7330
+ reply.code(404);
7331
+ return { error: "Preferences catalog not found" };
7332
+ }
7333
+ return { catalog };
7334
+ });
5927
7335
  app.patch("/api/v1/preferences/catalogs/:id", async (request) => {
5928
7336
  requireScopedAccess(request.headers, ["write"], {
5929
7337
  route: "/api/v1/preferences/catalogs/:id"
@@ -5940,6 +7348,9 @@ export async function buildServer(options = {}) {
5940
7348
  const { id } = request.params;
5941
7349
  return { catalog: deletePreferenceCatalog(id) };
5942
7350
  });
7351
+ app.get("/api/v1/preferences/catalog-items", async () => ({
7352
+ items: listPreferenceCatalogItems()
7353
+ }));
5943
7354
  app.post("/api/v1/preferences/catalog-items", async (request, reply) => {
5944
7355
  requireScopedAccess(request.headers, ["write"], {
5945
7356
  route: "/api/v1/preferences/catalog-items"
@@ -5948,6 +7359,15 @@ export async function buildServer(options = {}) {
5948
7359
  reply.code(201);
5949
7360
  return { item };
5950
7361
  });
7362
+ app.get("/api/v1/preferences/catalog-items/:id", async (request, reply) => {
7363
+ const { id } = request.params;
7364
+ const item = getPreferenceCatalogItemById(id);
7365
+ if (!item) {
7366
+ reply.code(404);
7367
+ return { error: "Preferences catalog item not found" };
7368
+ }
7369
+ return { item };
7370
+ });
5951
7371
  app.patch("/api/v1/preferences/catalog-items/:id", async (request) => {
5952
7372
  requireScopedAccess(request.headers, ["write"], {
5953
7373
  route: "/api/v1/preferences/catalog-items/:id"
@@ -5964,6 +7384,9 @@ export async function buildServer(options = {}) {
5964
7384
  const { id } = request.params;
5965
7385
  return { item: deletePreferenceCatalogItem(id) };
5966
7386
  });
7387
+ app.get("/api/v1/preferences/contexts", async () => ({
7388
+ contexts: listPreferenceContexts()
7389
+ }));
5967
7390
  app.post("/api/v1/preferences/contexts", async (request, reply) => {
5968
7391
  requireScopedAccess(request.headers, ["write"], {
5969
7392
  route: "/api/v1/preferences/contexts"
@@ -5972,6 +7395,15 @@ export async function buildServer(options = {}) {
5972
7395
  reply.code(201);
5973
7396
  return { context };
5974
7397
  });
7398
+ app.get("/api/v1/preferences/contexts/:id", async (request, reply) => {
7399
+ const { id } = request.params;
7400
+ const context = getPreferenceContextById(id);
7401
+ if (!context) {
7402
+ reply.code(404);
7403
+ return { error: "Preferences context not found" };
7404
+ }
7405
+ return { context };
7406
+ });
5975
7407
  app.patch("/api/v1/preferences/contexts/:id", async (request) => {
5976
7408
  requireScopedAccess(request.headers, ["write"], {
5977
7409
  route: "/api/v1/preferences/contexts/:id"
@@ -5981,6 +7413,13 @@ export async function buildServer(options = {}) {
5981
7413
  context: updatePreferenceContext(id, updatePreferenceContextSchema.parse(request.body ?? {}))
5982
7414
  };
5983
7415
  });
7416
+ app.delete("/api/v1/preferences/contexts/:id", async (request) => {
7417
+ requireScopedAccess(request.headers, ["write"], {
7418
+ route: "/api/v1/preferences/contexts/:id"
7419
+ });
7420
+ const { id } = request.params;
7421
+ return { context: deletePreferenceContext(id) };
7422
+ });
5984
7423
  app.post("/api/v1/preferences/contexts/merge", async (request) => {
5985
7424
  requireScopedAccess(request.headers, ["write"], {
5986
7425
  route: "/api/v1/preferences/contexts/merge"
@@ -5989,6 +7428,9 @@ export async function buildServer(options = {}) {
5989
7428
  merge: mergePreferenceContexts(mergePreferenceContextsSchema.parse(request.body ?? {}))
5990
7429
  };
5991
7430
  });
7431
+ app.get("/api/v1/preferences/items", async () => ({
7432
+ items: listPreferenceItems()
7433
+ }));
5992
7434
  app.post("/api/v1/preferences/items", async (request, reply) => {
5993
7435
  requireScopedAccess(request.headers, ["write"], {
5994
7436
  route: "/api/v1/preferences/items"
@@ -5997,6 +7439,15 @@ export async function buildServer(options = {}) {
5997
7439
  reply.code(201);
5998
7440
  return { item };
5999
7441
  });
7442
+ app.get("/api/v1/preferences/items/:id", async (request, reply) => {
7443
+ const { id } = request.params;
7444
+ const item = getPreferenceItemById(id);
7445
+ if (!item) {
7446
+ reply.code(404);
7447
+ return { error: "Preferences item not found" };
7448
+ }
7449
+ return { item };
7450
+ });
6000
7451
  app.patch("/api/v1/preferences/items/:id", async (request) => {
6001
7452
  requireScopedAccess(request.headers, ["write"], {
6002
7453
  route: "/api/v1/preferences/items/:id"
@@ -6006,6 +7457,13 @@ export async function buildServer(options = {}) {
6006
7457
  item: updatePreferenceItem(id, updatePreferenceItemSchema.parse(request.body ?? {}))
6007
7458
  };
6008
7459
  });
7460
+ app.delete("/api/v1/preferences/items/:id", async (request) => {
7461
+ requireScopedAccess(request.headers, ["write"], {
7462
+ route: "/api/v1/preferences/items/:id"
7463
+ });
7464
+ const { id } = request.params;
7465
+ return { item: deletePreferenceItem(id) };
7466
+ });
6009
7467
  app.post("/api/v1/preferences/items/from-entity", async (request, reply) => {
6010
7468
  requireScopedAccess(request.headers, ["write"], {
6011
7469
  route: "/api/v1/preferences/items/from-entity"
@@ -6321,6 +7779,59 @@ export async function buildServer(options = {}) {
6321
7779
  requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/settings/bin" });
6322
7780
  return { bin: getSettingsBinPayload() };
6323
7781
  });
7782
+ app.get("/api/v1/settings/data", async (request) => {
7783
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/settings/data" });
7784
+ return { data: await getDataManagementState() };
7785
+ });
7786
+ app.patch("/api/v1/settings/data", async (request) => {
7787
+ requireScopedAccess(request.headers, ["write"], {
7788
+ route: "/api/v1/settings/data"
7789
+ });
7790
+ return {
7791
+ settings: await updateDataManagementSettings(updateDataManagementSettingsSchema.parse(request.body ?? {})),
7792
+ data: await getDataManagementState()
7793
+ };
7794
+ });
7795
+ app.post("/api/v1/settings/data/scan", async (request) => {
7796
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/settings/data/scan" });
7797
+ return { candidates: await scanForDataRecoveryCandidates() };
7798
+ });
7799
+ app.post("/api/v1/settings/data/backups", async (request, reply) => {
7800
+ requireScopedAccess(request.headers, ["write"], {
7801
+ route: "/api/v1/settings/data/backups"
7802
+ });
7803
+ const backup = await createDataBackup(createDataBackupSchema.parse(request.body ?? {}));
7804
+ reply.code(201);
7805
+ return {
7806
+ backup,
7807
+ data: await getDataManagementState()
7808
+ };
7809
+ });
7810
+ app.post("/api/v1/settings/data/backups/:id/restore", async (request) => {
7811
+ requireScopedAccess(request.headers, ["write"], {
7812
+ route: "/api/v1/settings/data/backups/:id/restore"
7813
+ });
7814
+ const { id } = request.params;
7815
+ return {
7816
+ data: await restoreDataBackup(id, restoreDataBackupSchema.parse(request.body ?? {}), { secretsManager: managers.secrets })
7817
+ };
7818
+ });
7819
+ app.post("/api/v1/settings/data/switch-root", async (request) => {
7820
+ requireScopedAccess(request.headers, ["write"], {
7821
+ route: "/api/v1/settings/data/switch-root"
7822
+ });
7823
+ return {
7824
+ data: await switchDataRoot(switchDataRootSchema.parse(request.body ?? {}), { secretsManager: managers.secrets })
7825
+ };
7826
+ });
7827
+ app.get("/api/v1/settings/data/export", async (request, reply) => {
7828
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/settings/data/export" });
7829
+ const query = dataExportQuerySchema.parse(request.query ?? {});
7830
+ const exported = await exportData(query.format);
7831
+ reply.header("Content-Disposition", `attachment; filename="${exported.fileName}"`);
7832
+ reply.type(exported.mimeType);
7833
+ return reply.send(exported.body);
7834
+ });
6324
7835
  app.post("/api/v1/projects", async (request, reply) => {
6325
7836
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/projects" });
6326
7837
  const project = createProject(createProjectSchema.parse(request.body ?? {}), toActivityContext(auth));
@@ -6409,6 +7920,15 @@ export async function buildServer(options = {}) {
6409
7920
  userIds: resolveScopedUserIds(request.query)
6410
7921
  })
6411
7922
  }));
7923
+ app.get("/api/v1/calendar/work-block-templates/:id", async (request, reply) => {
7924
+ const { id } = request.params;
7925
+ const template = getWorkBlockTemplateById(id);
7926
+ if (!template) {
7927
+ reply.code(404);
7928
+ return { error: "Work block template not found" };
7929
+ }
7930
+ return { template };
7931
+ });
6412
7932
  app.post("/api/v1/calendar/work-block-templates", async (request, reply) => {
6413
7933
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/work-block-templates" });
6414
7934
  const template = createWorkBlockTemplate(createWorkBlockTemplateSchema.parse(request.body ?? {}));
@@ -6484,6 +8004,15 @@ export async function buildServer(options = {}) {
6484
8004
  timeboxes: listTaskTimeboxes({ from, to, userIds: query.userIds })
6485
8005
  };
6486
8006
  });
8007
+ app.get("/api/v1/calendar/timeboxes/:id", async (request, reply) => {
8008
+ const { id } = request.params;
8009
+ const timebox = getTaskTimeboxById(id);
8010
+ if (!timebox) {
8011
+ reply.code(404);
8012
+ return { error: "Task timebox not found" };
8013
+ }
8014
+ return { timebox };
8015
+ });
6487
8016
  app.post("/api/v1/calendar/timeboxes", async (request, reply) => {
6488
8017
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/timeboxes" });
6489
8018
  const timebox = createTaskTimebox(createTaskTimeboxSchema.parse(request.body ?? {}));
@@ -6558,6 +8087,17 @@ export async function buildServer(options = {}) {
6558
8087
  })
6559
8088
  };
6560
8089
  });
8090
+ app.get("/api/v1/calendar/events", async (request) => {
8091
+ const query = calendarOverviewQuerySchema.parse(request.query ?? {});
8092
+ const now = new Date();
8093
+ const from = query.from ??
8094
+ new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
8095
+ const to = query.to ??
8096
+ new Date(now.getTime() + 21 * 24 * 60 * 60 * 1000).toISOString();
8097
+ return {
8098
+ events: listCalendarEvents({ from, to, userIds: query.userIds })
8099
+ };
8100
+ });
6561
8101
  app.post("/api/v1/calendar/events", async (request, reply) => {
6562
8102
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/events" });
6563
8103
  const event = createCalendarEvent(createCalendarEventSchema.parse(request.body ?? {}));
@@ -6579,6 +8119,15 @@ export async function buildServer(options = {}) {
6579
8119
  reply.code(201);
6580
8120
  return { event: refreshed };
6581
8121
  });
8122
+ app.get("/api/v1/calendar/events/:id", async (request, reply) => {
8123
+ const { id } = request.params;
8124
+ const event = getCalendarEventById(id);
8125
+ if (!event) {
8126
+ reply.code(404);
8127
+ return { error: "Calendar event not found" };
8128
+ }
8129
+ return { event };
8130
+ });
6582
8131
  app.patch("/api/v1/calendar/events/:id", async (request, reply) => {
6583
8132
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/events/:id" });
6584
8133
  const { id } = request.params;
@@ -6750,6 +8299,7 @@ export async function buildServer(options = {}) {
6750
8299
  }
6751
8300
  }, { secrets: managers.secrets });
6752
8301
  }
8302
+ mirrorSettingsFileFromCurrentState();
6753
8303
  reply.code(201);
6754
8304
  return { connection };
6755
8305
  });
@@ -6760,6 +8310,7 @@ export async function buildServer(options = {}) {
6760
8310
  reply.code(404);
6761
8311
  return { error: "AI model connection not found" };
6762
8312
  }
8313
+ mirrorSettingsFileFromCurrentState();
6763
8314
  return { deletedId };
6764
8315
  });
6765
8316
  app.post("/api/v1/settings/models/connections/test", async (request, reply) => {
@@ -6790,7 +8341,9 @@ export async function buildServer(options = {}) {
6790
8341
  recordDiagnosticLog({
6791
8342
  level,
6792
8343
  source: normalizeDiagnosticSource(request.headers["x-forge-source"]),
6793
- scope: typeof details.scope === "string" ? details.scope : "model_settings",
8344
+ scope: typeof details.scope === "string"
8345
+ ? details.scope
8346
+ : "model_settings",
6794
8347
  eventKey: typeof details.eventKey === "string"
6795
8348
  ? details.eventKey
6796
8349
  : "model_connection_test",
@@ -6826,7 +8379,9 @@ export async function buildServer(options = {}) {
6826
8379
  }
6827
8380
  });
6828
8381
  app.post("/api/v1/settings/models/oauth/openai-codex/session/:id/manual", async (request) => {
6829
- requireScopedAccess(request.headers, ["write"], { route: "/api/v1/settings/models/oauth/openai-codex/session/:id/manual" });
8382
+ requireScopedAccess(request.headers, ["write"], {
8383
+ route: "/api/v1/settings/models/oauth/openai-codex/session/:id/manual"
8384
+ });
6830
8385
  return {
6831
8386
  session: submitOpenAiCodexOauthManualInput(request.params.id, submitOpenAiCodexOauthManualCodeSchema.parse(request.body ?? {})
6832
8387
  .codeOrUrl)
@@ -6959,132 +8514,265 @@ export async function buildServer(options = {}) {
6959
8514
  secrets: managers.secrets
6960
8515
  }, { trigger: "route" });
6961
8516
  });
6962
- app.get("/api/v1/ai-connectors/catalog/boxes", async (request) => {
6963
- requireScopedAccess(request.headers, ["read"], {
6964
- route: "/api/v1/ai-connectors/catalog/boxes"
8517
+ const registerFlowApiRoutes = (basePath, noun, options) => {
8518
+ const collectionKey = options?.collectionKey ?? "connectors";
8519
+ const singularKey = options?.singularKey ?? "connector";
8520
+ const catalogPath = options?.catalogPath ?? `${basePath}/catalog/boxes`;
8521
+ app.get(catalogPath, async (request) => {
8522
+ requireScopedAccess(request.headers, ["read"], {
8523
+ route: catalogPath
8524
+ });
8525
+ return {
8526
+ boxes: [
8527
+ ...listForgeBoxCatalog(),
8528
+ ...listAiConnectors().flatMap((connector) => connector.publishedOutputs.map((output) => buildConnectorOutputCatalogEntry({
8529
+ connectorId: connector.id,
8530
+ title: connector.title,
8531
+ outputId: output.id
8532
+ })))
8533
+ ]
8534
+ };
6965
8535
  });
6966
- return {
6967
- boxes: listForgeBoxCatalog()
6968
- };
6969
- });
6970
- app.get("/api/v1/ai-connectors", async (request) => {
6971
- requireScopedAccess(request.headers, ["read"], {
6972
- route: "/api/v1/ai-connectors"
8536
+ app.get(basePath, async (request) => {
8537
+ requireScopedAccess(request.headers, ["read"], {
8538
+ route: basePath
8539
+ });
8540
+ return {
8541
+ [collectionKey]: listAiConnectors()
8542
+ };
6973
8543
  });
6974
- return {
6975
- connectors: listAiConnectors()
6976
- };
6977
- });
6978
- app.post("/api/v1/ai-connectors", async (request, reply) => {
6979
- requireScopedAccess(request.headers, ["write"], {
6980
- route: "/api/v1/ai-connectors"
8544
+ app.post(basePath, async (request, reply) => {
8545
+ requireScopedAccess(request.headers, ["write"], {
8546
+ route: basePath
8547
+ });
8548
+ const connector = createAiConnector(createAiConnectorSchema.parse(request.body ?? {}));
8549
+ reply.code(201);
8550
+ return { [singularKey]: connector };
6981
8551
  });
6982
- const connector = createAiConnector(createAiConnectorSchema.parse(request.body ?? {}));
6983
- reply.code(201);
6984
- return { connector };
6985
- });
6986
- app.get("/api/v1/ai-connectors/:id", async (request, reply) => {
6987
- requireScopedAccess(request.headers, ["read"], {
6988
- route: "/api/v1/ai-connectors/:id"
8552
+ app.get(`${basePath}/:id`, async (request, reply) => {
8553
+ requireScopedAccess(request.headers, ["read"], {
8554
+ route: `${basePath}/:id`
8555
+ });
8556
+ const connector = getAiConnectorById(request.params.id);
8557
+ if (!connector) {
8558
+ reply.code(404);
8559
+ return { error: `${noun} not found` };
8560
+ }
8561
+ return {
8562
+ [singularKey]: connector,
8563
+ runs: listAiConnectorRuns(connector.id),
8564
+ conversation: getAiConnectorConversationForConnector(connector.id)
8565
+ };
6989
8566
  });
6990
- const connector = getAiConnectorById(request.params.id);
6991
- if (!connector) {
6992
- reply.code(404);
6993
- return { error: "AI connector not found" };
6994
- }
6995
- return {
6996
- connector,
6997
- runs: listAiConnectorRuns(connector.id),
6998
- conversation: getAiConnectorConversationForConnector(connector.id)
6999
- };
7000
- });
7001
- app.patch("/api/v1/ai-connectors/:id", async (request, reply) => {
7002
- requireScopedAccess(request.headers, ["write"], {
7003
- route: "/api/v1/ai-connectors/:id"
8567
+ app.patch(`${basePath}/:id`, async (request, reply) => {
8568
+ requireScopedAccess(request.headers, ["write"], {
8569
+ route: `${basePath}/:id`
8570
+ });
8571
+ const connector = updateAiConnector(request.params.id, updateAiConnectorSchema.parse(request.body ?? {}));
8572
+ if (!connector) {
8573
+ reply.code(404);
8574
+ return { error: `${noun} not found` };
8575
+ }
8576
+ return { [singularKey]: connector };
7004
8577
  });
7005
- const connector = updateAiConnector(request.params.id, updateAiConnectorSchema.parse(request.body ?? {}));
7006
- if (!connector) {
7007
- reply.code(404);
7008
- return { error: "AI connector not found" };
7009
- }
7010
- return { connector };
7011
- });
7012
- app.delete("/api/v1/ai-connectors/:id", async (request, reply) => {
7013
- requireScopedAccess(request.headers, ["write"], {
7014
- route: "/api/v1/ai-connectors/:id"
8578
+ app.delete(`${basePath}/:id`, async (request, reply) => {
8579
+ requireScopedAccess(request.headers, ["write"], {
8580
+ route: `${basePath}/:id`
8581
+ });
8582
+ const connector = deleteAiConnector(request.params.id);
8583
+ if (!connector) {
8584
+ reply.code(404);
8585
+ return { error: `${noun} not found` };
8586
+ }
8587
+ return { [singularKey]: connector };
7015
8588
  });
7016
- const connector = deleteAiConnector(request.params.id);
7017
- if (!connector) {
7018
- reply.code(404);
7019
- return { error: "AI connector not found" };
7020
- }
7021
- return { connector };
7022
- });
7023
- app.post("/api/v1/ai-connectors/:id/run", async (request, reply) => {
7024
- requireScopedAccess(request.headers, ["write"], {
7025
- route: "/api/v1/ai-connectors/:id/run"
8589
+ app.post(`${basePath}/:id/run`, async (request, reply) => {
8590
+ requireScopedAccess(request.headers, ["write"], {
8591
+ route: `${basePath}/:id/run`
8592
+ });
8593
+ const connector = getAiConnectorById(request.params.id);
8594
+ if (!connector) {
8595
+ reply.code(404);
8596
+ return { error: `${noun} not found` };
8597
+ }
8598
+ const execution = await runAiConnector(connector.id, runAiConnectorSchema.parse(request.body ?? {}), {
8599
+ llm: managers.llm,
8600
+ secrets: managers.secrets
8601
+ }, "run");
8602
+ return {
8603
+ [singularKey]: execution.connector,
8604
+ run: execution.run,
8605
+ conversation: execution.conversation
8606
+ };
7026
8607
  });
7027
- const connector = getAiConnectorById(request.params.id);
7028
- if (!connector) {
7029
- reply.code(404);
7030
- return { error: "AI connector not found" };
7031
- }
7032
- return await runAiConnector(connector.id, runAiConnectorSchema.parse(request.body ?? {}), {
7033
- llm: managers.llm,
7034
- secrets: managers.secrets
7035
- }, "run");
8608
+ app.post(`${basePath}/:id/chat`, async (request, reply) => {
8609
+ requireScopedAccess(request.headers, ["write"], {
8610
+ route: `${basePath}/:id/chat`
8611
+ });
8612
+ const connector = getAiConnectorById(request.params.id);
8613
+ if (!connector) {
8614
+ reply.code(404);
8615
+ return { error: `${noun} not found` };
8616
+ }
8617
+ const execution = await runAiConnector(connector.id, runAiConnectorSchema.parse(request.body ?? {}), {
8618
+ llm: managers.llm,
8619
+ secrets: managers.secrets
8620
+ }, "chat");
8621
+ return {
8622
+ [singularKey]: execution.connector,
8623
+ run: execution.run,
8624
+ conversation: execution.conversation
8625
+ };
8626
+ });
8627
+ app.get(`${basePath}/:id/output`, async (request, reply) => {
8628
+ requireScopedAccess(request.headers, ["read"], {
8629
+ route: `${basePath}/:id/output`
8630
+ });
8631
+ const connector = getAiConnectorById(request.params.id);
8632
+ if (!connector) {
8633
+ reply.code(404);
8634
+ return { error: `${noun} not found` };
8635
+ }
8636
+ return {
8637
+ [singularKey]: connector,
8638
+ output: connector.lastRun?.result ?? null
8639
+ };
8640
+ });
8641
+ app.get(`${basePath}/:id/runs`, async (request, reply) => {
8642
+ requireScopedAccess(request.headers, ["read"], {
8643
+ route: `${basePath}/:id/runs`
8644
+ });
8645
+ const connector = getAiConnectorById(request.params.id);
8646
+ if (!connector) {
8647
+ reply.code(404);
8648
+ return { error: `${noun} not found` };
8649
+ }
8650
+ return {
8651
+ runs: listAiConnectorRuns(connector.id)
8652
+ };
8653
+ });
8654
+ app.get(`${basePath}/:id/runs/:runId`, async (request, reply) => {
8655
+ requireScopedAccess(request.headers, ["read"], {
8656
+ route: `${basePath}/:id/runs/:runId`
8657
+ });
8658
+ const params = request.params;
8659
+ const connector = getAiConnectorById(params.id);
8660
+ if (!connector) {
8661
+ reply.code(404);
8662
+ return { error: `${noun} not found` };
8663
+ }
8664
+ const run = getAiConnectorRunById(connector.id, params.runId);
8665
+ if (!run) {
8666
+ reply.code(404);
8667
+ return { error: `${noun} run not found` };
8668
+ }
8669
+ return {
8670
+ [singularKey]: connector,
8671
+ run
8672
+ };
8673
+ });
8674
+ app.get(`${basePath}/:id/runs/:runId/nodes`, async (request, reply) => {
8675
+ requireScopedAccess(request.headers, ["read"], {
8676
+ route: `${basePath}/:id/runs/:runId/nodes`
8677
+ });
8678
+ const params = request.params;
8679
+ const connector = getAiConnectorById(params.id);
8680
+ if (!connector) {
8681
+ reply.code(404);
8682
+ return { error: `${noun} not found` };
8683
+ }
8684
+ const nodeResults = getAiConnectorRunNodeResults(connector.id, params.runId);
8685
+ if (!nodeResults) {
8686
+ reply.code(404);
8687
+ return { error: `${noun} run not found` };
8688
+ }
8689
+ return {
8690
+ [singularKey]: connector,
8691
+ nodeResults
8692
+ };
8693
+ });
8694
+ app.get(`${basePath}/:id/runs/:runId/nodes/:nodeId`, async (request, reply) => {
8695
+ requireScopedAccess(request.headers, ["read"], {
8696
+ route: `${basePath}/:id/runs/:runId/nodes/:nodeId`
8697
+ });
8698
+ const params = request.params;
8699
+ const connector = getAiConnectorById(params.id);
8700
+ if (!connector) {
8701
+ reply.code(404);
8702
+ return { error: `${noun} not found` };
8703
+ }
8704
+ const nodeResult = getAiConnectorRunNodeResult(connector.id, params.runId, params.nodeId);
8705
+ if (!nodeResult) {
8706
+ reply.code(404);
8707
+ return { error: `${noun} node result not found` };
8708
+ }
8709
+ return {
8710
+ [singularKey]: connector,
8711
+ nodeResult
8712
+ };
8713
+ });
8714
+ app.get(`${basePath}/:id/nodes/:nodeId/output`, async (request, reply) => {
8715
+ requireScopedAccess(request.headers, ["read"], {
8716
+ route: `${basePath}/:id/nodes/:nodeId/output`
8717
+ });
8718
+ const params = request.params;
8719
+ const connector = getAiConnectorById(params.id);
8720
+ if (!connector) {
8721
+ reply.code(404);
8722
+ return { error: `${noun} not found` };
8723
+ }
8724
+ const latest = getLatestAiConnectorNodeOutput(connector.id, params.nodeId);
8725
+ if (!latest) {
8726
+ reply.code(404);
8727
+ return { error: `${noun} node output not found` };
8728
+ }
8729
+ return {
8730
+ [singularKey]: connector,
8731
+ run: latest.run,
8732
+ nodeResult: latest.nodeResult
8733
+ };
8734
+ });
8735
+ };
8736
+ registerFlowApiRoutes("/api/v1/workbench/flows", "Workbench flow", {
8737
+ collectionKey: "flows",
8738
+ singularKey: "flow",
8739
+ catalogPath: "/api/v1/workbench/catalog/boxes"
7036
8740
  });
7037
- app.post("/api/v1/ai-connectors/:id/chat", async (request, reply) => {
8741
+ app.post("/api/v1/workbench/run", async (request, reply) => {
7038
8742
  requireScopedAccess(request.headers, ["write"], {
7039
- route: "/api/v1/ai-connectors/:id/chat"
8743
+ route: "/api/v1/workbench/run"
7040
8744
  });
7041
- const connector = getAiConnectorById(request.params.id);
7042
- if (!connector) {
8745
+ const payload = runAiConnectorSchema
8746
+ .extend({
8747
+ flowId: z.string().trim().min(1)
8748
+ })
8749
+ .parse(request.body ?? {});
8750
+ const flow = getAiConnectorById(payload.flowId);
8751
+ if (!flow) {
7043
8752
  reply.code(404);
7044
- return { error: "AI connector not found" };
8753
+ return { error: "Workbench flow not found" };
7045
8754
  }
7046
- return await runAiConnector(connector.id, runAiConnectorSchema.parse(request.body ?? {}), {
8755
+ const { flowId, ...runInput } = payload;
8756
+ const execution = await runAiConnector(flow.id, runInput, {
7047
8757
  llm: managers.llm,
7048
8758
  secrets: managers.secrets
7049
- }, "chat");
7050
- });
7051
- app.get("/api/v1/ai-connectors/:id/output", async (request, reply) => {
7052
- requireScopedAccess(request.headers, ["read"], {
7053
- route: "/api/v1/ai-connectors/:id/output"
7054
- });
7055
- const connector = getAiConnectorById(request.params.id);
7056
- if (!connector) {
7057
- reply.code(404);
7058
- return { error: "AI connector not found" };
7059
- }
7060
- return {
7061
- connector,
7062
- output: connector.lastRun?.result ?? null
7063
- };
7064
- });
7065
- app.get("/api/v1/ai-connectors/:id/runs", async (request, reply) => {
7066
- requireScopedAccess(request.headers, ["read"], {
7067
- route: "/api/v1/ai-connectors/:id/runs"
7068
- });
7069
- const connector = getAiConnectorById(request.params.id);
7070
- if (!connector) {
7071
- reply.code(404);
7072
- return { error: "AI connector not found" };
7073
- }
8759
+ }, "run");
7074
8760
  return {
7075
- runs: listAiConnectorRuns(connector.id)
8761
+ flow: execution.connector,
8762
+ run: execution.run,
8763
+ conversation: execution.conversation
7076
8764
  };
7077
8765
  });
7078
- app.get("/api/v1/ai-connectors/by-slug/:slug", async (request, reply) => {
8766
+ app.get("/api/v1/workbench/flows/by-slug/:slug", async (request, reply) => {
7079
8767
  requireScopedAccess(request.headers, ["read"], {
7080
- route: "/api/v1/ai-connectors/by-slug/:slug"
8768
+ route: "/api/v1/workbench/flows/by-slug/:slug"
7081
8769
  });
7082
8770
  const connector = getAiConnectorBySlug(request.params.slug);
7083
8771
  if (!connector) {
7084
8772
  reply.code(404);
7085
- return { error: "AI connector not found" };
8773
+ return { error: "Workbench flow not found" };
7086
8774
  }
7087
- return { connector };
8775
+ return { flow: connector };
7088
8776
  });
7089
8777
  app.post("/api/v1/settings/tokens", async (request, reply) => {
7090
8778
  const auth = requireOperatorSession(request.headers, { route: "/api/v1/settings/tokens" });
@@ -7446,6 +9134,16 @@ export async function buildServer(options = {}) {
7446
9134
  }
7447
9135
  return { task };
7448
9136
  });
9137
+ app.post("/api/v1/tasks/:id/split", async (request, reply) => {
9138
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/tasks/:id/split" });
9139
+ const { id } = request.params;
9140
+ const result = splitTask(id, taskSplitCreateSchema.parse(request.body ?? {}), toActivityContext(auth));
9141
+ if (!result) {
9142
+ reply.code(404);
9143
+ return { error: "Task not found" };
9144
+ }
9145
+ return result;
9146
+ });
7449
9147
  app.delete("/api/v1/tasks/:id", async (request, reply) => {
7450
9148
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/tasks/:id" });
7451
9149
  const { id } = request.params;