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.
- package/README.md +133 -2
- package/dist/assets/board-_C6oMy5w.js +6 -0
- package/dist/assets/{board-8L3uX7_O.js.map → board-_C6oMy5w.js.map} +1 -1
- package/dist/assets/index-B4A6TooJ.js +63 -0
- package/dist/assets/index-B4A6TooJ.js.map +1 -0
- package/dist/assets/index-D6Xs_2mo.css +1 -0
- package/dist/assets/{motion-1GAqqi8M.js → motion-D4sZgCHd.js} +2 -2
- package/dist/assets/{motion-1GAqqi8M.js.map → motion-D4sZgCHd.js.map} +1 -1
- package/dist/assets/{table-DBGlgRjk.js → table-BWzTaky1.js} +2 -2
- package/dist/assets/{table-DBGlgRjk.js.map → table-BWzTaky1.js.map} +1 -1
- package/dist/assets/{ui-iTluWjC4.js → ui-BzK4azQb.js} +7 -7
- package/dist/assets/{ui-iTluWjC4.js.map → ui-BzK4azQb.js.map} +1 -1
- package/dist/assets/vendor-DT3pnAKJ.css +1 -0
- package/dist/assets/vendor-De38P6YR.js +729 -0
- package/dist/assets/vendor-De38P6YR.js.map +1 -0
- package/dist/assets/viz-C6hfyqzu.js +34 -0
- package/dist/assets/viz-C6hfyqzu.js.map +1 -0
- package/dist/index.html +9 -9
- package/dist/openclaw/parity.d.ts +1 -1
- package/dist/openclaw/parity.js +29 -2
- package/dist/openclaw/routes.js +207 -24
- package/dist/openclaw/tools.js +324 -35
- package/dist/server/app.js +2080 -92
- package/dist/server/db.js +3 -0
- package/dist/server/health.js +1284 -0
- package/dist/server/managers/platform/background-job-manager.js +138 -2
- package/dist/server/managers/platform/llm-manager.js +126 -0
- package/dist/server/managers/platform/openai-responses-provider.js +773 -0
- package/dist/server/managers/runtime.js +6 -1
- package/dist/server/openapi.js +718 -0
- package/dist/server/preferences-seeds.js +409 -0
- package/dist/server/preferences-types.js +368 -0
- package/dist/server/psyche-types.js +42 -18
- package/dist/server/repositories/activity-events.js +53 -4
- package/dist/server/repositories/calendar.js +89 -15
- package/dist/server/repositories/collaboration.js +8 -3
- package/dist/server/repositories/diagnostic-logs.js +243 -0
- package/dist/server/repositories/entity-ownership.js +92 -0
- package/dist/server/repositories/goals.js +7 -2
- package/dist/server/repositories/habits.js +122 -16
- package/dist/server/repositories/notes.js +119 -41
- package/dist/server/repositories/preferences.js +1765 -0
- package/dist/server/repositories/projects.js +18 -7
- package/dist/server/repositories/psyche.js +84 -27
- package/dist/server/repositories/rewards.js +112 -4
- package/dist/server/repositories/strategies.js +450 -0
- package/dist/server/repositories/tags.js +11 -6
- package/dist/server/repositories/task-runs.js +10 -2
- package/dist/server/repositories/tasks.js +99 -17
- package/dist/server/repositories/users.js +417 -0
- package/dist/server/repositories/wiki-memory.js +3366 -0
- package/dist/server/services/context.js +20 -18
- package/dist/server/services/dashboard.js +29 -6
- package/dist/server/services/entity-crud.js +21 -3
- package/dist/server/services/insights.js +9 -7
- package/dist/server/services/projects.js +2 -1
- package/dist/server/services/psyche.js +10 -9
- package/dist/server/types.js +594 -30
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/015_multi_user_and_strategies.sql +244 -0
- package/server/migrations/016_health_companion.sql +158 -0
- package/server/migrations/016_strategy_contracts_and_user_graph.sql +22 -0
- package/server/migrations/017_preferences.sql +131 -0
- package/server/migrations/018_preference_catalogs.sql +31 -0
- package/server/migrations/019_wiki_memory.sql +255 -0
- package/server/migrations/020_wiki_page_hierarchy.sql +11 -0
- package/server/migrations/021_hide_evidence_from_wiki_index.sql +3 -0
- package/server/migrations/022_wiki_ingest_background.sql +85 -0
- package/server/migrations/023_diagnostic_logs.sql +28 -0
- package/skills/forge-openclaw/SKILL.md +126 -34
- package/skills/forge-openclaw/entity_conversation_playbooks.md +337 -0
- package/skills/forge-openclaw/psyche_entity_playbooks.md +404 -0
- package/dist/assets/board-8L3uX7_O.js +0 -6
- package/dist/assets/index-Cj1IBH_w.js +0 -36
- package/dist/assets/index-Cj1IBH_w.js.map +0 -1
- package/dist/assets/index-DQT6EbuS.css +0 -1
- package/dist/assets/vendor-BvM2F9Dp.js +0 -503
- package/dist/assets/vendor-BvM2F9Dp.js.map +0 -1
- package/dist/assets/vendor-CRS-psbw.css +0 -1
- package/dist/assets/viz-CNeunkfu.js +0 -34
- package/dist/assets/viz-CNeunkfu.js.map +0 -1
package/dist/server/app.js
CHANGED
|
@@ -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 {
|
|
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"
|
|
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:
|
|
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
|
-
"
|
|
1747
|
-
"Identify the typical cue or context.",
|
|
1748
|
-
"
|
|
1749
|
-
"Clarify the short-term payoff or
|
|
1750
|
-
"Clarify the long-term cost.",
|
|
1751
|
-
"
|
|
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
|
-
"
|
|
1767
|
-
"What
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
1819
|
-
"
|
|
1820
|
-
"
|
|
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
|
-
"
|
|
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:
|
|
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
|
|
2480
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
|
|
2491
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
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:
|
|
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({
|
|
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,
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 () =>
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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/
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
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" };
|