forge-openclaw-plugin 0.2.19 → 0.2.21

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 (82) hide show
  1. package/README.md +133 -2
  2. package/dist/assets/board-_C6oMy5w.js +6 -0
  3. package/dist/assets/{board-8L3uX7_O.js.map → board-_C6oMy5w.js.map} +1 -1
  4. package/dist/assets/index-B4A6TooJ.js +63 -0
  5. package/dist/assets/index-B4A6TooJ.js.map +1 -0
  6. package/dist/assets/index-D6Xs_2mo.css +1 -0
  7. package/dist/assets/{motion-1GAqqi8M.js → motion-D4sZgCHd.js} +2 -2
  8. package/dist/assets/{motion-1GAqqi8M.js.map → motion-D4sZgCHd.js.map} +1 -1
  9. package/dist/assets/{table-DBGlgRjk.js → table-BWzTaky1.js} +2 -2
  10. package/dist/assets/{table-DBGlgRjk.js.map → table-BWzTaky1.js.map} +1 -1
  11. package/dist/assets/{ui-iTluWjC4.js → ui-BzK4azQb.js} +7 -7
  12. package/dist/assets/{ui-iTluWjC4.js.map → ui-BzK4azQb.js.map} +1 -1
  13. package/dist/assets/vendor-DT3pnAKJ.css +1 -0
  14. package/dist/assets/vendor-De38P6YR.js +729 -0
  15. package/dist/assets/vendor-De38P6YR.js.map +1 -0
  16. package/dist/assets/viz-C6hfyqzu.js +34 -0
  17. package/dist/assets/viz-C6hfyqzu.js.map +1 -0
  18. package/dist/index.html +9 -9
  19. package/dist/openclaw/parity.d.ts +1 -1
  20. package/dist/openclaw/parity.js +29 -2
  21. package/dist/openclaw/routes.js +207 -24
  22. package/dist/openclaw/tools.js +324 -35
  23. package/dist/server/app.js +2080 -92
  24. package/dist/server/db.js +3 -0
  25. package/dist/server/health.js +1284 -0
  26. package/dist/server/managers/platform/background-job-manager.js +138 -2
  27. package/dist/server/managers/platform/llm-manager.js +126 -0
  28. package/dist/server/managers/platform/openai-responses-provider.js +773 -0
  29. package/dist/server/managers/runtime.js +6 -1
  30. package/dist/server/openapi.js +718 -0
  31. package/dist/server/preferences-seeds.js +409 -0
  32. package/dist/server/preferences-types.js +368 -0
  33. package/dist/server/psyche-types.js +42 -18
  34. package/dist/server/repositories/activity-events.js +53 -4
  35. package/dist/server/repositories/calendar.js +89 -15
  36. package/dist/server/repositories/collaboration.js +8 -3
  37. package/dist/server/repositories/diagnostic-logs.js +243 -0
  38. package/dist/server/repositories/entity-ownership.js +92 -0
  39. package/dist/server/repositories/goals.js +7 -2
  40. package/dist/server/repositories/habits.js +122 -16
  41. package/dist/server/repositories/notes.js +119 -41
  42. package/dist/server/repositories/preferences.js +1765 -0
  43. package/dist/server/repositories/projects.js +18 -7
  44. package/dist/server/repositories/psyche.js +84 -27
  45. package/dist/server/repositories/rewards.js +112 -4
  46. package/dist/server/repositories/strategies.js +450 -0
  47. package/dist/server/repositories/tags.js +11 -6
  48. package/dist/server/repositories/task-runs.js +10 -2
  49. package/dist/server/repositories/tasks.js +99 -17
  50. package/dist/server/repositories/users.js +417 -0
  51. package/dist/server/repositories/wiki-memory.js +3366 -0
  52. package/dist/server/services/context.js +20 -18
  53. package/dist/server/services/dashboard.js +29 -6
  54. package/dist/server/services/entity-crud.js +21 -3
  55. package/dist/server/services/insights.js +9 -7
  56. package/dist/server/services/projects.js +2 -1
  57. package/dist/server/services/psyche.js +10 -9
  58. package/dist/server/types.js +594 -30
  59. package/openclaw.plugin.json +1 -1
  60. package/package.json +1 -1
  61. package/server/migrations/015_multi_user_and_strategies.sql +244 -0
  62. package/server/migrations/016_health_companion.sql +158 -0
  63. package/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
  64. package/server/migrations/017_preferences.sql +131 -0
  65. package/server/migrations/018_preference_catalogs.sql +31 -0
  66. package/server/migrations/019_wiki_memory.sql +255 -0
  67. package/server/migrations/020_wiki_page_hierarchy.sql +11 -0
  68. package/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
  69. package/server/migrations/022_wiki_ingest_background.sql +85 -0
  70. package/server/migrations/023_diagnostic_logs.sql +28 -0
  71. package/skills/forge-openclaw/SKILL.md +126 -34
  72. package/skills/forge-openclaw/entity_conversation_playbooks.md +337 -0
  73. package/skills/forge-openclaw/psyche_entity_playbooks.md +404 -0
  74. package/dist/assets/board-8L3uX7_O.js +0 -6
  75. package/dist/assets/index-Cj1IBH_w.js +0 -36
  76. package/dist/assets/index-Cj1IBH_w.js.map +0 -1
  77. package/dist/assets/index-DQT6EbuS.css +0 -1
  78. package/dist/assets/vendor-BvM2F9Dp.js +0 -503
  79. package/dist/assets/vendor-BvM2F9Dp.js.map +0 -1
  80. package/dist/assets/vendor-CRS-psbw.css +0 -1
  81. package/dist/assets/viz-CNeunkfu.js +0 -34
  82. package/dist/assets/viz-CNeunkfu.js.map +0 -1
@@ -1,20 +1,27 @@
1
1
  import Fastify from "fastify";
2
2
  import cors from "@fastify/cors";
3
+ import multipart from "@fastify/multipart";
3
4
  import { ZodError } from "zod";
4
5
  import { configureDatabase, configureDatabaseSeeding, runInTransaction } from "./db.js";
5
6
  import { HttpError, isHttpError } from "./errors.js";
6
7
  import { listActivityEvents, listActivityEventsForTask, recordActivityEvent, removeActivityEvent } from "./repositories/activity-events.js";
7
8
  import { approveApprovalRequest, createAgentAction, createInsight, createInsightFeedback, deleteInsight, getInsightById, listAgentActions, listApprovalRequests, listInsights, rejectApprovalRequest, updateInsight } from "./repositories/collaboration.js";
8
9
  import { listEventLog } from "./repositories/event-log.js";
10
+ import { createDiagnosticMessage, DIAGNOSTIC_LOG_RETENTION_SWEEP_INTERVAL_MS, enforceDiagnosticLogRetention, listDiagnosticLogs, normalizeDiagnosticSource, recordDiagnosticLog, serializeDiagnosticError } from "./repositories/diagnostic-logs.js";
9
11
  import { createGoal, getGoalById, listGoals, updateGoal } from "./repositories/goals.js";
10
- import { createHabit, createHabitCheckIn, getHabitById, listHabits, updateHabit } from "./repositories/habits.js";
12
+ import { createHabit, createHabitCheckIn, deleteHabitCheckIn, getHabitById, listHabits, updateHabit } from "./repositories/habits.js";
11
13
  import { listDomains } from "./repositories/domains.js";
12
14
  import { buildNotesSummaryByEntity, createNote, getNoteById, listNotes, updateNote } from "./repositories/notes.js";
15
+ 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";
16
+ import { filterOwnedEntities, setEntityOwner } from "./repositories/entity-ownership.js";
13
17
  import { createBehavior, createBehaviorPattern, createBeliefEntry, createEmotionDefinition, createEventType, createModeGuideSession, createModeProfile, createPsycheValue, createTriggerReport, getBehaviorById, getBehaviorPatternById, getBeliefEntryById, getEmotionDefinitionById, getEventTypeById, getModeGuideSessionById, getModeProfileById, getPsycheValueById, getTriggerReportById, listBehaviors, listBehaviorPatterns, listBeliefEntries, listEmotionDefinitions, listEventTypes, listModeGuideSessions, listModeProfiles, listPsycheValues, listSchemaCatalog, listTriggerReports, updateBehavior, updateBehaviorPattern, updateBeliefEntry, updateEmotionDefinition, updateEventType, updateModeGuideSession, updateModeProfile, updatePsycheValue, updateTriggerReport } from "./repositories/psyche.js";
14
18
  import { createProject, updateProject } from "./repositories/projects.js";
19
+ import { createPreferenceCatalog, createPreferenceCatalogItem, createPreferenceContext, createPreferenceItem, createPreferenceItemFromEntity, deletePreferenceCatalog, deletePreferenceCatalogItem, getPreferenceWorkspace, mergePreferenceContexts, startPreferenceGame, submitAbsoluteSignal, submitPairwiseJudgment, updatePreferenceCatalog, updatePreferenceCatalogItem, updatePreferenceContext, updatePreferenceItem, updatePreferenceScore } from "./repositories/preferences.js";
20
+ import { createStrategy, getStrategyById, listStrategies, updateStrategy } from "./repositories/strategies.js";
15
21
  import { createManualRewardGrant, getDailyAmbientXp, getRewardRuleById, listRewardLedger, listRewardRules, recordWorkAdjustmentReward, recordSessionEvent, updateRewardRule } from "./repositories/rewards.js";
16
22
  import { listAgentIdentities, getSettings, isPsycheAuthRequired, updateSettings, verifyAgentToken } from "./repositories/settings.js";
17
23
  import { createTag, getTagById, listTags, updateTag } from "./repositories/tags.js";
24
+ import { createUser, ensureSystemUsers, getUserById, listUserAccessGrants, listUserOwnershipSummaries, listUserXpSummaries, listUsers, resolveUserForMutation, updateUserAccessGrant, updateUser } from "./repositories/users.js";
18
25
  import { claimTaskRun, completeTaskRun, focusTaskRun, heartbeatTaskRun, listTaskRuns, recoverTimedOutTaskRuns, releaseTaskRun } from "./repositories/task-runs.js";
19
26
  import { createTask, createTaskWithIdempotency, getTaskById, listTasks, uncompleteTask, updateTask } from "./repositories/tasks.js";
20
27
  import { createWorkAdjustment } from "./repositories/work-adjustments.js";
@@ -32,11 +39,13 @@ import { createTaskRunWatchdog } from "./services/task-run-watchdog.js";
32
39
  import { suggestTags } from "./services/tagging.js";
33
40
  import { CalendarConnectionConflictError, completeMicrosoftCalendarOauth, createCalendarConnection, deleteCalendarEventProjection, discoverCalendarConnection, discoverExistingCalendarConnection, getMicrosoftCalendarOauthSession, listConnectedCalendarConnections, removeCalendarConnection, pushCalendarEventUpdate, readCalendarOverview, syncCalendarConnection, startMicrosoftCalendarOauth, testMicrosoftCalendarOauthConfiguration, listCalendarProviderMetadata, updateCalendarConnectionSelection } from "./services/calendar-runtime.js";
34
41
  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";
35
- import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createCalendarEventSchema, createHabitCheckInSchema, createCalendarConnectionSchema, discoverCalendarConnectionSchema, startMicrosoftCalendarOauthSchema, testMicrosoftCalendarOauthConfigurationSchema, createHabitSchema, createTaskTimeboxSchema, createWorkBlockTemplateSchema, createSessionEventSchema, createWorkAdjustmentSchema, createTagSchema, calendarOverviewQuerySchema, notesListQuerySchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateCalendarConnectionSchema, updateCalendarEventSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskTimeboxSchema, updateTaskSchema, updateWorkBlockTemplateSchema, workAdjustmentResultSchema, finalizeWeeklyReviewResultSchema, recommendTaskTimeboxesSchema } from "./types.js";
42
+ import { createPreferenceCatalogItemSchema, createPreferenceCatalogSchema, createPreferenceContextSchema, createPreferenceItemSchema, enqueueEntityPreferenceItemSchema, mergePreferenceContextsSchema, preferenceWorkspaceQuerySchema, startPreferenceGameSchema, submitAbsoluteSignalSchema, submitPairwiseJudgmentSchema, updatePreferenceCatalogItemSchema, updatePreferenceCatalogSchema, updatePreferenceContextSchema, updatePreferenceItemSchema, updatePreferenceScoreSchema } from "./preferences-types.js";
43
+ import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, 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, workAdjustmentResultSchema, finalizeWeeklyReviewResultSchema, goalListQuerySchema, recommendTaskTimeboxesSchema, strategyListQuerySchema } from "./types.js";
36
44
  import { buildOpenApiDocument } from "./openapi.js";
37
45
  import { registerWebRoutes } from "./web.js";
38
46
  import { createManagerRuntime } from "./managers/runtime.js";
39
47
  import { isManagerError } from "./managers/type-guards.js";
48
+ import { createCompanionPairingSession, createCompanionPairingSessionSchema, getCompanionOverview, getFitnessViewData, getSleepViewData, ingestMobileHealthSync, mobileHealthSyncSchema, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
40
49
  const COMPATIBILITY_SUNSET = "transitional-node";
41
50
  function markCompatibilityRoute(reply) {
42
51
  reply.header("Deprecation", "true");
@@ -100,6 +109,30 @@ function buildEventStreamMeta() {
100
109
  }
101
110
  };
102
111
  }
112
+ function buildApiBaseUrl(request) {
113
+ const referer = typeof request.headers.referer === "string"
114
+ ? request.headers.referer.trim()
115
+ : "";
116
+ if (referer) {
117
+ try {
118
+ const url = new URL(referer);
119
+ const forgeMounted = url.pathname.startsWith("/forge/");
120
+ return `${url.origin}${forgeMounted ? "/forge" : ""}/api/v1`;
121
+ }
122
+ catch {
123
+ // Fall through to host-based resolution.
124
+ }
125
+ }
126
+ const host = typeof request.headers.host === "string" &&
127
+ request.headers.host.trim().length > 0
128
+ ? request.headers.host.trim()
129
+ : "127.0.0.1:4317";
130
+ const forwardedPrefix = typeof request.headers["x-forwarded-prefix"] === "string"
131
+ ? request.headers["x-forwarded-prefix"].trim()
132
+ : "";
133
+ const basePath = forwardedPrefix.replace(/\/$/, "");
134
+ return `${request.protocol}://${host}${basePath}/api/v1`;
135
+ }
103
136
  function readSingleForwardedHeader(value) {
104
137
  if (Array.isArray(value)) {
105
138
  return value[0]?.split(",")[0]?.trim() || null;
@@ -166,6 +199,14 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
166
199
  enumValues: ["active", "paused", "completed"],
167
200
  defaultValue: "active"
168
201
  },
202
+ {
203
+ name: "userId",
204
+ type: "string|null",
205
+ required: false,
206
+ description: "Owning human or bot user id. Omit it to use Forge's default owner.",
207
+ defaultValue: null,
208
+ nullable: true
209
+ },
169
210
  {
170
211
  name: "targetPoints",
171
212
  type: "integer",
@@ -239,6 +280,14 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
239
280
  enumValues: ["active", "paused", "completed"],
240
281
  defaultValue: "active"
241
282
  },
283
+ {
284
+ name: "userId",
285
+ type: "string|null",
286
+ required: false,
287
+ description: "Owning human or bot user id. Omit it to use Forge's default owner.",
288
+ defaultValue: null,
289
+ nullable: true
290
+ },
242
291
  {
243
292
  name: "targetPoints",
244
293
  type: "integer",
@@ -262,6 +311,89 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
262
311
  }
263
312
  ]
264
313
  },
314
+ {
315
+ entityType: "strategy",
316
+ purpose: "A directed, non-loopy execution plan that connects current work to target goals or projects.",
317
+ minimumCreateFields: ["title", "graph"],
318
+ relationshipRules: [
319
+ "Strategies can target one or many goals or projects.",
320
+ "Graph nodes must reference existing projects or tasks.",
321
+ "Graph edges must remain directed and acyclic.",
322
+ "linkedEntities is for related context that should stay visible without becoming part of the main sequence."
323
+ ],
324
+ searchHints: [
325
+ "Search by title or linked target before creating a duplicate strategy.",
326
+ "Use userIds when you want strategies owned by specific humans or bots."
327
+ ],
328
+ examples: [
329
+ '{"title":"Ship multi-user Forge","overview":"Separate humans and bots, then connect the systems with shared strategies.","endStateDescription":"Forge supports human and bot users across all routes and views.","targetGoalIds":["goal_123"],"targetProjectIds":["project_123"],"graph":{"nodes":[{"id":"node_a","entityType":"project","entityId":"project_123","title":"Multi-user backend","branchLabel":"Core","notes":"Land ownership and route scope first."},{"id":"node_b","entityType":"task","entityId":"task_123","title":"Strategy UI polish","branchLabel":"UI","notes":"Surface alignment and graph editing in the app."}],"edges":[{"from":"node_a","to":"node_b","label":"after backend lands","condition":""}]}}'
330
+ ],
331
+ fieldGuide: [
332
+ {
333
+ name: "title",
334
+ type: "string",
335
+ required: true,
336
+ description: "Strategy name."
337
+ },
338
+ {
339
+ name: "overview",
340
+ type: "string",
341
+ required: false,
342
+ description: "What this strategy is for and why it matters.",
343
+ defaultValue: ""
344
+ },
345
+ {
346
+ name: "endStateDescription",
347
+ type: "string",
348
+ required: false,
349
+ description: "What done looks like when the strategy lands.",
350
+ defaultValue: ""
351
+ },
352
+ {
353
+ name: "status",
354
+ type: "active|paused|completed",
355
+ required: false,
356
+ description: "Lifecycle state.",
357
+ enumValues: ["active", "paused", "completed"],
358
+ defaultValue: "active"
359
+ },
360
+ {
361
+ name: "userId",
362
+ type: "string|null",
363
+ required: false,
364
+ description: "Owning human or bot user id. Omit it to use Forge's default owner.",
365
+ defaultValue: null,
366
+ nullable: true
367
+ },
368
+ {
369
+ name: "targetGoalIds",
370
+ type: "string[]",
371
+ required: false,
372
+ description: "Goal ids this strategy is meant to land.",
373
+ defaultValue: []
374
+ },
375
+ {
376
+ name: "targetProjectIds",
377
+ type: "string[]",
378
+ required: false,
379
+ description: "Project ids this strategy is meant to land.",
380
+ defaultValue: []
381
+ },
382
+ {
383
+ name: "linkedEntities",
384
+ type: "Array<{ entityType, entityId }>",
385
+ required: false,
386
+ description: "Related entities that should stay visible in the strategy context.",
387
+ defaultValue: []
388
+ },
389
+ {
390
+ name: "graph",
391
+ type: "StrategyGraph",
392
+ required: true,
393
+ description: "Directed acyclic graph with nodes referencing projects/tasks and edges defining the flow order."
394
+ }
395
+ ]
396
+ },
265
397
  {
266
398
  entityType: "task",
267
399
  purpose: "A concrete actionable work item. Tasks are what the user actually does.",
@@ -315,6 +447,14 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
315
447
  description: "Human-facing owner label.",
316
448
  defaultValue: "Albert"
317
449
  },
450
+ {
451
+ name: "userId",
452
+ type: "string|null",
453
+ required: false,
454
+ description: "Owning human or bot user id. Omit it to use Forge's default owner.",
455
+ defaultValue: null,
456
+ nullable: true
457
+ },
318
458
  {
319
459
  name: "goalId",
320
460
  type: "string|null",
@@ -1531,7 +1671,7 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
1531
1671
  {
1532
1672
  entityType: "mode_guide_session",
1533
1673
  purpose: "A guided mode-mapping session that stores structured answers and candidate mode interpretations.",
1534
- minimumCreateFields: ["summary", "answers", "results"],
1674
+ minimumCreateFields: ["summary", "answers"],
1535
1675
  relationshipRules: [
1536
1676
  "Mode guide sessions help the user reason toward likely modes before or alongside mode profiles.",
1537
1677
  "Use mode guide sessions for guided interpretation, not as a replacement for durable mode profiles."
@@ -1558,7 +1698,7 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
1558
1698
  {
1559
1699
  name: "results",
1560
1700
  type: "array",
1561
- required: true,
1701
+ required: false,
1562
1702
  description: "List of { family, archetype, label, confidence 0-1, reasoning } candidate mode interpretations."
1563
1703
  }
1564
1704
  ]
@@ -1737,18 +1877,189 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
1737
1877
  ]
1738
1878
  }
1739
1879
  ];
1880
+ const AGENT_ONBOARDING_CONVERSATION_RULES = [
1881
+ "Ask only for what is missing or unclear instead of walking the user through every optional field.",
1882
+ "Use a progression of concrete example or intent, working name, purpose or meaning, placement in Forge, operational details, and linked context.",
1883
+ "Ask one to three focused questions at a time. One is usually best when the user is uncertain or emotionally loaded.",
1884
+ "Before saving, briefly summarize the working formulation in the user's own language when that would reduce ambiguity.",
1885
+ "When updating an entity, start with what is changing, what should stay true, and what prompted the update now."
1886
+ ];
1887
+ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
1888
+ {
1889
+ focus: "goal",
1890
+ openingQuestion: "What direction are you trying to hold onto here?",
1891
+ coachingGoal: "Clarify the direction and why it matters, not just produce a title.",
1892
+ askSequence: [
1893
+ "Ask what direction or outcome the user wants to keep in view.",
1894
+ "Ask why it matters now.",
1895
+ "Distinguish the goal from a project or task.",
1896
+ "Clarify horizon and status only after the meaning is clear."
1897
+ ]
1898
+ },
1899
+ {
1900
+ focus: "project",
1901
+ openingQuestion: "If this becomes a project, what would you want it to be called and what should it accomplish?",
1902
+ coachingGoal: "Turn an intention into a bounded workstream with a clear outcome.",
1903
+ askSequence: [
1904
+ "Ask what this piece of work should be called.",
1905
+ "Ask what outcome would make the project feel real or complete for now.",
1906
+ "Ask which goal it belongs under.",
1907
+ "Clarify status, owner, and notes only after the scope is clear."
1908
+ ]
1909
+ },
1910
+ {
1911
+ focus: "strategy",
1912
+ openingQuestion: "What future state is this strategy supposed to make real?",
1913
+ coachingGoal: "Turn a vague plan into a deliberate sequence toward a real end state.",
1914
+ askSequence: [
1915
+ "Ask what end state the strategy is trying to land.",
1916
+ "Ask which goals or projects are the true targets.",
1917
+ "Ask what the major steps or nodes are.",
1918
+ "Ask about order, dependencies, and anything that must not be skipped."
1919
+ ]
1920
+ },
1921
+ {
1922
+ focus: "task",
1923
+ openingQuestion: "What is the next concrete move you want to remember or do?",
1924
+ coachingGoal: "Identify the next concrete move, not just capture a vague obligation.",
1925
+ askSequence: [
1926
+ "Ask what the next concrete action is.",
1927
+ "Ask where it belongs: project, goal, both, or standalone.",
1928
+ "Ask what would make it easier to do: due date, priority, owner, or brief context."
1929
+ ]
1930
+ },
1931
+ {
1932
+ focus: "habit",
1933
+ openingQuestion: "What is the recurring behavior you want Forge to keep track of?",
1934
+ coachingGoal: "Define the recurring behavior and cadence clearly enough for honest later check-ins.",
1935
+ askSequence: [
1936
+ "Ask what the recurring behavior is in plain language.",
1937
+ "Ask whether doing it is aligned or a slip.",
1938
+ "Ask about cadence and what counts as success in practice.",
1939
+ "Ask about links only if they will help later review."
1940
+ ]
1941
+ },
1942
+ {
1943
+ focus: "note",
1944
+ openingQuestion: "What do you want this note to preserve, and what should it stay attached to?",
1945
+ coachingGoal: "Preserve the useful context and link it to the right places without turning the note into a dump.",
1946
+ askSequence: [
1947
+ "Ask what the note needs to preserve.",
1948
+ "Ask what entities it should stay attached to.",
1949
+ "Ask whether it should be durable or temporary.",
1950
+ "Ask about tags or author only if they help retrieval or handoff."
1951
+ ]
1952
+ },
1953
+ {
1954
+ focus: "insight",
1955
+ openingQuestion: "What observation or recommendation do you want Forge to remember?",
1956
+ coachingGoal: "Capture one grounded observation or recommendation clearly enough that it remains useful later.",
1957
+ askSequence: [
1958
+ "Ask what pattern, tension, or observation should be remembered.",
1959
+ "Ask what entity or timeframe it belongs to, if any.",
1960
+ "Ask what recommendation, caution, or invitation should remain explicit."
1961
+ ]
1962
+ },
1963
+ {
1964
+ focus: "calendar_event",
1965
+ openingQuestion: "What is the event, and when should it happen in your local time?",
1966
+ coachingGoal: "Make the event legible as a real commitment in time, with the right timezone and links.",
1967
+ askSequence: [
1968
+ "Ask what the event is.",
1969
+ "Ask when it starts and ends in local time.",
1970
+ "Ask where it belongs or what it supports.",
1971
+ "Ask whether it should stay Forge-only only if that choice matters."
1972
+ ]
1973
+ },
1974
+ {
1975
+ focus: "work_block_template",
1976
+ openingQuestion: "What recurring block do you want to set up, and when should it repeat?",
1977
+ coachingGoal: "Define a reusable availability rule rather than a one-off event.",
1978
+ askSequence: [
1979
+ "Ask what kind of block it is and what it should be called.",
1980
+ "Ask on which days and at what local times it should repeat.",
1981
+ "Ask whether it allows or blocks work.",
1982
+ "Ask whether it has a start or end date."
1983
+ ]
1984
+ },
1985
+ {
1986
+ focus: "task_timebox",
1987
+ openingQuestion: "Which task are you trying to make time for, and when should the slot be?",
1988
+ coachingGoal: "Reserve real time for one task without confusing planned work with completed work.",
1989
+ askSequence: [
1990
+ "Ask which task the slot belongs to.",
1991
+ "Ask when the slot should start and end.",
1992
+ "Ask about source or override reason only when that context matters."
1993
+ ]
1994
+ },
1995
+ {
1996
+ focus: "event_type",
1997
+ openingQuestion: "What kind of incident should this category stand for?",
1998
+ coachingGoal: "Create a reusable incident category that will actually help future reports stay consistent.",
1999
+ askSequence: [
2000
+ "Ask what category the label should capture.",
2001
+ "Ask how narrow or broad it should be.",
2002
+ "Ask for a short description only if the label could be ambiguous later."
2003
+ ]
2004
+ },
2005
+ {
2006
+ focus: "emotion_definition",
2007
+ openingQuestion: "What emotion label do you want to keep reusable in Forge?",
2008
+ coachingGoal: "Create a reusable emotion label with enough clarity to use consistently later.",
2009
+ askSequence: [
2010
+ "Ask what emotion label the user wants to preserve.",
2011
+ "Ask what distinguishes it from nearby emotions.",
2012
+ "Ask for a broader category only if it will help later browsing or reporting."
2013
+ ]
2014
+ }
2015
+ ];
1740
2016
  const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
2017
+ {
2018
+ focus: "psyche_value",
2019
+ 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.",
2020
+ 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.",
2021
+ askSequence: [
2022
+ "Start with what matters and why it matters now.",
2023
+ "Ask for one concrete example of what living this value would look like in ordinary life.",
2024
+ "Separate the value direction from any specific outcome or achievement goal.",
2025
+ "Notice tensions, barriers, or situations where the value gets lost.",
2026
+ "Name one small committed action that would move toward the value."
2027
+ ],
2028
+ requiredForCreate: ["title"],
2029
+ highValueOptionalFields: [
2030
+ "description",
2031
+ "valuedDirection",
2032
+ "whyItMatters",
2033
+ "committedActions",
2034
+ "linkedGoalIds",
2035
+ "linkedProjectIds",
2036
+ "linkedTaskIds"
2037
+ ],
2038
+ exampleQuestions: [
2039
+ "What feels deeply important about this to you?",
2040
+ "If you were living this value a little more this week, what would someone be able to see?",
2041
+ "What goal or area of life does this value belong to most clearly?",
2042
+ "When this value is hard to live, what tends to get in the way?",
2043
+ "What is one small action that would express it in practice?"
2044
+ ],
2045
+ notes: [
2046
+ "Use an ACT-style values clarification stance: values are directions to live toward, not boxes to complete.",
2047
+ "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.",
2048
+ "If the user says they want to understand it first, start with one orienting question before offering a formulation or save suggestion."
2049
+ ]
2050
+ },
1741
2051
  {
1742
2052
  focus: "behavior_pattern",
1743
2053
  useWhen: "Use for a recurring loop that shows up across multiple situations and can be described as cue -> response -> payoff -> cost -> preferred response.",
1744
- coachingGoal: "Help the user build a CBT-style functional analysis instead of just naming the problem vaguely.",
2054
+ coachingGoal: "Help the user build a CBT-style functional analysis with active listening instead of just naming the problem vaguely.",
1745
2055
  askSequence: [
1746
- "Name the loop in plain language.",
1747
- "Identify the typical cue or context.",
1748
- "Describe the visible behavior or sequence once it starts.",
1749
- "Clarify the short-term payoff or protection.",
1750
- "Clarify the long-term cost.",
1751
- "Name the preferred alternative response."
2056
+ "Start from one recent concrete example before generalizing the loop.",
2057
+ "Identify the typical cue, vulnerability, or context that makes the loop more likely.",
2058
+ "Reflect back the sequence of thoughts, feelings, body state, and visible behavior once it starts.",
2059
+ "Clarify the short-term payoff, protection, or escape function.",
2060
+ "Clarify the long-term cost to the self, relationships, work, or values.",
2061
+ "Ask what a slightly more workable response would look like.",
2062
+ "Notice adjacent beliefs, schema themes, modes, or values that should be linked or saved separately."
1752
2063
  ],
1753
2064
  requiredForCreate: ["title"],
1754
2065
  highValueOptionalFields: [
@@ -1763,26 +2074,72 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
1763
2074
  "linkedValueIds"
1764
2075
  ],
1765
2076
  exampleQuestions: [
1766
- "What usually sets this loop off?",
1767
- "What do you tend to do next, outwardly or inwardly?",
2077
+ "Can we slow this down using one recent example first?",
2078
+ "What usually sets this loop off, and what was going on just before it started?",
2079
+ "What do you notice in your thoughts, body, and actions once it gets going?",
1768
2080
  "What does that move do for you immediately?",
1769
2081
  "What does it cost you later?",
2082
+ "What belief, rule, or vulnerable part seems to get activated inside this loop?",
1770
2083
  "If this loop loosened a little, what response would you want to make instead?"
1771
2084
  ],
1772
2085
  notes: [
1773
2086
  "A pattern is usually the best Psyche container for functional analysis.",
1774
- "If the user is describing one specific episode rather than a repeated loop, prefer a trigger report."
2087
+ "If the user is describing one specific episode rather than a repeated loop, prefer a trigger report.",
2088
+ "Reflect before the next question, and avoid interrogating through the schema fields in order.",
2089
+ "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."
2090
+ ]
2091
+ },
2092
+ {
2093
+ focus: "behavior",
2094
+ 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.",
2095
+ 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.",
2096
+ askSequence: [
2097
+ "Start with a recent example of the behavior in context.",
2098
+ "Name what the user actually does or tends to do.",
2099
+ "Clarify what cues, urges, or situations pull the behavior online.",
2100
+ "Clarify the short-term payoff or relief.",
2101
+ "Clarify the long-term cost or price.",
2102
+ "Decide whether the behavior is away, committed, or recovery.",
2103
+ "Identify a replacement move or repair plan if the user wants one."
2104
+ ],
2105
+ requiredForCreate: ["kind", "title"],
2106
+ highValueOptionalFields: [
2107
+ "description",
2108
+ "commonCues",
2109
+ "urgeStory",
2110
+ "shortTermPayoff",
2111
+ "longTermCost",
2112
+ "replacementMove",
2113
+ "repairPlan",
2114
+ "linkedPatternIds",
2115
+ "linkedValueIds",
2116
+ "linkedSchemaIds",
2117
+ "linkedModeIds"
2118
+ ],
2119
+ exampleQuestions: [
2120
+ "What does this behavior actually look like when it happens?",
2121
+ "What usually pulls you toward it?",
2122
+ "What does it do for you in the moment?",
2123
+ "What cost shows up later?",
2124
+ "Would you call this an away move, a committed move, or a recovery move?",
2125
+ "If you wanted another option available, what would it be?"
2126
+ ],
2127
+ notes: [
2128
+ "Keep the user close to observable behavior rather than jumping straight to labels.",
2129
+ "When the behavior clearly belongs inside a larger loop, suggest linking or also mapping the related behavior_pattern.",
2130
+ "If the user asks for understanding before storage, ask about the recent example and function of the move before classifying it."
1775
2131
  ]
1776
2132
  },
1777
2133
  {
1778
2134
  focus: "belief_entry",
1779
2135
  useWhen: "Use for a belief, rule, or self-statement that keeps showing up in reactions, especially when the user can phrase it as a sentence.",
1780
- coachingGoal: "Turn implicit self-talk or a likely schema theme into one explicit belief statement that can be tested and linked to patterns, reports, and modes.",
2136
+ coachingGoal: "Turn implicit self-talk or a likely schema theme into one explicit belief statement that can be tested and linked to patterns, reports, and modes without forcing the user into a debate too early.",
1781
2137
  askSequence: [
1782
- "Capture the belief in the user's own words.",
2138
+ "Reflect the likely belief in the user's own words and ask for confirmation or correction.",
1783
2139
  "Decide whether it is absolute or conditional.",
1784
2140
  "Estimate how true it feels from 0 to 100.",
1785
2141
  "Collect evidence for and evidence against.",
2142
+ "Notice where the belief may have been learned or reinforced.",
1786
2143
  "Offer a more flexible alternative belief.",
1787
2144
  "Link a schemaId only when a real schema catalog match is known."
1788
2145
  ],
@@ -1799,32 +2156,39 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
1799
2156
  "linkedModeIds"
1800
2157
  ],
1801
2158
  exampleQuestions: [
1802
- "What is the sentence your mind seems to be pushing here?",
2159
+ "If we turned that reaction into one sentence, what would it sound like?",
1803
2160
  "Is it more of an always/never belief, or an if-then rule?",
1804
2161
  "How true does it feel right now from 0 to 100?",
1805
2162
  "What seems to support it, and what weakens it?",
2163
+ "Where do you think you learned or rehearsed that rule?",
1806
2164
  "What would a more flexible alternative sound like?"
1807
2165
  ],
1808
2166
  notes: [
1809
2167
  "Schema catalog entries are reference concepts; belief_entry is the user-owned record.",
1810
- "If no schema catalog match is known, omit schemaId rather than inventing one."
2168
+ "If no schema catalog match is known, omit schemaId rather than inventing one.",
2169
+ "Do not argue the user out of the belief. Reflect it, understand its function, and then collaboratively test for flexibility."
1811
2170
  ]
1812
2171
  },
1813
2172
  {
1814
2173
  focus: "mode_profile",
1815
2174
  useWhen: "Use when the user is describing a recurring part-state, protector, critic, vulnerable child state, or healthy adult stance.",
1816
- coachingGoal: "Help the user describe what the mode is trying to do, what it fears, and how it presents, rather than reducing it to a label only.",
2175
+ coachingGoal: "Help the user describe how the mode shows up, what it is trying to do, what it fears, and what burden it carries, rather than reducing it to a label only.",
1817
2176
  askSequence: [
1818
- "Choose the mode family first.",
1819
- "Name the mode.",
1820
- "Describe the felt persona or imagery.",
2177
+ "Start with a recent moment when this part-state took over.",
2178
+ "Choose the mode family once the lived description is clearer.",
2179
+ "Name the mode in the user's language.",
2180
+ "Describe the felt persona, body posture, imagery, or symbolic form.",
1821
2181
  "Clarify its fear, burden, and protective job.",
1822
- "Optionally note origin context and linked patterns or behaviors."
2182
+ "Explore when it first became necessary or familiar.",
2183
+ "Notice linked patterns, behaviors, values, and what a healthy-adult response would need to do."
1823
2184
  ],
1824
2185
  requiredForCreate: ["family", "title"],
1825
2186
  highValueOptionalFields: [
2187
+ "archetype",
1826
2188
  "persona",
1827
2189
  "imagery",
2190
+ "symbolicForm",
2191
+ "facialExpression",
1828
2192
  "fear",
1829
2193
  "burden",
1830
2194
  "protectiveJob",
@@ -1834,25 +2198,54 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
1834
2198
  "linkedValueIds"
1835
2199
  ],
1836
2200
  exampleQuestions: [
2201
+ "When this part shows up, what is it like from the inside?",
1837
2202
  "What kind of part does this feel like: coping, child, critic-parent, healthy-adult, or happy-child?",
1838
2203
  "If you gave this mode a name, what would it be?",
1839
2204
  "What is it afraid would happen if it stopped doing its job?",
1840
- "What burden or pain does it seem to carry?"
2205
+ "What burden or pain does it seem to carry?",
2206
+ "When do you remember needing this way of coping or surviving?"
1841
2207
  ],
1842
2208
  notes: [
1843
2209
  "Mode profiles are durable parts descriptions.",
1844
- "Mode guide sessions are the guided reasoning process that can lead toward a mode profile."
2210
+ "Mode guide sessions are the guided reasoning process that can lead toward a mode profile.",
2211
+ "Do not overpathologize. The point is to understand the part's job and cost, then increase choice.",
2212
+ "If the user asks to understand the mode first, start from a recent moment and ask what the part is trying to do before you name it."
2213
+ ]
2214
+ },
2215
+ {
2216
+ focus: "mode_guide_session",
2217
+ useWhen: "Use when the user is in a live reaction or is unsure which mode is active and needs a gentle structured exploration before committing to a durable mode profile.",
2218
+ coachingGoal: "Guide a present-moment inquiry that names the likely active mode, gathers the user's answers cleanly, and leaves a traceable bridge toward later mode work.",
2219
+ askSequence: [
2220
+ "Anchor the exploration in one current or recent situation.",
2221
+ "Ask what the part is feeling, saying, trying to stop, or trying to make happen.",
2222
+ "Ask what the part fears and what it seems to need.",
2223
+ "Reflect the answers back in plain language before suggesting any candidate mode labels.",
2224
+ "Offer one or two candidate interpretations only after enough evidence is present."
2225
+ ],
2226
+ requiredForCreate: ["summary", "answers"],
2227
+ highValueOptionalFields: [],
2228
+ exampleQuestions: [
2229
+ "What just happened that brought this up right now?",
2230
+ "If this part had a voice, what would it be saying?",
2231
+ "What is it trying to protect you from?",
2232
+ "What does it seem to need from you or from someone else?",
2233
+ "Would it be helpful if I suggest one or two possible mode labels, with reasons?"
2234
+ ],
2235
+ notes: [
2236
+ "A mode_guide_session is the exploration worksheet, not the final identity claim.",
2237
+ "Store the user's answers faithfully and keep interpretations tentative unless the user wants a durable mode_profile."
1845
2238
  ]
1846
2239
  },
1847
2240
  {
1848
2241
  focus: "trigger_report",
1849
2242
  useWhen: "Use for one specific emotionally meaningful incident that should be mapped from situation through emotions, thoughts, behaviors, consequences, and next moves.",
1850
- coachingGoal: "Help the user build a clear incident chain with enough structure to learn from one episode.",
2243
+ coachingGoal: "Help the user build a clear incident chain with enough structure to learn from one episode while staying grounded and not rushing past the user's felt experience.",
1851
2244
  askSequence: [
1852
- "Name the incident briefly.",
2245
+ "Name the incident briefly and anchor it in one concrete sequence.",
1853
2246
  "Describe what happened in the situation.",
1854
2247
  "Capture emotions and intensity.",
1855
- "Capture thoughts or belief-linked interpretations.",
2248
+ "Capture thoughts, meanings, or belief-linked interpretations.",
1856
2249
  "Capture behaviors and immediate coping moves.",
1857
2250
  "Capture short-term and long-term consequences.",
1858
2251
  "Identify next moves and linked patterns, beliefs, modes, values, or tasks."
@@ -1880,15 +2273,29 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
1880
2273
  "What thoughts or meanings showed up?",
1881
2274
  "What did you do next?",
1882
2275
  "What did that do for you short term, and what did it cost later?",
2276
+ "What pattern, belief, or part do you think was most active here?",
1883
2277
  "What would be the next good move now?"
1884
2278
  ],
1885
2279
  notes: [
1886
2280
  "Use eventTypeId only when a known event taxonomy item fits; otherwise use customEventType.",
1887
- "Use emotionDefinitionId only when a known emotion definition fits; otherwise keep the raw label."
2281
+ "Use emotionDefinitionId only when a known emotion definition fits; otherwise keep the raw label.",
2282
+ "If the user becomes overwhelmed, slow down, summarize, and return to one segment of the chain at a time instead of pushing for the full report in one turn."
1888
2283
  ]
1889
2284
  }
1890
2285
  ];
1891
2286
  const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
2287
+ {
2288
+ toolName: "forge_get_user_directory",
2289
+ summary: "Read the live human/bot directory and directional relationship graph.",
2290
+ whenToUse: "Use before multi-user planning, cross-owner linking, or user-aware search so you know which humans and bots exist and what the current edge rights look like.",
2291
+ inputShape: "{}",
2292
+ requiredFields: [],
2293
+ notes: [
2294
+ "The relationship graph is directional: subject -> target describes what the subject can see or do to the target.",
2295
+ "The current default is permissive, but agents should still inspect the graph before assuming future narrower access."
2296
+ ],
2297
+ example: "{}"
2298
+ },
1892
2299
  {
1893
2300
  toolName: "forge_search_entities",
1894
2301
  summary: "Search Forge entities before create or update.",
@@ -1970,6 +2377,161 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
1970
2377
  notes: ["Restore only works for soft-deleted entities."],
1971
2378
  example: '{"operations":[{"entityType":"goal","id":"goal_123","clientRef":"goal-restore-1"}]}'
1972
2379
  },
2380
+ {
2381
+ toolName: "forge_get_wiki_settings",
2382
+ summary: "Read the current wiki spaces plus enabled LLM and embedding profiles.",
2383
+ whenToUse: "Use before semantic wiki search, ingest, or wiki writes so the agent knows which spaces and profiles exist.",
2384
+ inputShape: "{}",
2385
+ requiredFields: [],
2386
+ notes: [
2387
+ "Semantic search is optional and profile-driven.",
2388
+ "The wiki is file-first, so spaces map to local vault directories."
2389
+ ],
2390
+ example: "{}"
2391
+ },
2392
+ {
2393
+ toolName: "forge_list_wiki_pages",
2394
+ summary: "List wiki and evidence pages inside one space.",
2395
+ whenToUse: "Use when browsing a space catalog, choosing a page to open, or building a crawl plan without ranking search results yet.",
2396
+ inputShape: '{ spaceId?: string, kind?: "wiki"|"evidence", limit?: integer }',
2397
+ requiredFields: [],
2398
+ notes: [
2399
+ "This returns the explicit page catalog, not a search-ranked result list.",
2400
+ "Use forge_search_wiki when recall or ranking matters."
2401
+ ],
2402
+ example: '{"spaceId":"wiki_space_shared","kind":"wiki","limit":100}'
2403
+ },
2404
+ {
2405
+ toolName: "forge_get_wiki_page",
2406
+ summary: "Read one wiki page with backlinks, source notes, and attached assets.",
2407
+ whenToUse: "Use after page discovery when an agent needs the full wiki context for one page.",
2408
+ inputShape: "{ pageId: string }",
2409
+ requiredFields: ["pageId"],
2410
+ notes: [
2411
+ "The detail payload includes backlinks and linked media assets.",
2412
+ "Forge entity links remain on the page.links field."
2413
+ ],
2414
+ example: '{"pageId":"note_123"}'
2415
+ },
2416
+ {
2417
+ toolName: "forge_search_wiki",
2418
+ summary: "Search the wiki with text, entity, semantic, or hybrid retrieval.",
2419
+ whenToUse: "Use when the agent needs recall across the explicit wiki memory surface instead of only structured entities.",
2420
+ inputShape: '{ spaceId?: string, kind?: "wiki"|"evidence", mode?: "text"|"semantic"|"entity"|"hybrid", query?: string, profileId?: string, linkedEntity?: { entityType, entityId }, limit?: integer }',
2421
+ requiredFields: [],
2422
+ notes: [
2423
+ "Hybrid search combines exact slug or title matches, FTS, entity links, and optional embeddings.",
2424
+ "If no embedding profile is configured, semantic and hybrid fall back to non-vector signals."
2425
+ ],
2426
+ example: '{"spaceId":"wiki_space_shared","mode":"hybrid","query":"landing page inspiration","limit":12}'
2427
+ },
2428
+ {
2429
+ toolName: "forge_upsert_wiki_page",
2430
+ summary: "Create a new wiki page or update an existing one through the file-backed wiki surface.",
2431
+ whenToUse: "Use when the user explicitly wants wiki memory persisted or reorganized.",
2432
+ inputShape: '{ pageId?: string, kind?: "wiki"|"evidence", title: string, slug?: string, summary?: string, aliases?: string[], contentMarkdown: string, author?: string|null, tags?: string[], spaceId?: string, frontmatter?: object, links?: Array<{ entityType, entityId, anchorKey? }> }',
2433
+ requiredFields: ["title", "contentMarkdown"],
2434
+ notes: [
2435
+ "When pageId is omitted, Forge creates a new page.",
2436
+ "When pageId is present, Forge patches the existing page and rewrites the canonical file."
2437
+ ],
2438
+ example: '{"title":"Taste map","contentMarkdown":"# Taste map\\n\\n[[forge:goal:goal_123|Core goal]] influences this page.","spaceId":"wiki_space_shared"}'
2439
+ },
2440
+ {
2441
+ toolName: "forge_get_wiki_health",
2442
+ summary: "Read wiki maintenance signals such as unresolved links, orphan pages, missing summaries, raw-source counts, and the generated index path.",
2443
+ whenToUse: "Use for memory quality checks, cleanup passes, or before asking an LLM to lint the wiki.",
2444
+ inputShape: "{ spaceId?: string }",
2445
+ requiredFields: [],
2446
+ notes: [
2447
+ "This is the explicit health surface for the file-first wiki vault.",
2448
+ "Use it before proposing cleanup work or auto-maintenance."
2449
+ ],
2450
+ example: '{"spaceId":"wiki_space_shared"}'
2451
+ },
2452
+ {
2453
+ toolName: "forge_sync_wiki_vault",
2454
+ summary: "Resync Markdown files from the local wiki vault into Forge metadata.",
2455
+ whenToUse: "Use after out-of-band file edits or imported file changes that should be reflected back in Forge.",
2456
+ inputShape: "{ spaceId?: string }",
2457
+ requiredFields: [],
2458
+ notes: [
2459
+ "Forge treats the vault as a first-class local artifact, so this route is the bridge back into app metadata."
2460
+ ],
2461
+ example: '{"spaceId":"wiki_space_shared"}'
2462
+ },
2463
+ {
2464
+ toolName: "forge_reindex_wiki_embeddings",
2465
+ summary: "Recompute wiki embedding chunks for one space and optional profile.",
2466
+ whenToUse: "Use after large wiki edits or when a new embedding profile is enabled.",
2467
+ inputShape: "{ spaceId?: string, profileId?: string }",
2468
+ requiredFields: [],
2469
+ notes: [
2470
+ "Only enabled embedding profiles are indexed.",
2471
+ "Reindexing does not modify the markdown files themselves."
2472
+ ],
2473
+ example: '{"spaceId":"wiki_space_shared","profileId":"wiki_embed_123"}'
2474
+ },
2475
+ {
2476
+ toolName: "forge_ingest_wiki_source",
2477
+ summary: "Ingest raw text, local files, or URLs into the wiki, preserving a raw source artifact and returning page plus proposal outputs.",
2478
+ whenToUse: "Use when the operator wants source material compiled into file-first wiki memory and optional Forge-entity proposals.",
2479
+ inputShape: '{ spaceId?: string, titleHint?: string, sourceKind: "raw_text"|"local_path"|"url", sourceText?: string, sourcePath?: string, sourceUrl?: string, mimeType?: string, llmProfileId?: string, parseStrategy?: "auto"|"text_only"|"multimodal", entityProposalMode?: "none"|"suggest", createAsKind?: "wiki"|"evidence", linkedEntityHints?: Array<{ entityType, entityId, anchorKey? }> }',
2480
+ requiredFields: ["sourceKind", "sourceText/sourcePath/sourceUrl"],
2481
+ notes: [
2482
+ "Forge preserves a raw artifact under the wiki space's raw directory.",
2483
+ "Entity proposals are suggestions only; they are not auto-applied."
2484
+ ],
2485
+ example: '{"sourceKind":"url","sourceUrl":"https://example.com/article","titleHint":"Research import","parseStrategy":"auto","entityProposalMode":"suggest"}'
2486
+ },
2487
+ {
2488
+ toolName: "forge_get_sleep_overview",
2489
+ summary: "Read the sleep surface with recent nights, scores, regularity, stage averages, and linked reflective context.",
2490
+ whenToUse: "Use when the operator wants to review sleep patterns or when an agent needs sleep context before planning or coaching.",
2491
+ inputShape: "{ userIds?: string[] }",
2492
+ requiredFields: [],
2493
+ notes: [
2494
+ "Sleep sessions are first-class Forge health records and can link back to goals, projects, tasks, habits, notes, and Psyche entities.",
2495
+ "This read model is multi-user aware through userIds."
2496
+ ],
2497
+ example: '{"userIds":["user_operator","user_hermes"]}'
2498
+ },
2499
+ {
2500
+ toolName: "forge_get_sports_overview",
2501
+ summary: "Read the sports surface with workout volume, workout types, effort signals, and linked session context.",
2502
+ whenToUse: "Use when the operator wants training context, habit-generated workout visibility, or workout review before planning.",
2503
+ inputShape: "{ userIds?: string[] }",
2504
+ requiredFields: [],
2505
+ notes: [
2506
+ "The API path stays /api/v1/health/fitness even though the UI route is /sports.",
2507
+ "Habit-generated and imported workouts reconcile into the same workout record model."
2508
+ ],
2509
+ example: '{"userIds":["user_operator"]}'
2510
+ },
2511
+ {
2512
+ toolName: "forge_update_sleep_session",
2513
+ summary: "Patch one sleep session with reflective notes, tags, or linked Forge context.",
2514
+ whenToUse: "Use after reviewing a specific night when the operator wants richer context stored on that sleep record.",
2515
+ inputShape: "{ sleepId: string, qualitySummary?: string, notes?: string, tags?: string[], links?: Array<{ entityType, entityId, relationshipType? }> }",
2516
+ requiredFields: ["sleepId"],
2517
+ notes: [
2518
+ "Use this to attach the night to goals, projects, habits, notes, or Psyche context without editing the raw imported timestamps.",
2519
+ "Links keep sleep review connected to the broader Forge graph."
2520
+ ],
2521
+ example: '{"sleepId":"sleep_123","qualitySummary":"Fell asleep late after travel but recovered well.","tags":["travel","recovery"],"links":[{"entityType":"habit","entityId":"habit_sleep_hygiene","relationshipType":"supports"}]}'
2522
+ },
2523
+ {
2524
+ toolName: "forge_update_workout_session",
2525
+ summary: "Patch one workout session with subjective effort, mood, meaning, tags, or linked Forge context.",
2526
+ whenToUse: "Use after reviewing one sports session when the operator wants the workout record to carry narrative or planning context.",
2527
+ inputShape: "{ workoutId: string, subjectiveEffort?: integer|null, moodBefore?: string, moodAfter?: string, meaningText?: string, plannedContext?: string, socialContext?: string, tags?: string[], links?: Array<{ entityType, entityId, relationshipType? }> }",
2528
+ requiredFields: ["workoutId"],
2529
+ notes: [
2530
+ "Use this for subjective or linked-context metadata, not for rewriting the raw imported workout duration or calories.",
2531
+ "This is the correct path for both imported HealthKit workouts and habit-generated sports sessions."
2532
+ ],
2533
+ example: '{"workoutId":"workout_123","subjectiveEffort":7,"meaningText":"Protected recovery and sleep rhythm after a heavy workday.","tags":["recovery","sleep-support"],"links":[{"entityType":"project","entityId":"project_endurance_reset","relationshipType":"supports"}]}'
2534
+ },
1973
2535
  {
1974
2536
  toolName: "forge_get_calendar_overview",
1975
2537
  summary: "Read connected calendars, Forge-native events, mirrored events, recurring work blocks, and task timeboxes together.",
@@ -2212,6 +2774,9 @@ function buildAgentOnboardingPayload(request) {
2212
2774
  task: "A concrete actionable work item. Task status is board state, not proof of live work.",
2213
2775
  taskRun: "A live work session attached to a task. Start, heartbeat, focus, complete, and release runs instead of faking work with status alone.",
2214
2776
  note: "A Markdown work note that can link to one or many entities. Use notes for progress evidence, context, and close-out summaries.",
2777
+ wiki: "Forge Wiki is the file-first memory layer: local Markdown pages plus media, backlinks, optional embeddings, explicit spaces, and structured links back to Forge entities.",
2778
+ sleepSession: "A sleep session is a first-class health record with timing, sleep and bed duration, stage breakdown, recovery metrics, annotations, and Forge links back to planning or Psyche context.",
2779
+ workoutSession: "A workout session is a first-class sports record imported from HealthKit or generated from a habit. It holds workout type, timing, energy or distance when available, subjective effort, narrative context, and Forge links.",
2215
2780
  insight: "An agent-authored observation or recommendation grounded in Forge data.",
2216
2781
  calendar: "A connected calendar source mirrored into Forge. Calendar state combines provider events, recurring work blocks, and task timeboxes.",
2217
2782
  workBlock: "A recurring half-day or custom time window such as Main Activity, Secondary Activity, Third Activity, Rest, Holiday, or Custom. Work blocks can allow or block work by default, can define active date bounds, and remain editable through the calendar surface.",
@@ -2231,10 +2796,17 @@ function buildAgentOnboardingPayload(request) {
2231
2796
  triggerReport: "A trigger report is the one-episode incident chain: situation, emotions, thoughts, behaviors, consequences, extra mode labels, schema themes, and next moves."
2232
2797
  },
2233
2798
  psycheCoachingPlaybooks: AGENT_ONBOARDING_PSYCHE_PLAYBOOKS,
2799
+ conversationRules: AGENT_ONBOARDING_CONVERSATION_RULES,
2800
+ entityConversationPlaybooks: AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS,
2234
2801
  relationshipModel: [
2802
+ "Every Forge record belongs to one typed user owner: either human or bot.",
2803
+ "Read routes may scope to one user with userId or to several users with repeated userIds.",
2804
+ "Ownership and linkage are separate: a human-owned project can link to bot-owned tasks, strategies, notes, or insights.",
2235
2805
  "Goals are the top-level strategic layer.",
2236
2806
  "Projects belong to one goal through goalId.",
2237
2807
  "Tasks can belong to a goal, a project, both, or neither.",
2808
+ "Strategies can target one or many goals or projects while sequencing project and task nodes through a directed acyclic graph.",
2809
+ "A strategy remains editable until it is locked. Once locked, the plan becomes a contract and graph-shape edits should stop until the strategy is explicitly unlocked.",
2238
2810
  "Habits are recurring records that can connect directly to goals, projects, tasks, and durable Psyche entities.",
2239
2811
  "Task runs represent live work sessions on tasks and are separate from task status.",
2240
2812
  "Notes can link to one or many entities and are the canonical place for Markdown progress context or close-out evidence.",
@@ -2242,12 +2814,81 @@ function buildAgentOnboardingPayload(request) {
2242
2814
  "Behavior patterns, behaviors, beliefs, modes, and trigger reports cross-link to describe one reflective model rather than isolated records.",
2243
2815
  "Insights can point at one entity, but they exist to capture interpretation or advice rather than raw work items."
2244
2816
  ],
2817
+ multiUserModel: {
2818
+ summary: "Forge is multi-user by default. Humans and bots share one entity graph, with explicit ownership on every record and directional relationship settings between every pair of users.",
2819
+ defaultUserScopeBehavior: "If no user scope is provided, Forge returns all visible users. Use userId or repeated userIds when an agent should focus on one owner namespace or on a specific human/bot slice.",
2820
+ routeScoping: [
2821
+ "List and overview routes accept userId or repeated userIds to narrow the response to one or many owners.",
2822
+ "Entity detail routes remain globally addressable by id because ownership is metadata, not a separate table namespace.",
2823
+ "Mixed-entity search should include userIds whenever duplicate risk depends on owner identity."
2824
+ ],
2825
+ relationshipGraphDefaults: [
2826
+ "The directional user graph starts fully open: all users can discover, read, search, coordinate with, link to, and affect each other.",
2827
+ "Each edge is directional. A -> B defines what A can see or do to B, while B -> A is configured separately.",
2828
+ "Each directional edge now explicitly carries see, message, share-context, plan, and affect rights so the UI can tighten one lane without rewriting the entity model."
2829
+ ]
2830
+ },
2831
+ strategyContractModel: {
2832
+ draftSummary: "Strategies begin as editable drafts. Agents may save and refine incomplete drafts while the plan is still being negotiated.",
2833
+ lockSummary: "Setting isLocked to true turns the strategy into a contract. Locking now requires a real target plus an overview or end-state description, and then the sequencing graph, targets, linked entities, and descriptive plan fields should be treated as frozen until explicitly unlocked.",
2834
+ unlockSummary: "Unlocking a strategy reopens normal editing. Use this only when the human wants to renegotiate the plan rather than merely update execution status.",
2835
+ alignmentSummary: "Alignment is about executing the agreed strategy faithfully, not merely finishing isolated work. Forge therefore scores coverage, order, scope discipline, and quality separately before producing one alignment score.",
2836
+ metricBreakdown: [
2837
+ "Agreed work moving: are the planned steps being done at all, regardless of order",
2838
+ "Order respected: are steps happening in the agreed sequence instead of jumping ahead",
2839
+ "Scope held: is other unagreed work leaking into the strategy scope",
2840
+ "End-state satisfaction: are the targets landing cleanly without too many blocked nodes",
2841
+ "Target progress plus off-plan counts: is the contract actually reaching the intended end state"
2842
+ ]
2843
+ },
2245
2844
  entityCatalog: AGENT_ONBOARDING_ENTITY_CATALOG,
2246
2845
  toolInputCatalog: AGENT_ONBOARDING_TOOL_INPUT_CATALOG,
2846
+ connectionGuides: {
2847
+ openclaw: {
2848
+ label: "OpenClaw",
2849
+ installSteps: [
2850
+ "Install the Forge plugin from the repo or published package.",
2851
+ "Restart the OpenClaw gateway so the tool surface and UI proxy routes refresh.",
2852
+ "Open Forge Settings -> Agents to issue or rotate a managed token when remote scoped auth is needed."
2853
+ ],
2854
+ verifyCommands: [
2855
+ `curl -s ${origin}/api/v1/health`,
2856
+ "openclaw plugins install ./projects/forge",
2857
+ "openclaw gateway restart"
2858
+ ],
2859
+ configNotes: [
2860
+ "Localhost and Tailscale targets can usually use the operator-session path without a long-lived token.",
2861
+ "Create each agent as a Forge bot user, then use userId or userIds in tool inputs whenever the agent should focus on one human, one bot, or a specific collaboration slice."
2862
+ ]
2863
+ },
2864
+ hermes: {
2865
+ label: "Hermes",
2866
+ installSteps: [
2867
+ "Install forge-hermes-plugin into the Python environment Hermes actually runs.",
2868
+ "Let Hermes load the Forge plugin and bundled skill pack on startup.",
2869
+ "Use Forge Settings -> Agents if Hermes needs a managed token for remote or durable access."
2870
+ ],
2871
+ verifyCommands: [
2872
+ "python -m pip show forge-hermes-plugin",
2873
+ "~/.hermes/hermes-agent/venv/bin/python -m pip show forge-hermes-plugin",
2874
+ `curl -s ${origin}/api/v1/health`
2875
+ ],
2876
+ configNotes: [
2877
+ "Hermes keeps its durable Forge config under ~/.hermes/forge/config.json.",
2878
+ "Hermes uses the same multi-user scoping rules and should pass userIds intentionally when working across humans and bots.",
2879
+ "The Forge relationship graph still decides whether Hermes may see, message, plan for, or affect another owner."
2880
+ ]
2881
+ }
2882
+ },
2247
2883
  verificationPaths: {
2248
2884
  context: "/api/v1/context",
2249
2885
  xpMetrics: "/api/v1/metrics/xp",
2250
2886
  weeklyReview: "/api/v1/reviews/weekly",
2887
+ sleepOverview: "/api/v1/health/sleep",
2888
+ sportsOverview: "/api/v1/health/fitness",
2889
+ wikiSettings: "/api/v1/wiki/settings",
2890
+ wikiSearch: "/api/v1/wiki/search",
2891
+ wikiHealth: "/api/v1/wiki/health",
2251
2892
  calendarOverview: "/api/v1/calendar/overview",
2252
2893
  settingsBin: "/api/v1/settings/bin",
2253
2894
  batchSearch: "/api/v1/entities/search",
@@ -2258,9 +2899,12 @@ function buildAgentOnboardingPayload(request) {
2258
2899
  recommendedPluginTools: {
2259
2900
  bootstrap: ["forge_get_operator_overview"],
2260
2901
  readModels: [
2902
+ "forge_get_user_directory",
2261
2903
  "forge_get_operator_context",
2262
2904
  "forge_get_current_work",
2263
2905
  "forge_get_psyche_overview",
2906
+ "forge_get_sleep_overview",
2907
+ "forge_get_sports_overview",
2264
2908
  "forge_get_xp_metrics",
2265
2909
  "forge_get_weekly_review"
2266
2910
  ],
@@ -2272,6 +2916,23 @@ function buildAgentOnboardingPayload(request) {
2272
2916
  "forge_delete_entities",
2273
2917
  "forge_restore_entities"
2274
2918
  ],
2919
+ wikiWorkflow: [
2920
+ "forge_get_wiki_settings",
2921
+ "forge_list_wiki_pages",
2922
+ "forge_get_wiki_page",
2923
+ "forge_search_wiki",
2924
+ "forge_upsert_wiki_page",
2925
+ "forge_get_wiki_health",
2926
+ "forge_sync_wiki_vault",
2927
+ "forge_reindex_wiki_embeddings",
2928
+ "forge_ingest_wiki_source"
2929
+ ],
2930
+ healthWorkflow: [
2931
+ "forge_get_sleep_overview",
2932
+ "forge_get_sports_overview",
2933
+ "forge_update_sleep_session",
2934
+ "forge_update_workout_session"
2935
+ ],
2275
2936
  rewardWorkflow: ["forge_grant_reward_bonus"],
2276
2937
  workWorkflow: [
2277
2938
  "forge_adjust_work_minutes",
@@ -2296,7 +2957,9 @@ function buildAgentOnboardingPayload(request) {
2296
2957
  conversationMode: "continue_main_discussion_first",
2297
2958
  saveSuggestionPlacement: "end_of_message",
2298
2959
  saveSuggestionTone: "gentle_optional",
2299
- maxQuestionsPerTurn: 3,
2960
+ maxQuestionsPerTurn: 1,
2961
+ 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.",
2962
+ 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.",
2300
2963
  duplicateCheckRoute: "/api/v1/entities/search",
2301
2964
  uiSuggestionRule: "offer_visual_ui_when_review_or_editing_would_be_easier",
2302
2965
  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.",
@@ -2476,30 +3139,91 @@ function shouldIncludeRuntimeProbe(headers) {
2476
3139
  }
2477
3140
  return typeof probeHeader === "string" && probeHeader.trim() === "1";
2478
3141
  }
2479
- function buildV1Context() {
2480
- const goals = listGoals();
2481
- const tasks = listTasks();
2482
- const habits = listHabits();
2483
- return {
2484
- meta: {
2485
- apiVersion: "v1",
2486
- transport: "rest+sse",
2487
- generatedAt: new Date().toISOString(),
2488
- backend: "forge-node-runtime",
2489
- mode: "transitional-node"
2490
- },
2491
- metrics: buildGamificationProfile(goals, tasks, habits),
2492
- dashboard: getDashboard(),
2493
- overview: getOverviewContext(),
2494
- today: getTodayContext(),
2495
- risk: getRiskContext(),
2496
- goals,
2497
- projects: listProjectSummaries(),
2498
- tags: listTags(),
2499
- tasks,
3142
+ function resolveScopedUserIds(query) {
3143
+ if (!query) {
3144
+ return undefined;
3145
+ }
3146
+ const values = [];
3147
+ const rawUserId = query.userId;
3148
+ const rawUserIds = query.userIds;
3149
+ if (typeof rawUserId === "string" && rawUserId.trim().length > 0) {
3150
+ values.push(rawUserId.trim());
3151
+ }
3152
+ const pushedRawUserIds = Array.isArray(rawUserIds)
3153
+ ? rawUserIds
3154
+ : rawUserIds === undefined
3155
+ ? []
3156
+ : [rawUserIds];
3157
+ for (const value of pushedRawUserIds) {
3158
+ if (typeof value !== "string") {
3159
+ continue;
3160
+ }
3161
+ for (const item of value.split(",")) {
3162
+ const trimmed = item.trim();
3163
+ if (trimmed) {
3164
+ values.push(trimmed);
3165
+ }
3166
+ }
3167
+ }
3168
+ const unique = Array.from(new Set(values));
3169
+ return unique.length > 0 ? unique : undefined;
3170
+ }
3171
+ function readRequestedUserIdFromBody(body) {
3172
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
3173
+ return undefined;
3174
+ }
3175
+ const value = body.userId;
3176
+ if (value === null) {
3177
+ return null;
3178
+ }
3179
+ if (typeof value === "string") {
3180
+ const trimmed = value.trim();
3181
+ return trimmed.length > 0 ? trimmed : null;
3182
+ }
3183
+ return undefined;
3184
+ }
3185
+ function syncEntityOwnerFromBody(options) {
3186
+ const requestedUserId = readRequestedUserIdFromBody(options.body);
3187
+ if (requestedUserId === undefined && !options.assignDefaultWhenMissing) {
3188
+ return;
3189
+ }
3190
+ const owner = resolveUserForMutation(requestedUserId, options.fallbackLabel);
3191
+ setEntityOwner(options.entityType, options.entityId, owner.id);
3192
+ }
3193
+ function buildV1Context(userIds) {
3194
+ const goals = filterOwnedEntities("goal", listGoals(), userIds);
3195
+ const tasks = filterOwnedEntities("task", listTasks(), userIds);
3196
+ const habits = filterOwnedEntities("habit", listHabits(), userIds);
3197
+ const users = listUsers();
3198
+ const selectedUsers = userIds && userIds.length > 0
3199
+ ? users.filter((user) => userIds.includes(user.id))
3200
+ : users;
3201
+ return {
3202
+ meta: {
3203
+ apiVersion: "v1",
3204
+ transport: "rest+sse",
3205
+ generatedAt: new Date().toISOString(),
3206
+ backend: "forge-node-runtime",
3207
+ mode: "transitional-node"
3208
+ },
3209
+ metrics: buildGamificationProfile(goals, tasks, habits),
3210
+ dashboard: getDashboard({ userIds }),
3211
+ overview: getOverviewContext(new Date(), { userIds }),
3212
+ today: getTodayContext(new Date(), { userIds }),
3213
+ risk: getRiskContext(new Date(), { userIds }),
3214
+ goals,
3215
+ projects: listProjectSummaries({ userIds }),
3216
+ tags: listTags(),
3217
+ tasks,
2500
3218
  habits,
3219
+ users,
3220
+ strategies: listStrategies({ userIds }),
3221
+ userScope: {
3222
+ selectedUserIds: userIds ?? [],
3223
+ selectedUsers
3224
+ },
2501
3225
  activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
2502
- activity: listActivityEvents({ limit: 25 })
3226
+ activity: getDashboard({ userIds }).recentActivity
2503
3227
  };
2504
3228
  }
2505
3229
  function buildXpMetricsPayload() {
@@ -2564,10 +3288,13 @@ function describeWorkAdjustment(input) {
2564
3288
  : `${appliedLabel} ${direction} from the tracked work total.`
2565
3289
  };
2566
3290
  }
2567
- function buildOperatorContext() {
2568
- const tasks = listTasks();
2569
- const dueHabits = listHabits({ dueToday: true }).slice(0, 12);
2570
- const activeProjects = listProjectSummaries({ status: "active" }).filter((project) => project.activeTaskCount > 0 || project.completedTaskCount > 0);
3291
+ function buildOperatorContext(userIds) {
3292
+ const tasks = filterOwnedEntities("task", listTasks(), userIds);
3293
+ const dueHabits = filterOwnedEntities("habit", listHabits({ dueToday: true }), userIds).slice(0, 12);
3294
+ const activeProjects = listProjectSummaries({
3295
+ status: "active",
3296
+ userIds
3297
+ }).filter((project) => project.activeTaskCount > 0 || project.completedTaskCount > 0);
2571
3298
  const focusTasks = tasks.filter((task) => task.status === "focus" || task.status === "in_progress");
2572
3299
  const recommendedNextTask = focusTasks[0] ??
2573
3300
  tasks.find((task) => task.status === "backlog") ??
@@ -2587,16 +3314,54 @@ function buildOperatorContext() {
2587
3314
  blocked: tasks.filter((task) => task.status === "blocked").slice(0, 20),
2588
3315
  done: tasks.filter((task) => task.status === "done").slice(0, 20)
2589
3316
  },
2590
- recentActivity: listActivityEvents({ limit: 20 }),
2591
- recentTaskRuns: listTaskRuns({ limit: 12 }),
3317
+ recentActivity: listActivityEvents({ limit: 20, userIds }),
3318
+ recentTaskRuns: listTaskRuns({ limit: 12, userIds }),
2592
3319
  recommendedNextTask,
2593
3320
  xp: buildXpMetricsPayload()
2594
3321
  };
2595
3322
  }
3323
+ function buildUserDirectoryPayload() {
3324
+ return {
3325
+ users: listUsers(),
3326
+ grants: listUserAccessGrants(),
3327
+ ownership: listUserOwnershipSummaries(),
3328
+ xp: listUserXpSummaries(),
3329
+ posture: {
3330
+ accessModel: "directional_graph",
3331
+ summary: "Forge now exposes a directional relationship graph between humans and bots. The current default stays maximally permissive: every user can discover, search, link to, view, and affect every other visible user until you narrow those edges.",
3332
+ futureReady: true
3333
+ }
3334
+ };
3335
+ }
3336
+ function parseRequestBody(parser, body) {
3337
+ let current = body;
3338
+ for (let depth = 0; depth < 2; depth += 1) {
3339
+ if (typeof current !== "string") {
3340
+ break;
3341
+ }
3342
+ const trimmed = current.trim();
3343
+ if (trimmed.length === 0) {
3344
+ return parser.parse({});
3345
+ }
3346
+ try {
3347
+ current = JSON.parse(trimmed);
3348
+ }
3349
+ catch {
3350
+ break;
3351
+ }
3352
+ }
3353
+ return parser.parse(current ?? {});
3354
+ }
2596
3355
  function buildOperatorOverviewRouteGuide() {
2597
3356
  return {
2598
3357
  preferredStart: "/api/v1/operator/overview",
2599
3358
  mainRoutes: [
3359
+ {
3360
+ id: "users_directory",
3361
+ path: "/api/v1/users + /api/v1/users/directory",
3362
+ summary: "User directory, ownership counts, and current human/bot sharing posture for multi-user routing and UI search.",
3363
+ requiredScope: null
3364
+ },
2600
3365
  {
2601
3366
  id: "context",
2602
3367
  path: "/api/v1/context",
@@ -2680,6 +3445,7 @@ function buildOperatorOverviewRouteGuide() {
2680
3445
  }
2681
3446
  function buildOperatorOverview(request) {
2682
3447
  const auth = parseRequestAuth(request.headers);
3448
+ const userIds = resolveScopedUserIds(request.query);
2683
3449
  const canReadPsyche = auth.token
2684
3450
  ? hasTokenScope(auth.token, "psyche.read")
2685
3451
  : true;
@@ -2690,10 +3456,10 @@ function buildOperatorOverview(request) {
2690
3456
  ];
2691
3457
  return {
2692
3458
  generatedAt: new Date().toISOString(),
2693
- snapshot: buildV1Context(),
2694
- operator: buildOperatorContext(),
3459
+ snapshot: buildV1Context(userIds),
3460
+ operator: buildOperatorContext(userIds),
2695
3461
  domains: listDomains(),
2696
- psyche: canReadPsyche ? getPsycheOverview() : null,
3462
+ psyche: canReadPsyche ? getPsycheOverview(userIds) : null,
2697
3463
  onboarding: buildAgentOnboardingPayload(request),
2698
3464
  capabilities: {
2699
3465
  tokenPresent: Boolean(auth.token),
@@ -2733,6 +3499,7 @@ export async function buildServer(options = {}) {
2733
3499
  configureDatabase({ dataRoot: runtimeConfig.dataRoot ?? undefined });
2734
3500
  configureDatabaseSeeding(options.seedDemoData ?? false);
2735
3501
  await managers.migration.initialize();
3502
+ ensureSystemUsers();
2736
3503
  const app = Fastify({
2737
3504
  logger: false,
2738
3505
  rewriteUrl: (request) => rewriteMountPath(request.url ?? "/")
@@ -2750,10 +3517,94 @@ export async function buildServer(options = {}) {
2750
3517
  },
2751
3518
  credentials: true
2752
3519
  });
3520
+ await app.register(multipart);
3521
+ enforceDiagnosticLogRetention({ force: true });
3522
+ const diagnosticRetentionTimer = setInterval(() => {
3523
+ try {
3524
+ enforceDiagnosticLogRetention({ force: true });
3525
+ }
3526
+ catch {
3527
+ // Diagnostics cleanup should never bring down the server loop.
3528
+ }
3529
+ }, DIAGNOSTIC_LOG_RETENTION_SWEEP_INTERVAL_MS);
3530
+ diagnosticRetentionTimer.unref?.();
2753
3531
  app.addHook("onClose", async () => {
3532
+ clearInterval(diagnosticRetentionTimer);
2754
3533
  taskRunWatchdog?.stop();
3534
+ await managers.backgroundJobs.stop();
3535
+ });
3536
+ const enqueueWikiIngestJob = (jobId) => {
3537
+ managers.backgroundJobs.enqueue({
3538
+ id: jobId,
3539
+ label: `Wiki ingest ${jobId}`,
3540
+ handler: async () => {
3541
+ await processWikiIngestJob(jobId, { llm: managers.llm });
3542
+ }
3543
+ });
3544
+ };
3545
+ for (const pendingJob of listWikiIngestJobs({ limit: 100 })) {
3546
+ if (["queued", "processing"].includes(pendingJob.job.status)) {
3547
+ enqueueWikiIngestJob(pendingJob.job.id);
3548
+ }
3549
+ }
3550
+ const shouldSkipAutomaticDiagnosticRoute = (url) => {
3551
+ if (!url) {
3552
+ return false;
3553
+ }
3554
+ return (url.startsWith("/api/v1/diagnostics/logs") ||
3555
+ url === "/api/health" ||
3556
+ url === "/api/v1/health" ||
3557
+ url.startsWith("/api/v1/events/meta"));
3558
+ };
3559
+ app.addHook("onRequest", async (request) => {
3560
+ request.diagnosticStartedAt =
3561
+ process.hrtime.bigint();
3562
+ });
3563
+ app.addHook("onResponse", async (request, reply) => {
3564
+ const routeUrl = request.routeOptions.url || request.url;
3565
+ if (shouldSkipAutomaticDiagnosticRoute(routeUrl)) {
3566
+ return;
3567
+ }
3568
+ const startedAt = request.diagnosticStartedAt;
3569
+ const durationMs = typeof startedAt === "bigint"
3570
+ ? Number(process.hrtime.bigint() - startedAt) / 1_000_000
3571
+ : null;
3572
+ const source = normalizeDiagnosticSource(request.headers["x-forge-source"]);
3573
+ try {
3574
+ recordDiagnosticLog({
3575
+ level: reply.statusCode >= 500
3576
+ ? "error"
3577
+ : reply.statusCode >= 400
3578
+ ? "warning"
3579
+ : "debug",
3580
+ source,
3581
+ scope: "api_request",
3582
+ eventKey: `http_${request.method.toLowerCase()}`,
3583
+ message: createDiagnosticMessage({
3584
+ method: request.method,
3585
+ route: routeUrl,
3586
+ statusCode: reply.statusCode
3587
+ }),
3588
+ route: routeUrl,
3589
+ requestId: request.id,
3590
+ details: {
3591
+ method: request.method,
3592
+ rawUrl: request.url,
3593
+ statusCode: reply.statusCode,
3594
+ durationMs: typeof durationMs === "number"
3595
+ ? Number(durationMs.toFixed(2))
3596
+ : null,
3597
+ userAgent: typeof request.headers["user-agent"] === "string"
3598
+ ? request.headers["user-agent"]
3599
+ : null
3600
+ }
3601
+ });
3602
+ }
3603
+ catch {
3604
+ // Avoid surfacing diagnostics failures as request failures.
3605
+ }
2755
3606
  });
2756
- app.setErrorHandler((error, _request, reply) => {
3607
+ app.setErrorHandler((error, request, reply) => {
2757
3608
  const validationIssues = error instanceof ZodError ? formatValidationIssues(error) : undefined;
2758
3609
  const statusCode = isHttpError(error)
2759
3610
  ? error.statusCode
@@ -2762,6 +3613,38 @@ export async function buildServer(options = {}) {
2762
3613
  : error instanceof ZodError
2763
3614
  ? 400
2764
3615
  : 500;
3616
+ const routeUrl = request.routeOptions.url || request.url;
3617
+ if (!shouldSkipAutomaticDiagnosticRoute(routeUrl)) {
3618
+ try {
3619
+ recordDiagnosticLog({
3620
+ level: statusCode >= 500 ? "error" : "warning",
3621
+ source: normalizeDiagnosticSource(request.headers["x-forge-source"]),
3622
+ scope: "api_error",
3623
+ eventKey: isHttpError(error)
3624
+ ? error.code
3625
+ : isManagerError(error)
3626
+ ? error.code
3627
+ : statusCode === 400
3628
+ ? "invalid_request"
3629
+ : "internal_error",
3630
+ message: getErrorMessage(error),
3631
+ route: routeUrl,
3632
+ functionName: "setErrorHandler",
3633
+ requestId: request.id,
3634
+ details: {
3635
+ statusCode,
3636
+ validationIssues: validationIssues?.map((issue) => ({
3637
+ path: issue.path,
3638
+ message: issue.message
3639
+ })) ?? [],
3640
+ error: serializeDiagnosticError(error)
3641
+ }
3642
+ });
3643
+ }
3644
+ catch {
3645
+ // Avoid cascading on the error path.
3646
+ }
3647
+ }
2765
3648
  reply.code(statusCode).send({
2766
3649
  code: isHttpError(error)
2767
3650
  ? error.code
@@ -2965,13 +3848,83 @@ export async function buildServer(options = {}) {
2965
3848
  revoked: managers.session.revokeCurrentSession(request.headers, reply)
2966
3849
  }));
2967
3850
  app.get("/api/v1/openapi.json", async () => buildOpenApiDocument());
2968
- app.get("/api/v1/context", async () => buildV1Context());
3851
+ app.get("/api/v1/context", async (request) => buildV1Context(resolveScopedUserIds(request.query)));
3852
+ app.get("/api/v1/health/overview", async (request) => ({
3853
+ overview: getCompanionOverview(resolveScopedUserIds(request.query))
3854
+ }));
3855
+ app.get("/api/v1/health/sleep", async (request) => ({
3856
+ sleep: getSleepViewData(resolveScopedUserIds(request.query))
3857
+ }));
3858
+ app.get("/api/v1/health/fitness", async (request) => ({
3859
+ fitness: getFitnessViewData(resolveScopedUserIds(request.query))
3860
+ }));
3861
+ app.post("/api/v1/health/pairing-sessions", async (request, reply) => {
3862
+ requireOperatorSession(request.headers, {
3863
+ route: "/api/v1/health/pairing-sessions"
3864
+ });
3865
+ reply.code(201);
3866
+ return createCompanionPairingSession(buildApiBaseUrl({
3867
+ protocol: request.protocol,
3868
+ headers: request.headers
3869
+ }), createCompanionPairingSessionSchema.parse(request.body ?? {}));
3870
+ });
3871
+ app.delete("/api/v1/health/pairing-sessions/:id", async (request, reply) => {
3872
+ const auth = requireOperatorSession(request.headers, {
3873
+ route: "/api/v1/health/pairing-sessions/:id"
3874
+ });
3875
+ const { id } = request.params;
3876
+ const session = revokeCompanionPairingSession(id, {
3877
+ actor: auth.actor ?? null,
3878
+ source: "ui"
3879
+ });
3880
+ if (!session) {
3881
+ reply.code(404);
3882
+ return { error: "Companion pairing session not found" };
3883
+ }
3884
+ return { session };
3885
+ });
3886
+ app.post("/api/v1/health/pairing-sessions/revoke-all", async (request) => {
3887
+ const auth = requireOperatorSession(request.headers, {
3888
+ route: "/api/v1/health/pairing-sessions/revoke-all"
3889
+ });
3890
+ return revokeAllCompanionPairingSessions(revokeAllCompanionPairingSessionsSchema.parse(request.body ?? {}), {
3891
+ actor: auth.actor ?? null,
3892
+ source: "ui"
3893
+ });
3894
+ });
3895
+ app.post("/api/v1/mobile/pairing/verify", async (request) => ({
3896
+ pairing: verifyCompanionPairing(verifyCompanionPairingSchema.parse(request.body ?? {}))
3897
+ }));
3898
+ app.post("/api/v1/mobile/healthkit/sync", async (request) => ({
3899
+ sync: ingestMobileHealthSync(mobileHealthSyncSchema.parse(request.body ?? {}))
3900
+ }));
3901
+ app.patch("/api/v1/health/workouts/:id", async (request, reply) => {
3902
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/health/workouts/:id" });
3903
+ const { id } = request.params;
3904
+ const workout = updateWorkoutMetadata(id, updateWorkoutMetadataSchema.parse(request.body ?? {}), toActivityContext(auth));
3905
+ if (!workout) {
3906
+ reply.code(404);
3907
+ return { error: "Workout session not found" };
3908
+ }
3909
+ return { workout };
3910
+ });
3911
+ app.patch("/api/v1/health/sleep/:id", async (request, reply) => {
3912
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/health/sleep/:id" });
3913
+ const { id } = request.params;
3914
+ const sleep = updateSleepMetadata(id, updateSleepMetadataSchema.parse(request.body ?? {}), toActivityContext(auth));
3915
+ if (!sleep) {
3916
+ reply.code(404);
3917
+ return { error: "Sleep session not found" };
3918
+ }
3919
+ return { sleep };
3920
+ });
2969
3921
  app.get("/api/v1/operator/context", async (request) => {
2970
3922
  requireOperatorSession(request.headers, {
2971
3923
  route: "/api/v1/operator/context"
2972
3924
  });
3925
+ const userIds = resolveScopedUserIds(request.query);
2973
3926
  return {
2974
- context: buildOperatorContext()
3927
+ context: buildOperatorContext(userIds)
2975
3928
  };
2976
3929
  });
2977
3930
  app.get("/api/v1/operator/overview", async (request) => {
@@ -2979,7 +3932,12 @@ export async function buildServer(options = {}) {
2979
3932
  route: "/api/v1/operator/overview"
2980
3933
  });
2981
3934
  return {
2982
- overview: buildOperatorOverview(request)
3935
+ overview: buildOperatorOverview({
3936
+ protocol: request.protocol,
3937
+ hostname: request.hostname,
3938
+ headers: request.headers,
3939
+ query: request.query
3940
+ })
2983
3941
  };
2984
3942
  });
2985
3943
  app.get("/api/v1/domains", async () => ({
@@ -2987,15 +3945,26 @@ export async function buildServer(options = {}) {
2987
3945
  }));
2988
3946
  app.get("/api/v1/psyche/overview", async (request) => {
2989
3947
  requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/overview" });
2990
- return { overview: getPsycheOverview() };
3948
+ const userIds = resolveScopedUserIds(request.query);
3949
+ return { overview: getPsycheOverview(userIds) };
2991
3950
  });
2992
3951
  app.get("/api/v1/psyche/values", async (request) => {
2993
3952
  requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/values" });
2994
- return { values: listPsycheValues() };
3953
+ const userIds = resolveScopedUserIds(request.query);
3954
+ return {
3955
+ values: filterOwnedEntities("psyche_value", listPsycheValues(), userIds)
3956
+ };
2995
3957
  });
2996
3958
  app.post("/api/v1/psyche/values", async (request, reply) => {
2997
3959
  const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/values" });
2998
3960
  const value = createPsycheValue(createPsycheValueSchema.parse(request.body ?? {}), toActivityContext(auth));
3961
+ syncEntityOwnerFromBody({
3962
+ entityType: "psyche_value",
3963
+ entityId: value.id,
3964
+ body: request.body,
3965
+ fallbackLabel: auth.actor,
3966
+ assignDefaultWhenMissing: true
3967
+ });
2999
3968
  reply.code(201);
3000
3969
  return { value };
3001
3970
  });
@@ -3017,6 +3986,11 @@ export async function buildServer(options = {}) {
3017
3986
  reply.code(404);
3018
3987
  return { error: "Psyche value not found" };
3019
3988
  }
3989
+ syncEntityOwnerFromBody({
3990
+ entityType: "psyche_value",
3991
+ entityId: value.id,
3992
+ body: request.body
3993
+ });
3020
3994
  return { value };
3021
3995
  });
3022
3996
  app.delete("/api/v1/psyche/values/:id", async (request, reply) => {
@@ -3031,11 +4005,21 @@ export async function buildServer(options = {}) {
3031
4005
  });
3032
4006
  app.get("/api/v1/psyche/patterns", async (request) => {
3033
4007
  requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/patterns" });
3034
- return { patterns: listBehaviorPatterns() };
4008
+ const userIds = resolveScopedUserIds(request.query);
4009
+ return {
4010
+ patterns: filterOwnedEntities("behavior_pattern", listBehaviorPatterns(), userIds)
4011
+ };
3035
4012
  });
3036
4013
  app.post("/api/v1/psyche/patterns", async (request, reply) => {
3037
4014
  const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/patterns" });
3038
4015
  const pattern = createBehaviorPattern(createBehaviorPatternSchema.parse(request.body ?? {}), toActivityContext(auth));
4016
+ syncEntityOwnerFromBody({
4017
+ entityType: "behavior_pattern",
4018
+ entityId: pattern.id,
4019
+ body: request.body,
4020
+ fallbackLabel: auth.actor,
4021
+ assignDefaultWhenMissing: true
4022
+ });
3039
4023
  reply.code(201);
3040
4024
  return { pattern };
3041
4025
  });
@@ -3047,6 +4031,11 @@ export async function buildServer(options = {}) {
3047
4031
  reply.code(404);
3048
4032
  return { error: "Behavior pattern not found" };
3049
4033
  }
4034
+ syncEntityOwnerFromBody({
4035
+ entityType: "behavior_pattern",
4036
+ entityId: pattern.id,
4037
+ body: request.body
4038
+ });
3050
4039
  return { pattern };
3051
4040
  });
3052
4041
  app.patch("/api/v1/psyche/patterns/:id", async (request, reply) => {
@@ -3071,11 +4060,21 @@ export async function buildServer(options = {}) {
3071
4060
  });
3072
4061
  app.get("/api/v1/psyche/behaviors", async (request) => {
3073
4062
  requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/behaviors" });
3074
- return { behaviors: listBehaviors() };
4063
+ const userIds = resolveScopedUserIds(request.query);
4064
+ return {
4065
+ behaviors: filterOwnedEntities("behavior", listBehaviors(), userIds)
4066
+ };
3075
4067
  });
3076
4068
  app.post("/api/v1/psyche/behaviors", async (request, reply) => {
3077
4069
  const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/behaviors" });
3078
4070
  const behavior = createBehavior(createBehaviorSchema.parse(request.body ?? {}), toActivityContext(auth));
4071
+ syncEntityOwnerFromBody({
4072
+ entityType: "behavior",
4073
+ entityId: behavior.id,
4074
+ body: request.body,
4075
+ fallbackLabel: auth.actor,
4076
+ assignDefaultWhenMissing: true
4077
+ });
3079
4078
  reply.code(201);
3080
4079
  return { behavior };
3081
4080
  });
@@ -3087,6 +4086,11 @@ export async function buildServer(options = {}) {
3087
4086
  reply.code(404);
3088
4087
  return { error: "Behavior not found" };
3089
4088
  }
4089
+ syncEntityOwnerFromBody({
4090
+ entityType: "behavior",
4091
+ entityId: behavior.id,
4092
+ body: request.body
4093
+ });
3090
4094
  return { behavior };
3091
4095
  });
3092
4096
  app.patch("/api/v1/psyche/behaviors/:id", async (request, reply) => {
@@ -3115,11 +4119,21 @@ export async function buildServer(options = {}) {
3115
4119
  });
3116
4120
  app.get("/api/v1/psyche/beliefs", async (request) => {
3117
4121
  requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/beliefs" });
3118
- return { beliefs: listBeliefEntries() };
4122
+ const userIds = resolveScopedUserIds(request.query);
4123
+ return {
4124
+ beliefs: filterOwnedEntities("belief_entry", listBeliefEntries(), userIds)
4125
+ };
3119
4126
  });
3120
4127
  app.post("/api/v1/psyche/beliefs", async (request, reply) => {
3121
4128
  const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/beliefs" });
3122
4129
  const belief = createBeliefEntry(createBeliefEntrySchema.parse(request.body ?? {}), toActivityContext(auth));
4130
+ syncEntityOwnerFromBody({
4131
+ entityType: "belief_entry",
4132
+ entityId: belief.id,
4133
+ body: request.body,
4134
+ fallbackLabel: auth.actor,
4135
+ assignDefaultWhenMissing: true
4136
+ });
3123
4137
  reply.code(201);
3124
4138
  return { belief };
3125
4139
  });
@@ -3131,6 +4145,11 @@ export async function buildServer(options = {}) {
3131
4145
  reply.code(404);
3132
4146
  return { error: "Belief not found" };
3133
4147
  }
4148
+ syncEntityOwnerFromBody({
4149
+ entityType: "belief_entry",
4150
+ entityId: belief.id,
4151
+ body: request.body
4152
+ });
3134
4153
  return { belief };
3135
4154
  });
3136
4155
  app.patch("/api/v1/psyche/beliefs/:id", async (request, reply) => {
@@ -3155,11 +4174,21 @@ export async function buildServer(options = {}) {
3155
4174
  });
3156
4175
  app.get("/api/v1/psyche/modes", async (request) => {
3157
4176
  requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/modes" });
3158
- return { modes: listModeProfiles() };
4177
+ const userIds = resolveScopedUserIds(request.query);
4178
+ return {
4179
+ modes: filterOwnedEntities("mode_profile", listModeProfiles(), userIds)
4180
+ };
3159
4181
  });
3160
4182
  app.post("/api/v1/psyche/modes", async (request, reply) => {
3161
4183
  const auth = requirePsycheScopedAccess(request.headers, ["psyche.mode"], { route: "/api/v1/psyche/modes" });
3162
4184
  const mode = createModeProfile(createModeProfileSchema.parse(request.body ?? {}), toActivityContext(auth));
4185
+ syncEntityOwnerFromBody({
4186
+ entityType: "mode_profile",
4187
+ entityId: mode.id,
4188
+ body: request.body,
4189
+ fallbackLabel: auth.actor,
4190
+ assignDefaultWhenMissing: true
4191
+ });
3163
4192
  reply.code(201);
3164
4193
  return { mode };
3165
4194
  });
@@ -3171,6 +4200,11 @@ export async function buildServer(options = {}) {
3171
4200
  reply.code(404);
3172
4201
  return { error: "Mode not found" };
3173
4202
  }
4203
+ syncEntityOwnerFromBody({
4204
+ entityType: "mode_profile",
4205
+ entityId: mode.id,
4206
+ body: request.body
4207
+ });
3174
4208
  return { mode };
3175
4209
  });
3176
4210
  app.patch("/api/v1/psyche/modes/:id", async (request, reply) => {
@@ -3195,11 +4229,21 @@ export async function buildServer(options = {}) {
3195
4229
  });
3196
4230
  app.get("/api/v1/psyche/mode-guides", async (request) => {
3197
4231
  requirePsycheScopedAccess(request.headers, ["psyche.mode"], { route: "/api/v1/psyche/mode-guides" });
3198
- return { sessions: listModeGuideSessions() };
4232
+ const userIds = resolveScopedUserIds(request.query);
4233
+ return {
4234
+ sessions: filterOwnedEntities("mode_guide_session", listModeGuideSessions(), userIds)
4235
+ };
3199
4236
  });
3200
4237
  app.post("/api/v1/psyche/mode-guides", async (request, reply) => {
3201
4238
  const auth = requirePsycheScopedAccess(request.headers, ["psyche.mode"], { route: "/api/v1/psyche/mode-guides" });
3202
4239
  const session = createModeGuideSession(createModeGuideSessionSchema.parse(request.body ?? {}), toActivityContext(auth));
4240
+ syncEntityOwnerFromBody({
4241
+ entityType: "mode_guide_session",
4242
+ entityId: session.id,
4243
+ body: request.body,
4244
+ fallbackLabel: auth.actor,
4245
+ assignDefaultWhenMissing: true
4246
+ });
3203
4247
  reply.code(201);
3204
4248
  return { session };
3205
4249
  });
@@ -3211,6 +4255,11 @@ export async function buildServer(options = {}) {
3211
4255
  reply.code(404);
3212
4256
  return { error: "Mode guide session not found" };
3213
4257
  }
4258
+ syncEntityOwnerFromBody({
4259
+ entityType: "mode_guide_session",
4260
+ entityId: session.id,
4261
+ body: request.body
4262
+ });
3214
4263
  return { session };
3215
4264
  });
3216
4265
  app.patch("/api/v1/psyche/mode-guides/:id", async (request, reply) => {
@@ -3235,11 +4284,21 @@ export async function buildServer(options = {}) {
3235
4284
  });
3236
4285
  app.get("/api/v1/psyche/event-types", async (request) => {
3237
4286
  requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/event-types" });
3238
- return { eventTypes: listEventTypes() };
4287
+ const userIds = resolveScopedUserIds(request.query);
4288
+ return {
4289
+ eventTypes: filterOwnedEntities("event_type", listEventTypes(), userIds)
4290
+ };
3239
4291
  });
3240
4292
  app.post("/api/v1/psyche/event-types", async (request, reply) => {
3241
4293
  const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/event-types" });
3242
4294
  const eventType = createEventType(createEventTypeSchema.parse(request.body ?? {}), toActivityContext(auth));
4295
+ syncEntityOwnerFromBody({
4296
+ entityType: "event_type",
4297
+ entityId: eventType.id,
4298
+ body: request.body,
4299
+ fallbackLabel: auth.actor,
4300
+ assignDefaultWhenMissing: true
4301
+ });
3243
4302
  reply.code(201);
3244
4303
  return { eventType };
3245
4304
  });
@@ -3251,6 +4310,11 @@ export async function buildServer(options = {}) {
3251
4310
  reply.code(404);
3252
4311
  return { error: "Event type not found" };
3253
4312
  }
4313
+ syncEntityOwnerFromBody({
4314
+ entityType: "event_type",
4315
+ entityId: eventType.id,
4316
+ body: request.body
4317
+ });
3254
4318
  return { eventType };
3255
4319
  });
3256
4320
  app.patch("/api/v1/psyche/event-types/:id", async (request, reply) => {
@@ -3275,11 +4339,21 @@ export async function buildServer(options = {}) {
3275
4339
  });
3276
4340
  app.get("/api/v1/psyche/emotions", async (request) => {
3277
4341
  requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/emotions" });
3278
- return { emotions: listEmotionDefinitions() };
4342
+ const userIds = resolveScopedUserIds(request.query);
4343
+ return {
4344
+ emotions: filterOwnedEntities("emotion_definition", listEmotionDefinitions(), userIds)
4345
+ };
3279
4346
  });
3280
4347
  app.post("/api/v1/psyche/emotions", async (request, reply) => {
3281
4348
  const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/emotions" });
3282
4349
  const emotion = createEmotionDefinition(createEmotionDefinitionSchema.parse(request.body ?? {}), toActivityContext(auth));
4350
+ syncEntityOwnerFromBody({
4351
+ entityType: "emotion_definition",
4352
+ entityId: emotion.id,
4353
+ body: request.body,
4354
+ fallbackLabel: auth.actor,
4355
+ assignDefaultWhenMissing: true
4356
+ });
3283
4357
  reply.code(201);
3284
4358
  return { emotion };
3285
4359
  });
@@ -3291,6 +4365,11 @@ export async function buildServer(options = {}) {
3291
4365
  reply.code(404);
3292
4366
  return { error: "Emotion definition not found" };
3293
4367
  }
4368
+ syncEntityOwnerFromBody({
4369
+ entityType: "emotion_definition",
4370
+ entityId: emotion.id,
4371
+ body: request.body
4372
+ });
3294
4373
  return { emotion };
3295
4374
  });
3296
4375
  app.patch("/api/v1/psyche/emotions/:id", async (request, reply) => {
@@ -3315,11 +4394,21 @@ export async function buildServer(options = {}) {
3315
4394
  });
3316
4395
  app.get("/api/v1/psyche/reports", async (request) => {
3317
4396
  requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/reports" });
3318
- return { reports: listTriggerReports() };
4397
+ const userIds = resolveScopedUserIds(request.query);
4398
+ return {
4399
+ reports: filterOwnedEntities("trigger_report", listTriggerReports(), userIds)
4400
+ };
3319
4401
  });
3320
4402
  app.post("/api/v1/psyche/reports", async (request, reply) => {
3321
4403
  const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/reports" });
3322
4404
  const report = createTriggerReport(createTriggerReportSchema.parse(request.body ?? {}), toActivityContext(auth));
4405
+ syncEntityOwnerFromBody({
4406
+ entityType: "trigger_report",
4407
+ entityId: report.id,
4408
+ body: request.body,
4409
+ fallbackLabel: auth.actor,
4410
+ assignDefaultWhenMissing: true
4411
+ });
3323
4412
  reply.code(201);
3324
4413
  return { report };
3325
4414
  });
@@ -3353,6 +4442,11 @@ export async function buildServer(options = {}) {
3353
4442
  reply.code(404);
3354
4443
  return { error: "Trigger report not found" };
3355
4444
  }
4445
+ syncEntityOwnerFromBody({
4446
+ entityType: "trigger_report",
4447
+ entityId: report.id,
4448
+ body: request.body
4449
+ });
3356
4450
  return { report };
3357
4451
  });
3358
4452
  app.delete("/api/v1/psyche/reports/:id", async (request, reply) => {
@@ -3373,7 +4467,9 @@ export async function buildServer(options = {}) {
3373
4467
  entityType: query.linkedEntityType
3374
4468
  });
3375
4469
  }
3376
- return { notes: listNotes(query) };
4470
+ return {
4471
+ notes: filterOwnedEntities("note", listNotes(query), query.userIds)
4472
+ };
3377
4473
  });
3378
4474
  app.post("/api/v1/notes", async (request, reply) => {
3379
4475
  const input = createNoteSchema.parse(request.body ?? {});
@@ -3436,6 +4532,623 @@ export async function buildServer(options = {}) {
3436
4532
  }
3437
4533
  return { note };
3438
4534
  });
4535
+ app.get("/api/v1/wiki/settings", async (request) => {
4536
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/wiki/settings" });
4537
+ return { settings: getWikiSettingsPayload() };
4538
+ });
4539
+ app.post("/api/v1/wiki/settings/llm-profiles", async (request, reply) => {
4540
+ requireScopedAccess(request.headers, ["write"], {
4541
+ route: "/api/v1/wiki/settings/llm-profiles"
4542
+ });
4543
+ const profile = upsertWikiLlmProfile(upsertWikiLlmProfileSchema.parse(request.body ?? {}), managers.secrets);
4544
+ reply.code(201);
4545
+ return { profile };
4546
+ });
4547
+ app.post("/api/v1/wiki/settings/llm-profiles/test", async (request, reply) => {
4548
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/settings/llm-profiles/test" });
4549
+ const parsed = testWikiLlmProfileSchema.parse(request.body ?? {});
4550
+ const existingProfile = parsed.profileId
4551
+ ? (listWikiLlmProfiles().find((entry) => entry.id === parsed.profileId) ?? null)
4552
+ : null;
4553
+ const profile = {
4554
+ provider: parsed.provider,
4555
+ baseUrl: parsed.baseUrl,
4556
+ model: parsed.model,
4557
+ systemPrompt: existingProfile?.systemPrompt ?? "",
4558
+ secretId: existingProfile?.secretId ?? null,
4559
+ metadata: {
4560
+ ...(existingProfile?.metadata ?? {}),
4561
+ ...(parsed.reasoningEffort
4562
+ ? { reasoningEffort: parsed.reasoningEffort }
4563
+ : {}),
4564
+ ...(parsed.verbosity ? { verbosity: parsed.verbosity } : {})
4565
+ }
4566
+ };
4567
+ const result = await managers.llm.testWikiConnection(profile, parsed.apiKey ?? null, ({ level, message, details = {} }) => {
4568
+ recordDiagnosticLog({
4569
+ level,
4570
+ source: normalizeDiagnosticSource(request.headers["x-forge-source"]),
4571
+ scope: typeof details.scope === "string" ? details.scope : "wiki_llm",
4572
+ eventKey: typeof details.eventKey === "string"
4573
+ ? details.eventKey
4574
+ : "llm_connection_test",
4575
+ message,
4576
+ route: "/api/v1/wiki/settings/llm-profiles/test",
4577
+ functionName: "testWikiConnection",
4578
+ details
4579
+ });
4580
+ });
4581
+ reply.code(200);
4582
+ return { result };
4583
+ });
4584
+ app.post("/api/v1/wiki/settings/embedding-profiles", async (request, reply) => {
4585
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/settings/embedding-profiles" });
4586
+ const profile = upsertWikiEmbeddingProfile(upsertWikiEmbeddingProfileSchema.parse(request.body ?? {}), managers.secrets);
4587
+ reply.code(201);
4588
+ return { profile };
4589
+ });
4590
+ app.delete("/api/v1/wiki/settings/:kind(llm|embedding)-profiles/:id", async (request, reply) => {
4591
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/settings/:kind-profiles/:id" });
4592
+ const params = request.params;
4593
+ deleteWikiProfile(params.kind, params.id);
4594
+ reply.code(204);
4595
+ return null;
4596
+ });
4597
+ app.get("/api/v1/wiki/spaces", async (request) => {
4598
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/wiki/spaces" });
4599
+ return { spaces: listWikiSpaces() };
4600
+ });
4601
+ app.post("/api/v1/wiki/spaces", async (request, reply) => {
4602
+ requireScopedAccess(request.headers, ["write"], {
4603
+ route: "/api/v1/wiki/spaces"
4604
+ });
4605
+ const space = createWikiSpace(createWikiSpaceSchema.parse(request.body ?? {}));
4606
+ reply.code(201);
4607
+ return { space };
4608
+ });
4609
+ app.get("/api/v1/wiki/pages", async (request) => {
4610
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/wiki/pages" });
4611
+ const query = request.query;
4612
+ return {
4613
+ pages: listWikiPages({
4614
+ spaceId: query.spaceId,
4615
+ kind: query.kind,
4616
+ limit: query.limit ? Number(query.limit) : undefined
4617
+ })
4618
+ };
4619
+ });
4620
+ app.get("/api/v1/wiki/home", async (request, reply) => {
4621
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/wiki/home" });
4622
+ const query = request.query;
4623
+ const payload = getWikiHomePageDetail({ spaceId: query.spaceId });
4624
+ if (!payload) {
4625
+ reply.code(404);
4626
+ return { error: "Wiki home page not found" };
4627
+ }
4628
+ return payload;
4629
+ });
4630
+ app.get("/api/v1/wiki/tree", async (request) => {
4631
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/wiki/tree" });
4632
+ const query = request.query;
4633
+ return {
4634
+ tree: listWikiPageTree({
4635
+ spaceId: query.spaceId,
4636
+ kind: query.kind ?? "wiki"
4637
+ })
4638
+ };
4639
+ });
4640
+ app.post("/api/v1/wiki/pages", async (request, reply) => {
4641
+ const input = createNoteSchema.parse({
4642
+ kind: "wiki",
4643
+ ...(request.body ?? {})
4644
+ });
4645
+ const linkedEntityType = input.links[0]?.entityType ?? null;
4646
+ const auth = requireNoteAccess(request.headers, linkedEntityType, {
4647
+ route: "/api/v1/wiki/pages",
4648
+ entityType: linkedEntityType
4649
+ });
4650
+ const note = createNote(input, toActivityContext(auth));
4651
+ reply.code(201);
4652
+ return getWikiPageDetail(note.id);
4653
+ });
4654
+ app.get("/api/v1/wiki/pages/:id", async (request, reply) => {
4655
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/wiki/pages/:id" });
4656
+ const { id } = request.params;
4657
+ const payload = getWikiPageDetail(id);
4658
+ if (!payload) {
4659
+ reply.code(404);
4660
+ return { error: "Wiki page not found" };
4661
+ }
4662
+ return payload;
4663
+ });
4664
+ app.get("/api/v1/wiki/by-slug/:slug", async (request, reply) => {
4665
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/wiki/by-slug/:slug" });
4666
+ const { slug } = request.params;
4667
+ const query = request.query;
4668
+ const payload = getWikiPageDetailBySlug({ spaceId: query.spaceId, slug });
4669
+ if (!payload) {
4670
+ reply.code(404);
4671
+ return { error: "Wiki page not found" };
4672
+ }
4673
+ return payload;
4674
+ });
4675
+ app.patch("/api/v1/wiki/pages/:id", async (request, reply) => {
4676
+ const { id } = request.params;
4677
+ const patch = updateNoteSchema.parse(request.body ?? {});
4678
+ const current = getNoteById(id);
4679
+ const linkedEntityType = current?.links[0]?.entityType ?? patch.links?.[0]?.entityType ?? null;
4680
+ const auth = requireNoteAccess(request.headers, linkedEntityType, {
4681
+ route: "/api/v1/wiki/pages/:id",
4682
+ entityType: linkedEntityType
4683
+ });
4684
+ const note = updateNote(id, patch, toActivityContext(auth));
4685
+ if (!note) {
4686
+ reply.code(404);
4687
+ return { error: "Wiki page not found" };
4688
+ }
4689
+ return getWikiPageDetail(note.id);
4690
+ });
4691
+ app.post("/api/v1/wiki/search", async (request) => {
4692
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/wiki/search" });
4693
+ return searchWikiPages(wikiSearchQuerySchema.parse(request.body ?? {}), managers.secrets);
4694
+ });
4695
+ app.get("/api/v1/wiki/health", async (request) => {
4696
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/wiki/health" });
4697
+ return {
4698
+ health: getWikiHealth(syncWikiVaultSchema.parse(request.query ?? {}))
4699
+ };
4700
+ });
4701
+ app.post("/api/v1/wiki/sync", async (request) => {
4702
+ requireScopedAccess(request.headers, ["write"], {
4703
+ route: "/api/v1/wiki/sync"
4704
+ });
4705
+ return syncWikiVaultFromDisk(syncWikiVaultSchema.parse(request.body ?? {}));
4706
+ });
4707
+ app.post("/api/v1/wiki/reindex", async (request) => {
4708
+ requireScopedAccess(request.headers, ["write"], {
4709
+ route: "/api/v1/wiki/reindex"
4710
+ });
4711
+ return reindexWikiEmbeddings(reindexWikiEmbeddingsSchema.parse(request.body ?? {}), managers.secrets);
4712
+ });
4713
+ const readStringField = (record, key, fallback = "") => (typeof record[key] === "string" ? record[key] : fallback);
4714
+ const readStringArrayField = (record, key) => Array.isArray(record[key])
4715
+ ? record[key].filter((entry) => typeof entry === "string" && entry.trim().length > 0)
4716
+ : [];
4717
+ const resolveMappedIngestEntity = (entityType, entityId) => {
4718
+ const result = searchEntities({
4719
+ searches: [
4720
+ {
4721
+ entityTypes: [entityType],
4722
+ ids: [entityId],
4723
+ includeDeleted: false,
4724
+ limit: 1
4725
+ }
4726
+ ]
4727
+ }).results[0];
4728
+ if (!result?.ok) {
4729
+ return null;
4730
+ }
4731
+ const match = result.matches?.find((entry) => entry.entityType === entityType && entry.id === entityId);
4732
+ return match
4733
+ ? {
4734
+ entityType,
4735
+ entityId
4736
+ }
4737
+ : null;
4738
+ };
4739
+ const publishIngestProposalEntity = (proposal, auth) => {
4740
+ const suggestedFields = proposal.suggestedFields &&
4741
+ typeof proposal.suggestedFields === "object" &&
4742
+ !Array.isArray(proposal.suggestedFields)
4743
+ ? proposal.suggestedFields
4744
+ : {};
4745
+ const entityType = readStringField(proposal, "entityType").trim();
4746
+ const title = readStringField(proposal, "title").trim() || "Imported candidate";
4747
+ const summary = readStringField(proposal, "summary").trim();
4748
+ switch (entityType) {
4749
+ case "goal": {
4750
+ const goal = createGoal({
4751
+ title,
4752
+ description: summary,
4753
+ horizon: readStringField(suggestedFields, "horizon", "year") === "quarter"
4754
+ ? "quarter"
4755
+ : readStringField(suggestedFields, "horizon", "year") ===
4756
+ "lifetime"
4757
+ ? "lifetime"
4758
+ : "year",
4759
+ status: readStringField(suggestedFields, "status", "active") === "paused"
4760
+ ? "paused"
4761
+ : readStringField(suggestedFields, "status", "active") ===
4762
+ "completed"
4763
+ ? "completed"
4764
+ : "active",
4765
+ targetPoints: Number(suggestedFields.targetPoints ?? 400) || 400,
4766
+ themeColor: readStringField(suggestedFields, "themeColor", "#c8a46b"),
4767
+ tagIds: readStringArrayField(suggestedFields, "tagIds"),
4768
+ notes: [],
4769
+ userId: typeof suggestedFields.userId === "string"
4770
+ ? suggestedFields.userId
4771
+ : null
4772
+ }, toActivityContext(auth));
4773
+ return { entityType: "goal", entityId: goal.id };
4774
+ }
4775
+ case "project": {
4776
+ const goalId = readStringField(suggestedFields, "goalId").trim() ||
4777
+ readStringArrayField(suggestedFields, "targetGoalIds")[0] ||
4778
+ readStringArrayField(suggestedFields, "linkedGoalIds")[0] ||
4779
+ "";
4780
+ if (!goalId) {
4781
+ throw new Error("Project proposals need a goalId to publish.");
4782
+ }
4783
+ const project = createProject({
4784
+ goalId,
4785
+ title,
4786
+ description: summary,
4787
+ status: readStringField(suggestedFields, "status", "active") === "paused"
4788
+ ? "paused"
4789
+ : readStringField(suggestedFields, "status", "active") ===
4790
+ "completed"
4791
+ ? "completed"
4792
+ : "active",
4793
+ targetPoints: Number(suggestedFields.targetPoints ?? 240) || 240,
4794
+ themeColor: readStringField(suggestedFields, "themeColor", "#c0c1ff"),
4795
+ schedulingRules: {
4796
+ allowWorkBlockKinds: [],
4797
+ blockWorkBlockKinds: [],
4798
+ allowCalendarIds: [],
4799
+ blockCalendarIds: [],
4800
+ allowEventTypes: [],
4801
+ blockEventTypes: [],
4802
+ allowEventKeywords: [],
4803
+ blockEventKeywords: [],
4804
+ allowAvailability: [],
4805
+ blockAvailability: []
4806
+ },
4807
+ notes: [],
4808
+ userId: typeof suggestedFields.userId === "string"
4809
+ ? suggestedFields.userId
4810
+ : null
4811
+ }, toActivityContext(auth));
4812
+ return { entityType: "project", entityId: project.id };
4813
+ }
4814
+ case "task": {
4815
+ const task = createTask({
4816
+ title,
4817
+ description: summary,
4818
+ status: "backlog",
4819
+ priority: "medium",
4820
+ owner: auth.actor ?? "Forge",
4821
+ userId: typeof suggestedFields.userId === "string"
4822
+ ? suggestedFields.userId
4823
+ : null,
4824
+ goalId: readStringField(suggestedFields, "goalId").trim() || null,
4825
+ projectId: readStringField(suggestedFields, "projectId").trim() || null,
4826
+ dueDate: null,
4827
+ effort: "deep",
4828
+ energy: "steady",
4829
+ points: Number(suggestedFields.points ?? 40) || 40,
4830
+ plannedDurationSeconds: null,
4831
+ schedulingRules: null,
4832
+ tagIds: readStringArrayField(suggestedFields, "tagIds"),
4833
+ notes: []
4834
+ }, toActivityContext(auth));
4835
+ return { entityType: "task", entityId: task.id };
4836
+ }
4837
+ case "habit": {
4838
+ const habit = createHabit({
4839
+ title,
4840
+ description: summary,
4841
+ status: "active",
4842
+ polarity: readStringField(suggestedFields, "polarity", "positive") ===
4843
+ "negative"
4844
+ ? "negative"
4845
+ : "positive",
4846
+ frequency: readStringField(suggestedFields, "frequency", "daily") ===
4847
+ "weekly"
4848
+ ? "weekly"
4849
+ : "daily",
4850
+ targetCount: Number(suggestedFields.targetCount ?? 1) || 1,
4851
+ weekDays: Array.isArray(suggestedFields.weekDays)
4852
+ ? suggestedFields.weekDays
4853
+ : [],
4854
+ linkedGoalIds: readStringArrayField(suggestedFields, "linkedGoalIds"),
4855
+ linkedProjectIds: readStringArrayField(suggestedFields, "linkedProjectIds"),
4856
+ linkedTaskIds: readStringArrayField(suggestedFields, "linkedTaskIds"),
4857
+ linkedValueIds: [],
4858
+ linkedPatternIds: [],
4859
+ linkedBehaviorIds: [],
4860
+ linkedBeliefIds: [],
4861
+ linkedModeIds: [],
4862
+ linkedReportIds: [],
4863
+ linkedBehaviorId: null,
4864
+ rewardXp: Number(suggestedFields.rewardXp ?? 12) || 12,
4865
+ penaltyXp: Number(suggestedFields.penaltyXp ?? 8) || 8,
4866
+ generatedHealthEventTemplate: {
4867
+ enabled: false,
4868
+ workoutType: "workout",
4869
+ title: "",
4870
+ durationMinutes: 45,
4871
+ xpReward: 0,
4872
+ tags: [],
4873
+ links: [],
4874
+ notesTemplate: ""
4875
+ },
4876
+ userId: typeof suggestedFields.userId === "string"
4877
+ ? suggestedFields.userId
4878
+ : null
4879
+ }, toActivityContext(auth));
4880
+ return { entityType: "habit", entityId: habit.id };
4881
+ }
4882
+ case "psyche_value": {
4883
+ const value = createPsycheValue({
4884
+ title,
4885
+ description: summary,
4886
+ valuedDirection: readStringField(suggestedFields, "valuedDirection"),
4887
+ whyItMatters: readStringField(suggestedFields, "whyItMatters"),
4888
+ linkedGoalIds: readStringArrayField(suggestedFields, "linkedGoalIds"),
4889
+ linkedProjectIds: readStringArrayField(suggestedFields, "linkedProjectIds"),
4890
+ linkedTaskIds: readStringArrayField(suggestedFields, "linkedTaskIds"),
4891
+ committedActions: readStringArrayField(suggestedFields, "committedActions"),
4892
+ userId: typeof suggestedFields.userId === "string"
4893
+ ? suggestedFields.userId
4894
+ : null
4895
+ }, toActivityContext(auth));
4896
+ return { entityType: "psyche_value", entityId: value.id };
4897
+ }
4898
+ case "strategy": {
4899
+ const targetProjectIds = readStringArrayField(suggestedFields, "targetProjectIds");
4900
+ const linkedEntities = Array.isArray(suggestedFields.linkedEntities) &&
4901
+ suggestedFields.linkedEntities.every((entry) => entry &&
4902
+ typeof entry === "object" &&
4903
+ typeof entry.entityType ===
4904
+ "string" &&
4905
+ typeof entry.entityId === "string")
4906
+ ? suggestedFields.linkedEntities
4907
+ : [];
4908
+ const firstProjectId = targetProjectIds[0] ||
4909
+ linkedEntities.find((entry) => entry.entityType === "project")
4910
+ ?.entityId ||
4911
+ "";
4912
+ const firstTaskId = linkedEntities.find((entry) => entry.entityType === "task")
4913
+ ?.entityId || "";
4914
+ const graphNodeId = firstProjectId || firstTaskId;
4915
+ if (!graphNodeId) {
4916
+ throw new Error("Strategy proposals need at least one linked project or task to publish.");
4917
+ }
4918
+ const strategy = createStrategy({
4919
+ title,
4920
+ overview: summary,
4921
+ endStateDescription: readStringField(suggestedFields, "endStateDescription", summary),
4922
+ status: "active",
4923
+ targetGoalIds: readStringArrayField(suggestedFields, "targetGoalIds"),
4924
+ targetProjectIds,
4925
+ linkedEntities: linkedEntities.filter((entry) => entry.entityType !== "goal" &&
4926
+ entry.entityType !== "note" &&
4927
+ typeof entry.entityId === "string"),
4928
+ graph: {
4929
+ nodes: [
4930
+ {
4931
+ id: `seed_${graphNodeId}`,
4932
+ entityType: firstProjectId ? "project" : "task",
4933
+ entityId: graphNodeId,
4934
+ title,
4935
+ branchLabel: "",
4936
+ notes: summary
4937
+ }
4938
+ ],
4939
+ edges: []
4940
+ },
4941
+ userId: typeof suggestedFields.userId === "string"
4942
+ ? suggestedFields.userId
4943
+ : null,
4944
+ isLocked: false,
4945
+ lockedByUserId: null
4946
+ });
4947
+ return { entityType: "strategy", entityId: strategy.id };
4948
+ }
4949
+ case "note": {
4950
+ const note = createNote({
4951
+ kind: "evidence",
4952
+ title,
4953
+ slug: "",
4954
+ spaceId: "",
4955
+ parentSlug: null,
4956
+ indexOrder: 0,
4957
+ showInIndex: false,
4958
+ aliases: [],
4959
+ summary,
4960
+ contentMarkdown: `# ${title}\n\n${summary}\n`,
4961
+ author: auth.actor ?? null,
4962
+ tags: [],
4963
+ destroyAt: null,
4964
+ links: [],
4965
+ sourcePath: "",
4966
+ frontmatter: {},
4967
+ revisionHash: "",
4968
+ userId: null
4969
+ }, toActivityContext(auth));
4970
+ return { entityType: "note", entityId: note.id };
4971
+ }
4972
+ default:
4973
+ throw new Error(`Unsupported ingest proposal entity type: ${entityType}`);
4974
+ }
4975
+ };
4976
+ app.get("/api/v1/wiki/ingest-jobs", async (request) => {
4977
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/wiki/ingest-jobs" });
4978
+ return {
4979
+ jobs: listWikiIngestJobs(request.query ?? {})
4980
+ };
4981
+ });
4982
+ app.post("/api/v1/wiki/ingest-jobs/uploads", async (request, reply) => {
4983
+ const parts = request.parts();
4984
+ const fields = new Map();
4985
+ const files = [];
4986
+ for await (const part of parts) {
4987
+ if (part.type === "file") {
4988
+ files.push({
4989
+ fileName: part.filename || "upload.bin",
4990
+ mimeType: part.mimetype || "application/octet-stream",
4991
+ payload: await part.toBuffer()
4992
+ });
4993
+ }
4994
+ else {
4995
+ fields.set(part.fieldname, String(part.value ?? ""));
4996
+ }
4997
+ }
4998
+ const linkedEntityHints = (() => {
4999
+ try {
5000
+ return JSON.parse(fields.get("linkedEntityHints") || "[]");
5001
+ }
5002
+ catch {
5003
+ return [];
5004
+ }
5005
+ })();
5006
+ const linkedEntityType = linkedEntityHints[0]?.entityType ?? null;
5007
+ const auth = requireNoteAccess(request.headers, linkedEntityType, {
5008
+ route: "/api/v1/wiki/ingest-jobs/uploads",
5009
+ entityType: linkedEntityType
5010
+ });
5011
+ const result = await createUploadedWikiIngestJob({
5012
+ spaceId: fields.get("spaceId") || undefined,
5013
+ titleHint: fields.get("titleHint") || undefined,
5014
+ llmProfileId: fields.get("llmProfileId") || undefined,
5015
+ parseStrategy: fields.get("parseStrategy") === "text_only" ||
5016
+ fields.get("parseStrategy") === "multimodal"
5017
+ ? fields.get("parseStrategy")
5018
+ : "auto",
5019
+ entityProposalMode: fields.get("entityProposalMode") === "none" ? "none" : "suggest",
5020
+ userId: null,
5021
+ createAsKind: fields.get("createAsKind") === "evidence" ? "evidence" : "wiki",
5022
+ linkedEntityHints
5023
+ }, files, {
5024
+ actor: auth.actor ?? null
5025
+ });
5026
+ const jobId = result.job?.job.id;
5027
+ if (jobId) {
5028
+ enqueueWikiIngestJob(jobId);
5029
+ }
5030
+ reply.code(201);
5031
+ return result;
5032
+ });
5033
+ app.post("/api/v1/wiki/ingest-jobs", async (request, reply) => {
5034
+ const payload = createWikiIngestJobSchema.parse(request.body ?? {});
5035
+ const linkedEntityType = payload.linkedEntityHints[0]?.entityType ?? null;
5036
+ const auth = requireNoteAccess(request.headers, linkedEntityType, {
5037
+ route: "/api/v1/wiki/ingest-jobs",
5038
+ entityType: linkedEntityType
5039
+ });
5040
+ const result = await ingestWikiSource(payload, {
5041
+ actor: auth.actor ?? null
5042
+ });
5043
+ const jobId = result.job?.job.id;
5044
+ if (jobId) {
5045
+ enqueueWikiIngestJob(jobId);
5046
+ }
5047
+ reply.code(201);
5048
+ return result;
5049
+ });
5050
+ app.get("/api/v1/wiki/ingest-jobs/:id", async (request, reply) => {
5051
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/wiki/ingest-jobs/:id" });
5052
+ const { id } = request.params;
5053
+ const job = getWikiIngestJob(id);
5054
+ if (!job) {
5055
+ reply.code(404);
5056
+ return { error: "Wiki ingest job not found" };
5057
+ }
5058
+ return job;
5059
+ });
5060
+ app.post("/api/v1/wiki/ingest-jobs/:id/rerun", async (request, reply) => {
5061
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/rerun" });
5062
+ const { id } = request.params;
5063
+ try {
5064
+ const result = await rerunWikiIngestJob(id, {
5065
+ actor: requireAuthenticatedActor(request.headers, { route: "/api/v1/wiki/ingest-jobs/:id/rerun" }).actor ?? null
5066
+ });
5067
+ if (!result) {
5068
+ reply.code(404);
5069
+ return { error: "Wiki ingest job not found" };
5070
+ }
5071
+ const nextJobId = result.job?.job.id;
5072
+ if (nextJobId) {
5073
+ enqueueWikiIngestJob(nextJobId);
5074
+ }
5075
+ reply.code(201);
5076
+ return result;
5077
+ }
5078
+ catch (error) {
5079
+ if (error instanceof Error &&
5080
+ error.message.includes("can only be rerun")) {
5081
+ reply.code(409);
5082
+ return { error: error.message };
5083
+ }
5084
+ throw error;
5085
+ }
5086
+ });
5087
+ app.post("/api/v1/wiki/ingest-jobs/:id/resume", async (request, reply) => {
5088
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/resume" });
5089
+ const { id } = request.params;
5090
+ const job = getWikiIngestJob(id);
5091
+ if (!job) {
5092
+ reply.code(404);
5093
+ return { error: "Wiki ingest job not found" };
5094
+ }
5095
+ const hasRecoverableOpenAiResponse = job.logs.some((entry) => typeof entry.metadata.responseId === "string") ||
5096
+ job.assets.some((asset) => typeof asset.metadata.openAiResponseId === "string" ||
5097
+ asset.status === "processing");
5098
+ const canResume = ["queued", "processing"].includes(job.job.status) ||
5099
+ (job.job.status === "failed" && hasRecoverableOpenAiResponse);
5100
+ if (!canResume) {
5101
+ reply.code(409);
5102
+ return {
5103
+ error: "Only active wiki ingest jobs, or failed jobs with a recoverable OpenAI background response, can be resumed.",
5104
+ job,
5105
+ resumed: false
5106
+ };
5107
+ }
5108
+ const alreadyActive = managers.backgroundJobs.has(id);
5109
+ if (!alreadyActive) {
5110
+ enqueueWikiIngestJob(id);
5111
+ }
5112
+ return {
5113
+ job: getWikiIngestJob(id),
5114
+ resumed: !alreadyActive
5115
+ };
5116
+ });
5117
+ app.delete("/api/v1/wiki/ingest-jobs/:id", async (request, reply) => {
5118
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id" });
5119
+ const { id } = request.params;
5120
+ try {
5121
+ const deleted = deleteWikiIngestJob(id);
5122
+ if (!deleted) {
5123
+ reply.code(404);
5124
+ return { error: "Wiki ingest job not found" };
5125
+ }
5126
+ return { deleted };
5127
+ }
5128
+ catch (error) {
5129
+ if (error instanceof Error &&
5130
+ error.message.includes("can only be deleted")) {
5131
+ reply.code(409);
5132
+ return { error: error.message };
5133
+ }
5134
+ throw error;
5135
+ }
5136
+ });
5137
+ app.post("/api/v1/wiki/ingest-jobs/:id/review", async (request, reply) => {
5138
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/wiki/ingest-jobs/:id/review" });
5139
+ const { id } = request.params;
5140
+ const reviewed = await reviewWikiIngestJob(id, reviewWikiIngestJobSchema.parse(request.body ?? {}), {
5141
+ createNote: (note) => createNote(note, toActivityContext(auth)),
5142
+ updateNote: (noteId, patch) => updateNote(noteId, patch, toActivityContext(auth)),
5143
+ publishEntity: (proposal) => publishIngestProposalEntity(proposal, auth),
5144
+ resolveMappedEntity: (entityType, entityId) => resolveMappedIngestEntity(entityType, entityId)
5145
+ });
5146
+ if (!reviewed) {
5147
+ reply.code(404);
5148
+ return { error: "Wiki ingest job not found" };
5149
+ }
5150
+ return { job: reviewed };
5151
+ });
3439
5152
  app.get("/api/v1/projects", async (request) => {
3440
5153
  const query = projectListQuerySchema.parse(request.query ?? {});
3441
5154
  return { projects: listProjectSummaries(query) };
@@ -3445,7 +5158,15 @@ export async function buildServer(options = {}) {
3445
5158
  const query = projectListQuerySchema.parse(request.query ?? {});
3446
5159
  return { projects: listProjectSummaries(query) };
3447
5160
  });
3448
- app.get("/api/v1/goals", async () => ({ goals: listGoals() }));
5161
+ app.get("/api/v1/goals", async (request) => {
5162
+ const query = goalListQuerySchema.parse(request.query ?? {});
5163
+ const goals = filterOwnedEntities("goal", listGoals(), query.userIds)
5164
+ .filter((goal) => (query.status ? goal.status === query.status : true))
5165
+ .filter((goal) => (query.horizon ? goal.horizon === query.horizon : true))
5166
+ .filter((goal) => query.tagId ? goal.tagIds.includes(query.tagId) : true)
5167
+ .slice(0, query.limit ?? 100);
5168
+ return { goals };
5169
+ });
3449
5170
  app.get("/api/v1/goals/:id", async (request, reply) => {
3450
5171
  const { id } = request.params;
3451
5172
  const goal = getGoalById(id);
@@ -3457,7 +5178,9 @@ export async function buildServer(options = {}) {
3457
5178
  });
3458
5179
  app.get("/api/v1/tasks", async (request) => {
3459
5180
  const query = taskListQuerySchema.parse(request.query ?? {});
3460
- return { tasks: listTasks(query) };
5181
+ return {
5182
+ tasks: filterOwnedEntities("task", listTasks(query), query.userIds)
5183
+ };
3461
5184
  });
3462
5185
  app.get("/api/v1/calendar/overview", async (request) => {
3463
5186
  const query = calendarOverviewQuerySchema.parse(request.query ?? {});
@@ -3466,7 +5189,9 @@ export async function buildServer(options = {}) {
3466
5189
  new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
3467
5190
  const to = query.to ??
3468
5191
  new Date(now.getTime() + 21 * 24 * 60 * 60 * 1000).toISOString();
3469
- return { calendar: readCalendarOverview({ from, to }) };
5192
+ return {
5193
+ calendar: readCalendarOverview({ from, to, userIds: query.userIds })
5194
+ };
3470
5195
  });
3471
5196
  app.get("/api/v1/calendar/agenda", async (request) => {
3472
5197
  const query = calendarOverviewQuerySchema.parse(request.query ?? {});
@@ -3478,9 +5203,9 @@ export async function buildServer(options = {}) {
3478
5203
  return {
3479
5204
  providers: listCalendarProviderMetadata(),
3480
5205
  calendars: listCalendars(),
3481
- events: listCalendarEvents({ from, to }),
3482
- workBlocks: listWorkBlockInstances({ from, to }),
3483
- timeboxes: listTaskTimeboxes({ from, to })
5206
+ events: listCalendarEvents({ from, to, userIds: query.userIds }),
5207
+ workBlocks: listWorkBlockInstances({ from, to, userIds: query.userIds }),
5208
+ timeboxes: listTaskTimeboxes({ from, to, userIds: query.userIds })
3484
5209
  };
3485
5210
  });
3486
5211
  app.get("/api/v1/calendar/connections", async () => ({
@@ -3602,7 +5327,9 @@ export async function buildServer(options = {}) {
3602
5327
  });
3603
5328
  app.get("/api/v1/habits", async (request) => {
3604
5329
  const query = habitListQuerySchema.parse(request.query ?? {});
3605
- return { habits: listHabits(query) };
5330
+ return {
5331
+ habits: filterOwnedEntities("habit", listHabits(query), query.userIds)
5332
+ };
3606
5333
  });
3607
5334
  app.get("/api/v1/habits/:id", async (request, reply) => {
3608
5335
  const { id } = request.params;
@@ -3631,7 +5358,205 @@ export async function buildServer(options = {}) {
3631
5358
  }
3632
5359
  return projectBoardPayloadSchema.parse(payload);
3633
5360
  });
3634
- app.get("/api/v1/tags", async () => ({ tags: listTags() }));
5361
+ app.get("/api/v1/users", async () => ({ users: listUsers() }));
5362
+ app.get("/api/v1/users/directory", async () => ({
5363
+ directory: buildUserDirectoryPayload()
5364
+ }));
5365
+ app.patch("/api/v1/users/access-grants/:id", async (request, reply) => {
5366
+ requireScopedAccess(request.headers, ["write"], {
5367
+ route: "/api/v1/users/access-grants/:id"
5368
+ });
5369
+ const { id } = request.params;
5370
+ const grant = updateUserAccessGrant(id, updateUserAccessGrantSchema.parse(request.body ?? {}));
5371
+ if (!grant) {
5372
+ reply.code(404);
5373
+ return { error: "User access grant not found." };
5374
+ }
5375
+ return { grant };
5376
+ });
5377
+ app.post("/api/v1/users", async (request, reply) => {
5378
+ requireScopedAccess(request.headers, ["write"], {
5379
+ route: "/api/v1/users"
5380
+ });
5381
+ const user = createUser(createUserSchema.parse(request.body ?? {}));
5382
+ reply.code(201);
5383
+ return { user };
5384
+ });
5385
+ app.patch("/api/v1/users/:id", async (request, reply) => {
5386
+ requireScopedAccess(request.headers, ["write"], {
5387
+ route: "/api/v1/users/:id"
5388
+ });
5389
+ const { id } = request.params;
5390
+ const user = updateUser(id, updateUserSchema.parse(request.body ?? {}));
5391
+ if (!user) {
5392
+ reply.code(404);
5393
+ return { error: "User not found" };
5394
+ }
5395
+ return { user };
5396
+ });
5397
+ app.get("/api/v1/users/:id", async (request, reply) => {
5398
+ const { id } = request.params;
5399
+ const user = getUserById(id);
5400
+ if (!user) {
5401
+ reply.code(404);
5402
+ return { error: "User not found" };
5403
+ }
5404
+ return { user };
5405
+ });
5406
+ app.get("/api/v1/preferences/workspace", async (request) => {
5407
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/preferences/workspace" });
5408
+ return {
5409
+ workspace: getPreferenceWorkspace(preferenceWorkspaceQuerySchema.parse(request.query ?? {}))
5410
+ };
5411
+ });
5412
+ app.post("/api/v1/preferences/game/start", async (request) => {
5413
+ requireScopedAccess(request.headers, ["write"], {
5414
+ route: "/api/v1/preferences/game/start"
5415
+ });
5416
+ return {
5417
+ workspace: startPreferenceGame(startPreferenceGameSchema.parse(request.body ?? {}))
5418
+ };
5419
+ });
5420
+ app.post("/api/v1/preferences/catalogs", async (request, reply) => {
5421
+ requireScopedAccess(request.headers, ["write"], {
5422
+ route: "/api/v1/preferences/catalogs"
5423
+ });
5424
+ const catalog = createPreferenceCatalog(createPreferenceCatalogSchema.parse(request.body ?? {}));
5425
+ reply.code(201);
5426
+ return { catalog };
5427
+ });
5428
+ app.patch("/api/v1/preferences/catalogs/:id", async (request) => {
5429
+ requireScopedAccess(request.headers, ["write"], {
5430
+ route: "/api/v1/preferences/catalogs/:id"
5431
+ });
5432
+ const { id } = request.params;
5433
+ return {
5434
+ catalog: updatePreferenceCatalog(id, updatePreferenceCatalogSchema.parse(request.body ?? {}))
5435
+ };
5436
+ });
5437
+ app.delete("/api/v1/preferences/catalogs/:id", async (request) => {
5438
+ requireScopedAccess(request.headers, ["write"], {
5439
+ route: "/api/v1/preferences/catalogs/:id"
5440
+ });
5441
+ const { id } = request.params;
5442
+ return { catalog: deletePreferenceCatalog(id) };
5443
+ });
5444
+ app.post("/api/v1/preferences/catalog-items", async (request, reply) => {
5445
+ requireScopedAccess(request.headers, ["write"], {
5446
+ route: "/api/v1/preferences/catalog-items"
5447
+ });
5448
+ const item = createPreferenceCatalogItem(createPreferenceCatalogItemSchema.parse(request.body ?? {}));
5449
+ reply.code(201);
5450
+ return { item };
5451
+ });
5452
+ app.patch("/api/v1/preferences/catalog-items/:id", async (request) => {
5453
+ requireScopedAccess(request.headers, ["write"], {
5454
+ route: "/api/v1/preferences/catalog-items/:id"
5455
+ });
5456
+ const { id } = request.params;
5457
+ return {
5458
+ item: updatePreferenceCatalogItem(id, updatePreferenceCatalogItemSchema.parse(request.body ?? {}))
5459
+ };
5460
+ });
5461
+ app.delete("/api/v1/preferences/catalog-items/:id", async (request) => {
5462
+ requireScopedAccess(request.headers, ["write"], {
5463
+ route: "/api/v1/preferences/catalog-items/:id"
5464
+ });
5465
+ const { id } = request.params;
5466
+ return { item: deletePreferenceCatalogItem(id) };
5467
+ });
5468
+ app.post("/api/v1/preferences/contexts", async (request, reply) => {
5469
+ requireScopedAccess(request.headers, ["write"], {
5470
+ route: "/api/v1/preferences/contexts"
5471
+ });
5472
+ const context = createPreferenceContext(createPreferenceContextSchema.parse(request.body ?? {}));
5473
+ reply.code(201);
5474
+ return { context };
5475
+ });
5476
+ app.patch("/api/v1/preferences/contexts/:id", async (request) => {
5477
+ requireScopedAccess(request.headers, ["write"], {
5478
+ route: "/api/v1/preferences/contexts/:id"
5479
+ });
5480
+ const { id } = request.params;
5481
+ return {
5482
+ context: updatePreferenceContext(id, updatePreferenceContextSchema.parse(request.body ?? {}))
5483
+ };
5484
+ });
5485
+ app.post("/api/v1/preferences/contexts/merge", async (request) => {
5486
+ requireScopedAccess(request.headers, ["write"], {
5487
+ route: "/api/v1/preferences/contexts/merge"
5488
+ });
5489
+ return {
5490
+ merge: mergePreferenceContexts(mergePreferenceContextsSchema.parse(request.body ?? {}))
5491
+ };
5492
+ });
5493
+ app.post("/api/v1/preferences/items", async (request, reply) => {
5494
+ requireScopedAccess(request.headers, ["write"], {
5495
+ route: "/api/v1/preferences/items"
5496
+ });
5497
+ const item = createPreferenceItem(createPreferenceItemSchema.parse(request.body ?? {}));
5498
+ reply.code(201);
5499
+ return { item };
5500
+ });
5501
+ app.patch("/api/v1/preferences/items/:id", async (request) => {
5502
+ requireScopedAccess(request.headers, ["write"], {
5503
+ route: "/api/v1/preferences/items/:id"
5504
+ });
5505
+ const { id } = request.params;
5506
+ return {
5507
+ item: updatePreferenceItem(id, updatePreferenceItemSchema.parse(request.body ?? {}))
5508
+ };
5509
+ });
5510
+ app.post("/api/v1/preferences/items/from-entity", async (request, reply) => {
5511
+ requireScopedAccess(request.headers, ["write"], {
5512
+ route: "/api/v1/preferences/items/from-entity"
5513
+ });
5514
+ const item = createPreferenceItemFromEntity(enqueueEntityPreferenceItemSchema.parse(request.body ?? {}));
5515
+ reply.code(201);
5516
+ return { item };
5517
+ });
5518
+ app.post("/api/v1/preferences/judgments", async (request, reply) => {
5519
+ requireScopedAccess(request.headers, ["write"], {
5520
+ route: "/api/v1/preferences/judgments"
5521
+ });
5522
+ const judgment = submitPairwiseJudgment(submitPairwiseJudgmentSchema.parse(request.body ?? {}));
5523
+ reply.code(201);
5524
+ return { judgment };
5525
+ });
5526
+ app.post("/api/v1/preferences/signals", async (request, reply) => {
5527
+ requireScopedAccess(request.headers, ["write"], {
5528
+ route: "/api/v1/preferences/signals"
5529
+ });
5530
+ const signal = submitAbsoluteSignal(submitAbsoluteSignalSchema.parse(request.body ?? {}));
5531
+ reply.code(201);
5532
+ return { signal };
5533
+ });
5534
+ app.patch("/api/v1/preferences/items/:id/score", async (request) => {
5535
+ requireScopedAccess(request.headers, ["write"], {
5536
+ route: "/api/v1/preferences/items/:id/score"
5537
+ });
5538
+ const { id } = request.params;
5539
+ return {
5540
+ workspace: updatePreferenceScore(id, updatePreferenceScoreSchema.parse(request.body ?? {}))
5541
+ };
5542
+ });
5543
+ app.get("/api/v1/strategies", async (request) => {
5544
+ const query = strategyListQuerySchema.parse(request.query ?? {});
5545
+ return { strategies: listStrategies(query) };
5546
+ });
5547
+ app.get("/api/v1/strategies/:id", async (request, reply) => {
5548
+ const { id } = request.params;
5549
+ const strategy = getStrategyById(id);
5550
+ if (!strategy) {
5551
+ reply.code(404);
5552
+ return { error: "Strategy not found" };
5553
+ }
5554
+ return { strategy };
5555
+ });
5556
+ app.get("/api/v1/tags", async (request) => {
5557
+ const userIds = resolveScopedUserIds(request.query);
5558
+ return { tags: filterOwnedEntities("tag", listTags(), userIds) };
5559
+ });
3635
5560
  app.get("/api/v1/tags/:id", async (request, reply) => {
3636
5561
  const { id } = request.params;
3637
5562
  const tag = getTagById(id);
@@ -3663,8 +5588,10 @@ export async function buildServer(options = {}) {
3663
5588
  app.get("/api/v1/metrics/xp", async () => ({
3664
5589
  metrics: buildXpMetricsPayload()
3665
5590
  }));
3666
- app.get("/api/v1/insights", async () => ({
3667
- insights: getInsightsPayload()
5591
+ app.get("/api/v1/insights", async (request) => ({
5592
+ insights: getInsightsPayload(new Date(), {
5593
+ userIds: resolveScopedUserIds(request.query)
5594
+ })
3668
5595
  }));
3669
5596
  app.post("/api/v1/insights", async (request, reply) => {
3670
5597
  const input = createInsightSchema.parse(request.body ?? {});
@@ -3842,6 +5769,23 @@ export async function buildServer(options = {}) {
3842
5769
  reply.code(201);
3843
5770
  return event;
3844
5771
  });
5772
+ app.post("/api/v1/diagnostics/logs", async (request, reply) => {
5773
+ const payload = createDiagnosticLogSchema.parse(request.body ?? {});
5774
+ const entry = recordDiagnosticLog({
5775
+ ...payload,
5776
+ source: payload.source ??
5777
+ normalizeDiagnosticSource(request.headers["x-forge-source"])
5778
+ });
5779
+ reply.code(201);
5780
+ return { log: entry };
5781
+ });
5782
+ app.get("/api/v1/diagnostics/logs", async (request) => {
5783
+ requireOperatorSession(request.headers, {
5784
+ route: "/api/v1/diagnostics/logs"
5785
+ });
5786
+ const query = diagnosticLogListQuerySchema.parse(request.query ?? {});
5787
+ return listDiagnosticLogs(query);
5788
+ });
3845
5789
  app.get("/api/v1/events", async (request) => {
3846
5790
  const query = eventsListQuerySchema.parse(request.query ?? {});
3847
5791
  return { events: listEventLog(query) };
@@ -3884,6 +5828,14 @@ export async function buildServer(options = {}) {
3884
5828
  reply.code(201);
3885
5829
  return { project };
3886
5830
  });
5831
+ app.post("/api/v1/strategies", async (request, reply) => {
5832
+ requireScopedAccess(request.headers, ["write"], {
5833
+ route: "/api/v1/strategies"
5834
+ });
5835
+ const strategy = createStrategy(createStrategySchema.parse(request.body ?? {}));
5836
+ reply.code(201);
5837
+ return { strategy };
5838
+ });
3887
5839
  app.post("/api/v1/calendar/connections", async (request, reply) => {
3888
5840
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/connections" });
3889
5841
  try {
@@ -3953,8 +5905,10 @@ export async function buildServer(options = {}) {
3953
5905
  }
3954
5906
  return { connection };
3955
5907
  });
3956
- app.get("/api/v1/calendar/work-block-templates", async () => ({
3957
- templates: listWorkBlockTemplates()
5908
+ app.get("/api/v1/calendar/work-block-templates", async (request) => ({
5909
+ templates: listWorkBlockTemplates({
5910
+ userIds: resolveScopedUserIds(request.query)
5911
+ })
3958
5912
  }));
3959
5913
  app.post("/api/v1/calendar/work-block-templates", async (request, reply) => {
3960
5914
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/work-block-templates" });
@@ -4027,7 +5981,9 @@ export async function buildServer(options = {}) {
4027
5981
  new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
4028
5982
  const to = query.to ??
4029
5983
  new Date(now.getTime() + 21 * 24 * 60 * 60 * 1000).toISOString();
4030
- return { timeboxes: listTaskTimeboxes({ from, to }) };
5984
+ return {
5985
+ timeboxes: listTaskTimeboxes({ from, to, userIds: query.userIds })
5986
+ };
4031
5987
  });
4032
5988
  app.post("/api/v1/calendar/timeboxes", async (request, reply) => {
4033
5989
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/timeboxes" });
@@ -4175,7 +6131,7 @@ export async function buildServer(options = {}) {
4175
6131
  });
4176
6132
  app.post("/api/v1/habits", async (request, reply) => {
4177
6133
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits" });
4178
- const habit = createHabit(createHabitSchema.parse(request.body ?? {}), toActivityContext(auth));
6134
+ const habit = createHabit(parseRequestBody(createHabitSchema, request.body), toActivityContext(auth));
4179
6135
  reply.code(201);
4180
6136
  return { habit };
4181
6137
  });
@@ -4199,10 +6155,32 @@ export async function buildServer(options = {}) {
4199
6155
  }
4200
6156
  return { project };
4201
6157
  });
6158
+ app.patch("/api/v1/strategies/:id", async (request, reply) => {
6159
+ requireScopedAccess(request.headers, ["write"], {
6160
+ route: "/api/v1/strategies/:id"
6161
+ });
6162
+ const { id } = request.params;
6163
+ const strategy = updateStrategy(id, updateStrategySchema.parse(request.body ?? {}));
6164
+ if (!strategy) {
6165
+ reply.code(404);
6166
+ return { error: "Strategy not found" };
6167
+ }
6168
+ return { strategy };
6169
+ });
6170
+ app.delete("/api/v1/strategies/:id", async (request, reply) => {
6171
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/strategies/:id" });
6172
+ const { id } = request.params;
6173
+ const strategy = deleteEntity("strategy", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
6174
+ if (!strategy) {
6175
+ reply.code(404);
6176
+ return { error: "Strategy not found" };
6177
+ }
6178
+ return { strategy };
6179
+ });
4202
6180
  app.patch("/api/v1/habits/:id", async (request, reply) => {
4203
6181
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id" });
4204
6182
  const { id } = request.params;
4205
- const habit = updateHabit(id, updateHabitSchema.parse(request.body ?? {}), toActivityContext(auth));
6183
+ const habit = updateHabit(id, parseRequestBody(updateHabitSchema, request.body), toActivityContext(auth));
4206
6184
  if (!habit) {
4207
6185
  reply.code(404);
4208
6186
  return { error: "Habit not found" };
@@ -4222,7 +6200,17 @@ export async function buildServer(options = {}) {
4222
6200
  app.post("/api/v1/habits/:id/check-ins", async (request, reply) => {
4223
6201
  const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id/check-ins" });
4224
6202
  const { id } = request.params;
4225
- const habit = createHabitCheckIn(id, createHabitCheckInSchema.parse(request.body ?? {}), toActivityContext(auth));
6203
+ const habit = createHabitCheckIn(id, parseRequestBody(createHabitCheckInSchema, request.body), toActivityContext(auth));
6204
+ if (!habit) {
6205
+ reply.code(404);
6206
+ return { error: "Habit not found" };
6207
+ }
6208
+ return { habit, metrics: buildXpMetricsPayload() };
6209
+ });
6210
+ app.delete("/api/v1/habits/:id/check-ins/:dateKey", async (request, reply) => {
6211
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id/check-ins/:dateKey" });
6212
+ const { id, dateKey } = request.params;
6213
+ const habit = deleteHabitCheckIn(id, dateKey, toActivityContext(auth));
4226
6214
  if (!habit) {
4227
6215
  reply.code(404);
4228
6216
  return { error: "Habit not found" };