forge-openclaw-plugin 0.2.26 → 0.2.28
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 +60 -3
- package/dist/assets/{board-ta0rUHOf.js → board-DPFvZf-D.js} +2 -2
- package/dist/assets/{board-ta0rUHOf.js.map → board-DPFvZf-D.js.map} +1 -1
- package/dist/assets/index-Auw3JrdE.css +1 -0
- package/dist/assets/index-D1H7myQH.js +85 -0
- package/dist/assets/index-D1H7myQH.js.map +1 -0
- package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js +2 -0
- package/dist/assets/knowledge-graph-layout.worker-DRvzPxhP.js.map +1 -0
- package/dist/assets/{motion-fBKPB6yw.js → motion-Bvwc85ch.js} +2 -2
- package/dist/assets/{motion-fBKPB6yw.js.map → motion-Bvwc85ch.js.map} +1 -1
- package/dist/assets/{table-C-IGTQni.js → table-FJQTJvUR.js} +2 -2
- package/dist/assets/{table-C-IGTQni.js.map → table-FJQTJvUR.js.map} +1 -1
- package/dist/assets/{ui-DInOpaYF.js → ui-GXFcgvSw.js} +2 -2
- package/dist/assets/{ui-DInOpaYF.js.map → ui-GXFcgvSw.js.map} +1 -1
- package/dist/assets/vendor-Cwf49UMz.js +1247 -0
- package/dist/assets/vendor-Cwf49UMz.js.map +1 -0
- package/dist/index.html +7 -7
- package/dist/openclaw/local-runtime.js +16 -0
- package/dist/openclaw/routes.d.ts +27 -0
- package/dist/openclaw/routes.js +16 -12
- package/dist/server/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
- package/dist/server/server/migrations/038_data_management_settings.sql +11 -0
- package/dist/server/server/migrations/039_life_force_and_action_points.sql +114 -0
- package/dist/server/server/migrations/040_screen_time_domain.sql +89 -0
- package/dist/server/server/migrations/041_companion_source_states.sql +21 -0
- package/dist/server/server/migrations/042_movement_boxes.sql +47 -0
- package/dist/server/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
- package/dist/server/server/src/app.js +1900 -91
- package/dist/server/server/src/connectors/box-registry.js +44 -9
- package/dist/server/server/src/data-management-types.js +107 -0
- package/dist/server/server/src/db.js +68 -4
- package/dist/server/server/src/demo-data.js +2 -2
- package/dist/server/server/src/health.js +702 -18
- package/dist/server/server/src/managers/platform/llm-manager.js +7 -4
- package/dist/server/server/src/managers/platform/mock-workbench-provider.js +149 -0
- package/dist/server/server/src/managers/platform/secrets-manager.js +18 -1
- package/dist/server/server/src/managers/runtime.js +9 -0
- package/dist/server/server/src/movement.js +1971 -112
- package/dist/server/server/src/openapi.js +1390 -105
- package/dist/server/server/src/psyche-types.js +9 -1
- package/dist/server/server/src/repositories/activity-events.js +8 -0
- package/dist/server/server/src/repositories/ai-connectors.js +522 -74
- package/dist/server/server/src/repositories/calendar.js +151 -0
- package/dist/server/server/src/repositories/habits.js +37 -1
- package/dist/server/server/src/repositories/model-settings.js +13 -3
- package/dist/server/server/src/repositories/notes.js +3 -0
- package/dist/server/server/src/repositories/settings.js +380 -18
- package/dist/server/server/src/repositories/tasks.js +170 -10
- package/dist/server/server/src/runtime-data-root.js +82 -0
- package/dist/server/server/src/screen-time.js +802 -0
- package/dist/server/server/src/services/data-management.js +788 -0
- package/dist/server/server/src/services/entity-crud.js +205 -2
- package/dist/server/server/src/services/knowledge-graph.js +1455 -0
- package/dist/server/server/src/services/life-force-model.js +217 -0
- package/dist/server/server/src/services/life-force.js +2506 -0
- package/dist/server/server/src/services/psyche-observation-calendar.js +383 -16
- package/dist/server/server/src/types.js +307 -14
- package/dist/server/server/src/web.js +228 -13
- package/dist/server/src/components/customization/utility-widgets.js +136 -27
- package/dist/server/src/components/ui/info-tooltip.js +25 -0
- package/dist/server/src/components/workbench-boxes/calendar/calendar-boxes.js +78 -0
- package/dist/server/src/components/workbench-boxes/goals/goals-boxes.js +62 -0
- package/dist/server/src/components/workbench-boxes/habits/habits-boxes.js +62 -0
- package/dist/server/src/components/workbench-boxes/health/health-boxes.js +63 -8
- package/dist/server/src/components/workbench-boxes/insights/insights-boxes.js +50 -0
- package/dist/server/src/components/workbench-boxes/kanban/kanban-boxes.js +62 -54
- package/dist/server/src/components/workbench-boxes/movement/movement-boxes.js +18 -8
- package/dist/server/src/components/workbench-boxes/notes/notes-boxes.js +56 -38
- package/dist/server/src/components/workbench-boxes/overview/overview-boxes.js +65 -0
- package/dist/server/src/components/workbench-boxes/preferences/preferences-boxes.js +78 -0
- package/dist/server/src/components/workbench-boxes/projects/projects-boxes.js +35 -30
- package/dist/server/src/components/workbench-boxes/psyche/psyche-boxes.js +88 -0
- package/dist/server/src/components/workbench-boxes/questionnaires/questionnaires-boxes.js +61 -0
- package/dist/server/src/components/workbench-boxes/review/review-boxes.js +53 -0
- package/dist/server/src/components/workbench-boxes/shared/define-workbench-box.js +3 -1
- package/dist/server/src/components/workbench-boxes/shared/generic-node-view.js +39 -3
- package/dist/server/src/components/workbench-boxes/strategies/strategies-boxes.js +62 -0
- package/dist/server/src/components/workbench-boxes/tasks/tasks-boxes.js +76 -0
- package/dist/server/src/components/workbench-boxes/today/today-boxes.js +47 -32
- package/dist/server/src/components/workbench-boxes/wiki/wiki-boxes.js +60 -0
- package/dist/server/src/lib/api.js +280 -21
- package/dist/server/src/lib/data-management-types.js +1 -0
- package/dist/server/src/lib/entity-visuals.js +279 -0
- package/dist/server/src/lib/knowledge-graph-types.js +276 -0
- package/dist/server/src/lib/knowledge-graph.js +470 -0
- package/dist/server/src/lib/schemas.js +4 -0
- package/dist/server/src/lib/snapshot-normalizer.js +45 -1
- package/dist/server/src/lib/workbench/contracts.js +229 -0
- package/dist/server/src/lib/workbench/nodes.js +200 -0
- package/dist/server/src/lib/workbench/registry.js +52 -5
- package/dist/server/src/lib/workbench/runtime.js +254 -38
- package/dist/server/src/lib/workbench/tool-catalog.js +68 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/037_workbench_public_inputs_and_run_inputs.sql +5 -0
- package/server/migrations/038_data_management_settings.sql +11 -0
- package/server/migrations/039_life_force_and_action_points.sql +114 -0
- package/server/migrations/040_screen_time_domain.sql +89 -0
- package/server/migrations/041_companion_source_states.sql +21 -0
- package/server/migrations/042_movement_boxes.sql +47 -0
- package/server/migrations/043_movement_box_overlap_overrides.sql +26 -0
- package/skills/forge-openclaw/SKILL.md +41 -11
- package/skills/forge-openclaw/entity_conversation_playbooks.md +448 -34
- package/skills/forge-openclaw/psyche_entity_playbooks.md +170 -17
- package/dist/assets/index-Ro0ZF_az.css +0 -1
- package/dist/assets/index-ytlpSj23.js +0 -79
- package/dist/assets/index-ytlpSj23.js.map +0 -1
- package/dist/assets/vendor-lE3tZJcC.js +0 -876
- package/dist/assets/vendor-lE3tZJcC.js.map +0 -1
|
@@ -3,11 +3,11 @@ import cors from "@fastify/cors";
|
|
|
3
3
|
import multipart from "@fastify/multipart";
|
|
4
4
|
import { CronExpressionParser } from "cron-parser";
|
|
5
5
|
import { z, ZodError } from "zod";
|
|
6
|
-
import { configureDatabase, configureDatabaseSeeding, runInTransaction } from "./db.js";
|
|
6
|
+
import { configureDatabase, configureDatabaseSeeding, getEffectiveDataRoot, resolveDataDir, resolveDatabasePathForDataRoot, runInTransaction } from "./db.js";
|
|
7
7
|
import { HttpError, isHttpError } from "./errors.js";
|
|
8
8
|
import { listActivityEvents, listActivityEventsForTask, recordActivityEvent, removeActivityEvent } from "./repositories/activity-events.js";
|
|
9
9
|
import { approveApprovalRequest, createAgentAction, createInsight, createInsightFeedback, deleteInsight, getInsightById, listAgentActions, listApprovalRequests, listInsights, rejectApprovalRequest, updateInsight } from "./repositories/collaboration.js";
|
|
10
|
-
import { createAiConnector, deleteAiConnector, getAiConnectorById, getAiConnectorBySlug, getAiConnectorConversationForConnector, listAiConnectorRuns, listAiConnectors, runAiConnector, updateAiConnector } from "./repositories/ai-connectors.js";
|
|
10
|
+
import { createAiConnector, deleteAiConnector, getAiConnectorById, getAiConnectorRunById, getAiConnectorRunNodeResult, getAiConnectorRunNodeResults, getLatestAiConnectorNodeOutput, getAiConnectorBySlug, getAiConnectorConversationForConnector, listAiConnectorRuns, listAiConnectors, runAiConnector, updateAiConnector } from "./repositories/ai-connectors.js";
|
|
11
11
|
import { createAiProcessor, createAiProcessorLink, deleteAiProcessor, deleteAiProcessorLink, getAiProcessorById, getAiProcessorBySlug, listAiProcessors, getSurfaceProcessorGraph, runAiProcessor, updateAiProcessor } from "./repositories/ai-processors.js";
|
|
12
12
|
import { listEventLog } from "./repositories/event-log.js";
|
|
13
13
|
import { createDiagnosticMessage, DIAGNOSTIC_LOG_RETENTION_SWEEP_INTERVAL_MS, enforceDiagnosticLogRetention, listDiagnosticLogs, normalizeDiagnosticSource, recordDiagnosticLog, serializeDiagnosticError } from "./repositories/diagnostic-logs.js";
|
|
@@ -20,27 +20,30 @@ import { buildNotesSummaryByEntity, createNote, getNoteById, listNotes, updateNo
|
|
|
20
20
|
import { createWikiIngestJobSchema, createUploadedWikiIngestJob, createWikiSpace, createWikiSpaceSchema, deleteWikiIngestJob, deleteWikiProfile, getWikiHealth, getWikiIngestJob, getWikiHomePageDetail, getWikiPageDetail, getWikiPageDetailBySlug, getWikiSettingsPayload, ingestWikiSource, listWikiIngestJobs, listWikiLlmProfiles, listWikiPageTree, listWikiPages, listWikiSpaces, processWikiIngestJob, reindexWikiEmbeddings, reindexWikiEmbeddingsSchema, rerunWikiIngestJob, reviewWikiIngestJob, reviewWikiIngestJobSchema, searchWikiPages, syncWikiVaultFromDisk, syncWikiVaultSchema, testWikiLlmProfileSchema, upsertWikiEmbeddingProfile, upsertWikiEmbeddingProfileSchema, upsertWikiLlmProfile, upsertWikiLlmProfileSchema, wikiSearchQuerySchema } from "./repositories/wiki-memory.js";
|
|
21
21
|
import { filterOwnedEntities, setEntityOwner } from "./repositories/entity-ownership.js";
|
|
22
22
|
import { createBehavior, createBehaviorPattern, createBeliefEntry, createEmotionDefinition, createEventType, createModeGuideSession, createModeProfile, createPsycheValue, createTriggerReport, getBehaviorById, getBehaviorPatternById, getBeliefEntryById, getEmotionDefinitionById, getEventTypeById, getModeGuideSessionById, getModeProfileById, getPsycheValueById, getTriggerReportById, listBehaviors, listBehaviorPatterns, listBeliefEntries, listEmotionDefinitions, listEventTypes, listModeGuideSessions, listModeProfiles, listPsycheValues, listSchemaCatalog, listTriggerReports, updateBehavior, updateBehaviorPattern, updateBeliefEntry, updateEmotionDefinition, updateEventType, updateModeGuideSession, updateModeProfile, updatePsycheValue, updateTriggerReport } from "./repositories/psyche.js";
|
|
23
|
-
import { cloneQuestionnaireInstrument, completeQuestionnaireRun, createQuestionnaireInstrument, ensureQuestionnaireDraftVersion, getQuestionnaireInstrumentDetail, getQuestionnaireRunDetail, listQuestionnaireInstruments, publishQuestionnaireDraftVersion, startQuestionnaireRun, updateQuestionnaireDraftVersion, updateQuestionnaireRun } from "./repositories/questionnaires.js";
|
|
23
|
+
import { cloneQuestionnaireInstrument, completeQuestionnaireRun, createQuestionnaireInstrument, deleteQuestionnaireInstrument, ensureQuestionnaireDraftVersion, getQuestionnaireInstrumentDetail, getQuestionnaireRunDetail, listQuestionnaireInstruments, publishQuestionnaireDraftVersion, startQuestionnaireRun, updateQuestionnaireInstrument, updateQuestionnaireInstrumentSchema, updateQuestionnaireDraftVersion, updateQuestionnaireRun } from "./repositories/questionnaires.js";
|
|
24
24
|
import { createProject, updateProject } from "./repositories/projects.js";
|
|
25
|
-
import { createPreferenceCatalog, createPreferenceCatalogItem, createPreferenceContext, createPreferenceItem, createPreferenceItemFromEntity, deletePreferenceCatalog, deletePreferenceCatalogItem, getPreferenceWorkspace, mergePreferenceContexts, startPreferenceGame, submitAbsoluteSignal, submitPairwiseJudgment, updatePreferenceCatalog, updatePreferenceCatalogItem, updatePreferenceContext, updatePreferenceItem, updatePreferenceScore } from "./repositories/preferences.js";
|
|
25
|
+
import { createPreferenceCatalog, createPreferenceCatalogItem, createPreferenceContext, createPreferenceItem, createPreferenceItemFromEntity, deletePreferenceCatalog, deletePreferenceCatalogItem, deletePreferenceContext, deletePreferenceItem, getPreferenceCatalogById, getPreferenceCatalogItemById, getPreferenceContextById, getPreferenceItemById, getPreferenceWorkspace, listPreferenceCatalogItems, listPreferenceCatalogs, listPreferenceContexts, listPreferenceItems, mergePreferenceContexts, startPreferenceGame, submitAbsoluteSignal, submitPairwiseJudgment, updatePreferenceCatalog, updatePreferenceCatalogItem, updatePreferenceContext, updatePreferenceItem, updatePreferenceScore } from "./repositories/preferences.js";
|
|
26
26
|
import { createStrategy, getStrategyById, listStrategies, updateStrategy } from "./repositories/strategies.js";
|
|
27
|
+
import { buildKnowledgeGraph, buildKnowledgeGraphFocus } from "./services/knowledge-graph.js";
|
|
27
28
|
import { createManualRewardGrant, getDailyAmbientXp, getRewardRuleById, listRewardLedger, listRewardRules, recordWorkAdjustmentReward, recordSessionEvent, updateRewardRule } from "./repositories/rewards.js";
|
|
28
|
-
import { listAgentIdentities, getSettings, isPsycheAuthRequired, updateSettings, verifyAgentToken } from "./repositories/settings.js";
|
|
29
|
+
import { getSettingsFileStatus, listAgentIdentities, getSettings, isPsycheAuthRequired, mirrorSettingsFileFromCurrentState, updateSettings, verifyAgentToken } from "./repositories/settings.js";
|
|
29
30
|
import { deleteAiModelConnection, getAiModelConnectionById, readModelConnectionCredential, upsertAiModelConnection } from "./repositories/model-settings.js";
|
|
30
31
|
import { createTag, getTagById, listTags, updateTag } from "./repositories/tags.js";
|
|
31
32
|
import { createUser, ensureSystemUsers, getDefaultUser, getUserById, listUserAccessGrants, listUserOwnershipSummaries, listUserXpSummaries, listUsers, resolveUserForMutation, updateUserAccessGrant, updateUser } from "./repositories/users.js";
|
|
32
33
|
import { claimTaskRun, completeTaskRun, focusTaskRun, heartbeatTaskRun, listTaskRuns, recoverTimedOutTaskRuns, releaseTaskRun } from "./repositories/task-runs.js";
|
|
33
|
-
import { createTask, createTaskWithIdempotency, getTaskById, listTasks, uncompleteTask, updateTask } from "./repositories/tasks.js";
|
|
34
|
+
import { createTask, createTaskWithIdempotency, getTaskById, listTasks, splitTask, uncompleteTask, updateTask } from "./repositories/tasks.js";
|
|
34
35
|
import { createWorkAdjustment } from "./repositories/work-adjustments.js";
|
|
35
|
-
import { createCalendarEvent, createTaskTimebox, createWorkBlockTemplate, deleteCalendarEvent, deleteTaskTimebox, deleteWorkBlockTemplate, getCalendarConnectionById, getCalendarEventById, listCalendars, listCalendarEvents, listTaskTimeboxes, suggestTaskTimeboxes, listWorkBlockInstances, listWorkBlockTemplates, updateCalendarEvent, updateTaskTimebox, updateWorkBlockTemplate } from "./repositories/calendar.js";
|
|
36
|
+
import { createCalendarEvent, createTaskTimebox, createWorkBlockTemplate, deleteCalendarEvent, deleteTaskTimebox, deleteWorkBlockTemplate, getCalendarConnectionById, getCalendarEventById, getTaskTimeboxById, getWorkBlockTemplateById, listCalendars, listCalendarEvents, listTaskTimeboxes, suggestTaskTimeboxes, listWorkBlockInstances, listWorkBlockTemplates, updateCalendarEvent, updateTaskTimebox, updateWorkBlockTemplate } from "./repositories/calendar.js";
|
|
36
37
|
import { getDashboard } from "./services/dashboard.js";
|
|
37
38
|
import { getOverviewContext, getRiskContext, getTodayContext } from "./services/context.js";
|
|
38
39
|
import { buildGamificationOverview, buildGamificationProfile, buildXpMomentumPulse } from "./services/gamification.js";
|
|
39
40
|
import { getInsightsPayload } from "./services/insights.js";
|
|
41
|
+
import { buildLifeForcePayload, createFatigueSignal, listLifeForceTemplates, resolveLifeForceUser, updateLifeForceProfile, updateLifeForceTemplate } from "./services/life-force.js";
|
|
40
42
|
import { createEntities, deleteEntities, deleteEntity, getSettingsBinPayload, restoreEntities, searchEntities, updateEntities } from "./services/entity-crud.js";
|
|
41
43
|
import { getPsycheOverview } from "./services/psyche.js";
|
|
42
|
-
import { getPsycheObservationCalendar } from "./services/psyche-observation-calendar.js";
|
|
44
|
+
import { exportPsycheObservationCalendar, getPsycheObservationCalendar } from "./services/psyche-observation-calendar.js";
|
|
43
45
|
import { getProjectBoard, getProjectSummary, listProjectSummaries } from "./services/projects.js";
|
|
46
|
+
import { createDataBackup, exportData, getDataManagementState, maybeRunAutomaticBackup, restoreDataBackup, scanForDataRecoveryCandidates, switchDataRoot, updateDataManagementSettings } from "./services/data-management.js";
|
|
44
47
|
import { getWeeklyReviewPayload } from "./services/reviews.js";
|
|
45
48
|
import { finalizeWeeklyReviewClosure } from "./repositories/weekly-reviews.js";
|
|
46
49
|
import { createTaskRunWatchdog } from "./services/task-run-watchdog.js";
|
|
@@ -50,13 +53,15 @@ import { consumeOpenAiCodexOauthCredentials, getOpenAiCodexOauthSession, startOp
|
|
|
50
53
|
import { PSYCHE_ENTITY_TYPES, createBehaviorSchema, createBeliefEntrySchema, createBehaviorPatternSchema, createEmotionDefinitionSchema, createEventTypeSchema, createModeGuideSessionSchema, createModeProfileSchema, createPsycheValueSchema, createTriggerReportSchema, updateBehaviorSchema, updateBeliefEntrySchema, updateBehaviorPatternSchema, updateEmotionDefinitionSchema, updateEventTypeSchema, updateModeGuideSessionSchema, updateModeProfileSchema, updatePsycheValueSchema, updateTriggerReportSchema } from "./psyche-types.js";
|
|
51
54
|
import { createQuestionnaireInstrumentSchema, publishQuestionnaireVersionSchema, startQuestionnaireRunSchema, updateQuestionnaireRunSchema, updateQuestionnaireVersionSchema } from "./questionnaire-types.js";
|
|
52
55
|
import { createPreferenceCatalogItemSchema, createPreferenceCatalogSchema, createPreferenceContextSchema, createPreferenceItemSchema, enqueueEntityPreferenceItemSchema, mergePreferenceContextsSchema, preferenceWorkspaceQuerySchema, startPreferenceGameSchema, submitAbsoluteSignalSchema, submitPairwiseJudgmentSchema, updatePreferenceCatalogItemSchema, updatePreferenceCatalogSchema, updatePreferenceContextSchema, updatePreferenceItemSchema, updatePreferenceScoreSchema } from "./preferences-types.js";
|
|
53
|
-
import {
|
|
56
|
+
import { createDataBackupSchema, dataExportQuerySchema, restoreDataBackupSchema, switchDataRootSchema, updateDataManagementSettingsSchema } from "./data-management-types.js";
|
|
57
|
+
import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, createAiConnectorSchema, createAiProcessorLinkSchema, createAiProcessorSchema, runAiConnectorSchema, writeSurfaceLayoutSchema, upsertAiModelConnectionSchema, testAiModelConnectionSchema, submitOpenAiCodexOauthManualCodeSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createStrategySchema, createUserSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createCalendarEventSchema, createHabitCheckInSchema, createCalendarConnectionSchema, createDiagnosticLogSchema, discoverCalendarConnectionSchema, startGoogleCalendarOauthSchema, startMicrosoftCalendarOauthSchema, testMicrosoftCalendarOauthConfigurationSchema, createHabitSchema, createTaskTimeboxSchema, createWorkBlockTemplateSchema, createSessionEventSchema, createWorkAdjustmentSchema, createTagSchema, calendarOverviewQuerySchema, psycheObservationCalendarExportQuerySchema, notesListQuerySchema, updateTagSchema, createTaskSchema, diagnosticLogListQuerySchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskSplitCreateSchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateStrategySchema, updateUserSchema, updateCalendarConnectionSchema, updateCalendarEventSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskTimeboxSchema, updateTaskSchema, lifeForceProfilePatchSchema, lifeForceTemplateUpdateSchema, fatigueSignalCreateSchema, updateUserAccessGrantSchema, updateWorkBlockTemplateSchema, updateAiConnectorSchema, updateAiProcessorSchema, runAiProcessorSchema, workAdjustmentResultSchema, finalizeWeeklyReviewResultSchema, goalListQuerySchema, recommendTaskTimeboxesSchema, strategyListQuerySchema } from "./types.js";
|
|
54
58
|
import { buildOpenApiDocument } from "./openapi.js";
|
|
55
59
|
import { registerWebRoutes } from "./web.js";
|
|
56
60
|
import { createManagerRuntime } from "./managers/runtime.js";
|
|
57
61
|
import { isManagerError } from "./managers/type-guards.js";
|
|
58
|
-
import { createCompanionPairingSession, createCompanionPairingSessionSchema, getCompanionOverview, getFitnessViewData, getSleepViewData, ingestMobileHealthSync, mobileHealthSyncSchema, requireValidPairing, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
|
|
59
|
-
import { createMovementPlace, getMovementAllTimeSummary, getMovementDayDetail, getMovementMobileBootstrap, getMovementTimeline, getMovementSelectionAggregate, getMovementSettings, getMovementTripDetail, getMovementMonthSummary, listMovementPlaces, movementMobileBootstrapSchema, movementMobilePlaceMutationSchema,
|
|
62
|
+
import { createCompanionPairingSession, createCompanionPairingSessionSchema, createSleepSession, createSleepSessionSchema, createWorkoutSession, createWorkoutSessionSchema, deleteSleepSession, deleteWorkoutSession, getCompanionPairingSessionById, getCompanionOverview, getFitnessViewData, getSleepSessionById, getSleepViewData, getWorkoutSessionById, ingestMobileHealthSync, mobileHealthSyncSchema, patchCompanionPairingSourceState, patchCompanionPairingSourceStateSchema, companionSourceKeySchema, requireValidPairing, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, updateMobileCompanionSourceState, updateMobileCompanionSourceStateSchema, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
|
|
63
|
+
import { analyzeMovementUserBoxPreflight, createMovementUserBox, createMovementPlace, deleteMovementUserBox, getMovementAllTimeSummary, getMovementDayDetail, getMovementMobileBootstrap, getMovementTimeline, getMovementSelectionAggregate, getMovementSettings, getMovementTripDetail, getMovementMonthSummary, invalidateAutomaticMovementBox, listMovementPlaces, movementAutomaticBoxInvalidateSchema, movementMobileBootstrapSchema, movementMobilePlaceMutationSchema, movementMobileUserBoxCreateSchema, movementMobileUserBoxPreflightSchema, movementMobileUserBoxPatchSchema, movementMobileAutomaticBoxInvalidateSchema, movementMobileTimelineSchema, movementPlaceMutationSchema, movementPlacePatchSchema, movementSelectionAggregateSchema, movementStayPatchSchema, movementTripPatchSchema, movementUserBoxCreateSchema, movementUserBoxPreflightSchema, movementUserBoxPatchSchema, movementSettingsPatchSchema, movementTimelineQuerySchema, movementTripPointPatchSchema, deleteMovementStay, deleteMovementTrip, deleteMovementTripPoint, updateMovementPlace, updateMovementSettings, updateMovementStay, updateMovementTrip, updateMovementUserBox, updateMovementTripPoint, resolveMovementTimelineSegmentForBox } from "./movement.js";
|
|
64
|
+
import { getScreenTimeAllTimeSummary, getScreenTimeDayDetail, getScreenTimeMonthSummary, getScreenTimeSettings, screenTimeSettingsPatchSchema, updateScreenTimeSettings } from "./screen-time.js";
|
|
60
65
|
import { assertWatchReady, buildWatchBootstrap, ingestWatchCaptureBatch, mobileWatchBootstrapSchema, mobileWatchCaptureBatchSchema, mobileWatchHabitCheckInSchema } from "./watch-mobile.js";
|
|
61
66
|
const COMPATIBILITY_SUNSET = "transitional-node";
|
|
62
67
|
function markCompatibilityRoute(reply) {
|
|
@@ -162,7 +167,7 @@ function getRequestOrigin(request) {
|
|
|
162
167
|
request.hostname;
|
|
163
168
|
return `${protocol}://${host}`;
|
|
164
169
|
}
|
|
165
|
-
const
|
|
170
|
+
const AGENT_ONBOARDING_ENTITY_CATALOG_BASE = [
|
|
166
171
|
{
|
|
167
172
|
entityType: "goal",
|
|
168
173
|
purpose: "A long-horizon outcome or direction. Goals anchor projects and tasks.",
|
|
@@ -1872,38 +1877,860 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
1872
1877
|
defaultValue: []
|
|
1873
1878
|
},
|
|
1874
1879
|
{
|
|
1875
|
-
name: "modeTimeline",
|
|
1876
|
-
type: "array",
|
|
1877
|
-
required: false,
|
|
1878
|
-
description: "List of { stage, modeId|null, label, note } items describing the sequence of modes.",
|
|
1879
|
-
defaultValue: []
|
|
1880
|
+
name: "modeTimeline",
|
|
1881
|
+
type: "array",
|
|
1882
|
+
required: false,
|
|
1883
|
+
description: "List of { stage, modeId|null, label, note } items describing the sequence of modes.",
|
|
1884
|
+
defaultValue: []
|
|
1885
|
+
},
|
|
1886
|
+
{
|
|
1887
|
+
name: "nextMoves",
|
|
1888
|
+
type: "string[]",
|
|
1889
|
+
required: false,
|
|
1890
|
+
description: "Concrete next steps or repair moves.",
|
|
1891
|
+
defaultValue: []
|
|
1892
|
+
}
|
|
1893
|
+
]
|
|
1894
|
+
}
|
|
1895
|
+
];
|
|
1896
|
+
const AGENT_ONBOARDING_BATCH_ROUTE_BASES = {
|
|
1897
|
+
goal: "/api/v1/goals",
|
|
1898
|
+
project: "/api/v1/projects",
|
|
1899
|
+
task: "/api/v1/tasks",
|
|
1900
|
+
strategy: "/api/v1/strategies",
|
|
1901
|
+
habit: "/api/v1/habits",
|
|
1902
|
+
tag: "/api/v1/tags",
|
|
1903
|
+
note: "/api/v1/notes",
|
|
1904
|
+
insight: "/api/v1/insights",
|
|
1905
|
+
calendar_event: "/api/v1/calendar/events",
|
|
1906
|
+
work_block_template: "/api/v1/calendar/work-block-templates",
|
|
1907
|
+
task_timebox: "/api/v1/calendar/timeboxes",
|
|
1908
|
+
sleep_session: "/api/v1/health/sleep",
|
|
1909
|
+
workout_session: "/api/v1/health/workouts",
|
|
1910
|
+
psyche_value: "/api/v1/psyche/values",
|
|
1911
|
+
behavior_pattern: "/api/v1/psyche/patterns",
|
|
1912
|
+
behavior: "/api/v1/psyche/behaviors",
|
|
1913
|
+
belief_entry: "/api/v1/psyche/beliefs",
|
|
1914
|
+
mode_profile: "/api/v1/psyche/modes",
|
|
1915
|
+
mode_guide_session: "/api/v1/psyche/mode-guides",
|
|
1916
|
+
event_type: "/api/v1/psyche/event-types",
|
|
1917
|
+
emotion_definition: "/api/v1/psyche/emotions",
|
|
1918
|
+
trigger_report: "/api/v1/psyche/reports",
|
|
1919
|
+
preference_catalog: "/api/v1/preferences/catalogs",
|
|
1920
|
+
preference_catalog_item: "/api/v1/preferences/catalog-items",
|
|
1921
|
+
preference_context: "/api/v1/preferences/contexts",
|
|
1922
|
+
preference_item: "/api/v1/preferences/items",
|
|
1923
|
+
questionnaire_instrument: "/api/v1/psyche/questionnaires"
|
|
1924
|
+
};
|
|
1925
|
+
function classifyOnboardingEntity(entityType) {
|
|
1926
|
+
if (entityType in AGENT_ONBOARDING_BATCH_ROUTE_BASES) {
|
|
1927
|
+
return "batch_crud_entity";
|
|
1928
|
+
}
|
|
1929
|
+
if (entityType === "wiki_page" || entityType === "calendar_connection") {
|
|
1930
|
+
return "specialized_crud_entity";
|
|
1931
|
+
}
|
|
1932
|
+
if (entityType === "task_run" ||
|
|
1933
|
+
entityType === "questionnaire_run" ||
|
|
1934
|
+
entityType === "preference_judgment" ||
|
|
1935
|
+
entityType === "preference_signal" ||
|
|
1936
|
+
entityType === "work_adjustment") {
|
|
1937
|
+
return "action_workflow_entity";
|
|
1938
|
+
}
|
|
1939
|
+
return "read_model_only_surface";
|
|
1940
|
+
}
|
|
1941
|
+
function buildPreferredMutationPath(entityType) {
|
|
1942
|
+
if (entityType in AGENT_ONBOARDING_BATCH_ROUTE_BASES) {
|
|
1943
|
+
return "/api/v1/entities/create | /api/v1/entities/update | /api/v1/entities/delete | /api/v1/entities/search";
|
|
1944
|
+
}
|
|
1945
|
+
switch (entityType) {
|
|
1946
|
+
case "wiki_page":
|
|
1947
|
+
return "Use /api/v1/wiki/pages with POST or PATCH for page CRUD.";
|
|
1948
|
+
case "calendar_connection":
|
|
1949
|
+
return "Use /api/v1/calendar/connections plus provider-specific setup flows.";
|
|
1950
|
+
case "task_run":
|
|
1951
|
+
return "Use the task-run action routes to start, heartbeat, focus, complete, or release live work.";
|
|
1952
|
+
case "questionnaire_run":
|
|
1953
|
+
return "Use the questionnaire-run action routes to start, patch answers, and complete the run.";
|
|
1954
|
+
case "preference_judgment":
|
|
1955
|
+
return "Use /api/v1/preferences/judgments to record one pairwise comparison.";
|
|
1956
|
+
case "preference_signal":
|
|
1957
|
+
return "Use /api/v1/preferences/signals to record one direct signal such as favorite or veto.";
|
|
1958
|
+
case "work_adjustment":
|
|
1959
|
+
return "Use /api/v1/work-adjustments to apply an explicit operator adjustment.";
|
|
1960
|
+
case "self_observation":
|
|
1961
|
+
return "Read the calendar surface; mutate it by creating or updating note-backed observations with frontmatter.observedAt.";
|
|
1962
|
+
case "sleep_overview":
|
|
1963
|
+
return "Read-only surface. Use batch CRUD for sleep_session records or the review enrichment route for reflective notes.";
|
|
1964
|
+
case "sports_overview":
|
|
1965
|
+
return "Read-only surface. Use batch CRUD for workout_session records or the review enrichment route for reflective notes.";
|
|
1966
|
+
default:
|
|
1967
|
+
return "Read-only surface.";
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
function buildPreferredReadPath(entityType) {
|
|
1971
|
+
if (entityType in AGENT_ONBOARDING_BATCH_ROUTE_BASES) {
|
|
1972
|
+
return AGENT_ONBOARDING_BATCH_ROUTE_BASES[entityType];
|
|
1973
|
+
}
|
|
1974
|
+
switch (entityType) {
|
|
1975
|
+
case "wiki_page":
|
|
1976
|
+
return "/api/v1/wiki/pages/:id";
|
|
1977
|
+
case "calendar_connection":
|
|
1978
|
+
return "/api/v1/calendar/connections";
|
|
1979
|
+
case "task_run":
|
|
1980
|
+
return "/api/v1/operator/context";
|
|
1981
|
+
case "questionnaire_run":
|
|
1982
|
+
return "/api/v1/psyche/questionnaire-runs/:id";
|
|
1983
|
+
case "preference_judgment":
|
|
1984
|
+
case "preference_signal":
|
|
1985
|
+
return "/api/v1/preferences/workspace";
|
|
1986
|
+
case "work_adjustment":
|
|
1987
|
+
return "/api/v1/operator/context";
|
|
1988
|
+
case "self_observation":
|
|
1989
|
+
return "/api/v1/psyche/self-observation/calendar";
|
|
1990
|
+
case "sleep_overview":
|
|
1991
|
+
return "/api/v1/health/sleep";
|
|
1992
|
+
case "sports_overview":
|
|
1993
|
+
return "/api/v1/health/fitness";
|
|
1994
|
+
default:
|
|
1995
|
+
return null;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
function enrichOnboardingEntityGuide(entry) {
|
|
1999
|
+
const classification = classifyOnboardingEntity(entry.entityType);
|
|
2000
|
+
return {
|
|
2001
|
+
...entry,
|
|
2002
|
+
classification,
|
|
2003
|
+
routeBase: classification === "batch_crud_entity"
|
|
2004
|
+
? AGENT_ONBOARDING_BATCH_ROUTE_BASES[entry.entityType]
|
|
2005
|
+
: null,
|
|
2006
|
+
preferredMutationPath: buildPreferredMutationPath(entry.entityType),
|
|
2007
|
+
preferredReadPath: buildPreferredReadPath(entry.entityType),
|
|
2008
|
+
preferredMutationTool: classification === "batch_crud_entity"
|
|
2009
|
+
? "forge_create_entities | forge_update_entities | forge_delete_entities | forge_search_entities"
|
|
2010
|
+
: null
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
2014
|
+
...AGENT_ONBOARDING_ENTITY_CATALOG_BASE.map(enrichOnboardingEntityGuide),
|
|
2015
|
+
enrichOnboardingEntityGuide({
|
|
2016
|
+
entityType: "tag",
|
|
2017
|
+
purpose: "A shared classification label used across Forge entities and notes.",
|
|
2018
|
+
minimumCreateFields: ["label"],
|
|
2019
|
+
relationshipRules: [
|
|
2020
|
+
"Tags are simple reusable labels, not a substitute for richer entity links.",
|
|
2021
|
+
"They use batch CRUD like other simple entities."
|
|
2022
|
+
],
|
|
2023
|
+
searchHints: ["Search by label before creating a near-duplicate tag."],
|
|
2024
|
+
examples: ['{"label":"Deep work","kind":"execution"}'],
|
|
2025
|
+
fieldGuide: [
|
|
2026
|
+
{
|
|
2027
|
+
name: "label",
|
|
2028
|
+
type: "string",
|
|
2029
|
+
required: true,
|
|
2030
|
+
description: "Human-readable tag label."
|
|
2031
|
+
},
|
|
2032
|
+
{
|
|
2033
|
+
name: "kind",
|
|
2034
|
+
type: "value|category|execution",
|
|
2035
|
+
required: false,
|
|
2036
|
+
description: "Optional tag family.",
|
|
2037
|
+
enumValues: ["value", "category", "execution"],
|
|
2038
|
+
defaultValue: "category"
|
|
2039
|
+
}
|
|
2040
|
+
]
|
|
2041
|
+
}),
|
|
2042
|
+
enrichOnboardingEntityGuide({
|
|
2043
|
+
entityType: "sleep_session",
|
|
2044
|
+
purpose: "A first-class health record for one night with timing, derived sleep scores, optional stage detail, and reflective links back into Forge.",
|
|
2045
|
+
minimumCreateFields: ["startedAt", "endedAt"],
|
|
2046
|
+
relationshipRules: [
|
|
2047
|
+
"Use batch CRUD for ordinary sleep_session create, update, delete, and search work.",
|
|
2048
|
+
"The direct PATCH route is still available when enriching an existing night with reflective notes after review.",
|
|
2049
|
+
"Sleep deletions are immediate and do not go through the settings bin."
|
|
2050
|
+
],
|
|
2051
|
+
searchHints: [
|
|
2052
|
+
"Search by linked entities or date window before creating a duplicate manual night."
|
|
2053
|
+
],
|
|
2054
|
+
examples: [
|
|
2055
|
+
'{"startedAt":"2026-04-10T22:45:00.000Z","endedAt":"2026-04-11T06:45:00.000Z","qualitySummary":"Slept cleanly after a light evening.","links":[{"entityType":"habit","entityId":"habit_sleep_hygiene","relationshipType":"supports"}]}'
|
|
2056
|
+
],
|
|
2057
|
+
fieldGuide: [
|
|
2058
|
+
{
|
|
2059
|
+
name: "startedAt",
|
|
2060
|
+
type: "ISO datetime",
|
|
2061
|
+
required: true,
|
|
2062
|
+
description: "Sleep start timestamp."
|
|
2063
|
+
},
|
|
2064
|
+
{
|
|
2065
|
+
name: "endedAt",
|
|
2066
|
+
type: "ISO datetime",
|
|
2067
|
+
required: true,
|
|
2068
|
+
description: "Sleep end timestamp."
|
|
2069
|
+
},
|
|
2070
|
+
{
|
|
2071
|
+
name: "timeInBedSeconds",
|
|
2072
|
+
type: "integer",
|
|
2073
|
+
required: false,
|
|
2074
|
+
description: "Defaults from startedAt and endedAt when omitted."
|
|
2075
|
+
},
|
|
2076
|
+
{
|
|
2077
|
+
name: "asleepSeconds",
|
|
2078
|
+
type: "integer",
|
|
2079
|
+
required: false,
|
|
2080
|
+
description: "Defaults to timeInBedSeconds when omitted."
|
|
2081
|
+
},
|
|
2082
|
+
{
|
|
2083
|
+
name: "awakeSeconds",
|
|
2084
|
+
type: "integer",
|
|
2085
|
+
required: false,
|
|
2086
|
+
description: "Defaults to the residual between timeInBedSeconds and asleepSeconds."
|
|
2087
|
+
},
|
|
2088
|
+
{
|
|
2089
|
+
name: "stageBreakdown",
|
|
2090
|
+
type: "array",
|
|
2091
|
+
required: false,
|
|
2092
|
+
description: "Optional list of { stage, seconds } items.",
|
|
2093
|
+
defaultValue: []
|
|
2094
|
+
},
|
|
2095
|
+
{
|
|
2096
|
+
name: "recoveryMetrics",
|
|
2097
|
+
type: "object",
|
|
2098
|
+
required: false,
|
|
2099
|
+
description: "Optional metric bag attached to the night.",
|
|
2100
|
+
defaultValue: {}
|
|
2101
|
+
},
|
|
2102
|
+
{
|
|
2103
|
+
name: "qualitySummary",
|
|
2104
|
+
type: "string",
|
|
2105
|
+
required: false,
|
|
2106
|
+
description: "Optional reflection summary.",
|
|
2107
|
+
defaultValue: ""
|
|
2108
|
+
},
|
|
2109
|
+
{
|
|
2110
|
+
name: "notes",
|
|
2111
|
+
type: "string",
|
|
2112
|
+
required: false,
|
|
2113
|
+
description: "Optional longer reflective note.",
|
|
2114
|
+
defaultValue: ""
|
|
2115
|
+
},
|
|
2116
|
+
{
|
|
2117
|
+
name: "tags",
|
|
2118
|
+
type: "string[]",
|
|
2119
|
+
required: false,
|
|
2120
|
+
description: "Optional review tags.",
|
|
2121
|
+
defaultValue: []
|
|
2122
|
+
},
|
|
2123
|
+
{
|
|
2124
|
+
name: "links",
|
|
2125
|
+
type: "array",
|
|
2126
|
+
required: false,
|
|
2127
|
+
description: "Linked Forge entities for context or support.",
|
|
2128
|
+
defaultValue: []
|
|
2129
|
+
}
|
|
2130
|
+
]
|
|
2131
|
+
}),
|
|
2132
|
+
enrichOnboardingEntityGuide({
|
|
2133
|
+
entityType: "workout_session",
|
|
2134
|
+
purpose: "A first-class sports record with workout type, timing, optional effort or biometric detail, and linked Forge context.",
|
|
2135
|
+
minimumCreateFields: ["workoutType", "startedAt", "endedAt"],
|
|
2136
|
+
relationshipRules: [
|
|
2137
|
+
"Use batch CRUD for ordinary workout_session create, update, delete, and search work.",
|
|
2138
|
+
"The direct PATCH route remains useful for reflective enrichment after reviewing an existing imported or habit-generated workout.",
|
|
2139
|
+
"Workout deletions are immediate and do not go through the settings bin."
|
|
2140
|
+
],
|
|
2141
|
+
searchHints: [
|
|
2142
|
+
"Search by workoutType, linked entity, or nearby timestamps before creating another manual workout."
|
|
2143
|
+
],
|
|
2144
|
+
examples: [
|
|
2145
|
+
'{"workoutType":"walk","startedAt":"2026-04-11T10:00:00.000Z","endedAt":"2026-04-11T10:45:00.000Z","subjectiveEffort":6,"meaningText":"Reset after a long planning block."}'
|
|
2146
|
+
],
|
|
2147
|
+
fieldGuide: [
|
|
2148
|
+
{
|
|
2149
|
+
name: "workoutType",
|
|
2150
|
+
type: "string",
|
|
2151
|
+
required: true,
|
|
2152
|
+
description: "Canonical workout label such as walk, run, ride, or mobility."
|
|
2153
|
+
},
|
|
2154
|
+
{
|
|
2155
|
+
name: "startedAt",
|
|
2156
|
+
type: "ISO datetime",
|
|
2157
|
+
required: true,
|
|
2158
|
+
description: "Workout start timestamp."
|
|
2159
|
+
},
|
|
2160
|
+
{
|
|
2161
|
+
name: "endedAt",
|
|
2162
|
+
type: "ISO datetime",
|
|
2163
|
+
required: true,
|
|
2164
|
+
description: "Workout end timestamp."
|
|
2165
|
+
},
|
|
2166
|
+
{
|
|
2167
|
+
name: "activeEnergyKcal",
|
|
2168
|
+
type: "number|null",
|
|
2169
|
+
required: false,
|
|
2170
|
+
description: "Optional active calories.",
|
|
2171
|
+
defaultValue: null,
|
|
2172
|
+
nullable: true
|
|
2173
|
+
},
|
|
2174
|
+
{
|
|
2175
|
+
name: "totalEnergyKcal",
|
|
2176
|
+
type: "number|null",
|
|
2177
|
+
required: false,
|
|
2178
|
+
description: "Optional total calories.",
|
|
2179
|
+
defaultValue: null,
|
|
2180
|
+
nullable: true
|
|
2181
|
+
},
|
|
2182
|
+
{
|
|
2183
|
+
name: "distanceMeters",
|
|
2184
|
+
type: "number|null",
|
|
2185
|
+
required: false,
|
|
2186
|
+
description: "Optional distance.",
|
|
2187
|
+
defaultValue: null,
|
|
2188
|
+
nullable: true
|
|
2189
|
+
},
|
|
2190
|
+
{
|
|
2191
|
+
name: "exerciseMinutes",
|
|
2192
|
+
type: "number|null",
|
|
2193
|
+
required: false,
|
|
2194
|
+
description: "Optional exercise minutes.",
|
|
2195
|
+
defaultValue: null,
|
|
2196
|
+
nullable: true
|
|
2197
|
+
},
|
|
2198
|
+
{
|
|
2199
|
+
name: "subjectiveEffort",
|
|
2200
|
+
type: "integer|null",
|
|
2201
|
+
required: false,
|
|
2202
|
+
description: "Optional subjective effort 1-10.",
|
|
2203
|
+
defaultValue: null,
|
|
2204
|
+
nullable: true
|
|
2205
|
+
},
|
|
2206
|
+
{
|
|
2207
|
+
name: "meaningText",
|
|
2208
|
+
type: "string",
|
|
2209
|
+
required: false,
|
|
2210
|
+
description: "Optional reflective meaning or context.",
|
|
2211
|
+
defaultValue: ""
|
|
2212
|
+
},
|
|
2213
|
+
{
|
|
2214
|
+
name: "tags",
|
|
2215
|
+
type: "string[]",
|
|
2216
|
+
required: false,
|
|
2217
|
+
description: "Optional workout tags.",
|
|
2218
|
+
defaultValue: []
|
|
2219
|
+
},
|
|
2220
|
+
{
|
|
2221
|
+
name: "links",
|
|
2222
|
+
type: "array",
|
|
2223
|
+
required: false,
|
|
2224
|
+
description: "Linked Forge entities for context or support.",
|
|
2225
|
+
defaultValue: []
|
|
2226
|
+
}
|
|
2227
|
+
]
|
|
2228
|
+
}),
|
|
2229
|
+
enrichOnboardingEntityGuide({
|
|
2230
|
+
entityType: "preference_catalog",
|
|
2231
|
+
purpose: "A reusable concept list inside one preference domain, used to seed or organize comparison candidates.",
|
|
2232
|
+
minimumCreateFields: ["userId", "domain", "title"],
|
|
2233
|
+
relationshipRules: [
|
|
2234
|
+
"Preference catalogs are simple entities and should default to batch CRUD.",
|
|
2235
|
+
"Catalog items belong to one preference_catalog through catalogId."
|
|
2236
|
+
],
|
|
2237
|
+
searchHints: [
|
|
2238
|
+
"Search by title and domain before creating another concept list."
|
|
2239
|
+
],
|
|
2240
|
+
examples: [
|
|
2241
|
+
'{"userId":"user_operator","domain":"food","title":"Cafe shortlist"}'
|
|
2242
|
+
],
|
|
2243
|
+
fieldGuide: [
|
|
2244
|
+
{
|
|
2245
|
+
name: "userId",
|
|
2246
|
+
type: "string",
|
|
2247
|
+
required: true,
|
|
2248
|
+
description: "Owner user id."
|
|
2249
|
+
},
|
|
2250
|
+
{
|
|
2251
|
+
name: "domain",
|
|
2252
|
+
type: "string",
|
|
2253
|
+
required: true,
|
|
2254
|
+
description: "Preference domain such as food, places, tools, or custom."
|
|
2255
|
+
},
|
|
2256
|
+
{
|
|
2257
|
+
name: "title",
|
|
2258
|
+
type: "string",
|
|
2259
|
+
required: true,
|
|
2260
|
+
description: "Catalog display title."
|
|
2261
|
+
},
|
|
2262
|
+
{
|
|
2263
|
+
name: "description",
|
|
2264
|
+
type: "string",
|
|
2265
|
+
required: false,
|
|
2266
|
+
description: "Optional catalog summary.",
|
|
2267
|
+
defaultValue: ""
|
|
2268
|
+
},
|
|
2269
|
+
{
|
|
2270
|
+
name: "slug",
|
|
2271
|
+
type: "string",
|
|
2272
|
+
required: false,
|
|
2273
|
+
description: "Optional stable slug.",
|
|
2274
|
+
defaultValue: ""
|
|
2275
|
+
}
|
|
2276
|
+
]
|
|
2277
|
+
}),
|
|
2278
|
+
enrichOnboardingEntityGuide({
|
|
2279
|
+
entityType: "preference_catalog_item",
|
|
2280
|
+
purpose: "One comparable candidate inside a preference catalog.",
|
|
2281
|
+
minimumCreateFields: ["catalogId", "label"],
|
|
2282
|
+
relationshipRules: [
|
|
2283
|
+
"Catalog items belong to a preference_catalog and use batch CRUD.",
|
|
2284
|
+
"They are concept seeds, not judgments or inferred scores."
|
|
2285
|
+
],
|
|
2286
|
+
searchHints: [
|
|
2287
|
+
"Search inside the catalog before creating another near-duplicate concept item."
|
|
2288
|
+
],
|
|
2289
|
+
examples: ['{"catalogId":"preference_catalog_123","label":"Flat white"}'],
|
|
2290
|
+
fieldGuide: [
|
|
2291
|
+
{
|
|
2292
|
+
name: "catalogId",
|
|
2293
|
+
type: "string",
|
|
2294
|
+
required: true,
|
|
2295
|
+
description: "Parent catalog id."
|
|
2296
|
+
},
|
|
2297
|
+
{
|
|
2298
|
+
name: "label",
|
|
2299
|
+
type: "string",
|
|
2300
|
+
required: true,
|
|
2301
|
+
description: "Candidate label."
|
|
2302
|
+
},
|
|
2303
|
+
{
|
|
2304
|
+
name: "description",
|
|
2305
|
+
type: "string",
|
|
2306
|
+
required: false,
|
|
2307
|
+
description: "Optional description.",
|
|
2308
|
+
defaultValue: ""
|
|
2309
|
+
},
|
|
2310
|
+
{
|
|
2311
|
+
name: "tags",
|
|
2312
|
+
type: "string[]",
|
|
2313
|
+
required: false,
|
|
2314
|
+
description: "Optional tags.",
|
|
2315
|
+
defaultValue: []
|
|
2316
|
+
},
|
|
2317
|
+
{
|
|
2318
|
+
name: "featureWeights",
|
|
2319
|
+
type: "object",
|
|
2320
|
+
required: false,
|
|
2321
|
+
description: "Optional interpretable feature weight hints.",
|
|
2322
|
+
defaultValue: {}
|
|
2323
|
+
}
|
|
2324
|
+
]
|
|
2325
|
+
}),
|
|
2326
|
+
enrichOnboardingEntityGuide({
|
|
2327
|
+
entityType: "preference_context",
|
|
2328
|
+
purpose: "A named preference mode such as Work, Personal, or Discovery under one user and domain.",
|
|
2329
|
+
minimumCreateFields: ["userId", "domain", "name"],
|
|
2330
|
+
relationshipRules: [
|
|
2331
|
+
"Preference contexts are simple entities and should default to batch CRUD.",
|
|
2332
|
+
"Use the merge action only when the operator explicitly wants context consolidation."
|
|
2333
|
+
],
|
|
2334
|
+
searchHints: ["Search by name and domain before creating another context."],
|
|
2335
|
+
examples: [
|
|
2336
|
+
'{"userId":"user_operator","domain":"food","name":"Work breakfasts","shareMode":"blended"}'
|
|
2337
|
+
],
|
|
2338
|
+
fieldGuide: [
|
|
2339
|
+
{
|
|
2340
|
+
name: "userId",
|
|
2341
|
+
type: "string",
|
|
2342
|
+
required: true,
|
|
2343
|
+
description: "Owner user id."
|
|
2344
|
+
},
|
|
2345
|
+
{
|
|
2346
|
+
name: "domain",
|
|
2347
|
+
type: "string",
|
|
2348
|
+
required: true,
|
|
2349
|
+
description: "Preference domain."
|
|
2350
|
+
},
|
|
2351
|
+
{
|
|
2352
|
+
name: "name",
|
|
2353
|
+
type: "string",
|
|
2354
|
+
required: true,
|
|
2355
|
+
description: "Context display name."
|
|
2356
|
+
},
|
|
2357
|
+
{
|
|
2358
|
+
name: "description",
|
|
2359
|
+
type: "string",
|
|
2360
|
+
required: false,
|
|
2361
|
+
description: "Optional summary.",
|
|
2362
|
+
defaultValue: ""
|
|
2363
|
+
},
|
|
2364
|
+
{
|
|
2365
|
+
name: "shareMode",
|
|
2366
|
+
type: "shared|isolated|blended",
|
|
2367
|
+
required: false,
|
|
2368
|
+
description: "How this context mixes evidence with others.",
|
|
2369
|
+
enumValues: ["shared", "isolated", "blended"],
|
|
2370
|
+
defaultValue: "blended"
|
|
2371
|
+
},
|
|
2372
|
+
{
|
|
2373
|
+
name: "active",
|
|
2374
|
+
type: "boolean",
|
|
2375
|
+
required: false,
|
|
2376
|
+
description: "Whether the context is active.",
|
|
2377
|
+
defaultValue: true
|
|
2378
|
+
}
|
|
2379
|
+
]
|
|
2380
|
+
}),
|
|
2381
|
+
enrichOnboardingEntityGuide({
|
|
2382
|
+
entityType: "preference_item",
|
|
2383
|
+
purpose: "One modeled preference candidate that may stand alone or point back to another Forge entity.",
|
|
2384
|
+
minimumCreateFields: ["userId", "domain", "label"],
|
|
2385
|
+
relationshipRules: [
|
|
2386
|
+
"Preference items are simple entities and should default to batch CRUD.",
|
|
2387
|
+
"They can optionally point back to another Forge entity through sourceEntityType and sourceEntityId."
|
|
2388
|
+
],
|
|
2389
|
+
searchHints: [
|
|
2390
|
+
"Search by label, domain, or linked source entity before creating another preference item."
|
|
2391
|
+
],
|
|
2392
|
+
examples: [
|
|
2393
|
+
'{"userId":"user_operator","domain":"tools","label":"Mechanical keyboard"}'
|
|
2394
|
+
],
|
|
2395
|
+
fieldGuide: [
|
|
2396
|
+
{
|
|
2397
|
+
name: "userId",
|
|
2398
|
+
type: "string",
|
|
2399
|
+
required: true,
|
|
2400
|
+
description: "Owner user id."
|
|
2401
|
+
},
|
|
2402
|
+
{
|
|
2403
|
+
name: "domain",
|
|
2404
|
+
type: "string",
|
|
2405
|
+
required: true,
|
|
2406
|
+
description: "Preference domain."
|
|
2407
|
+
},
|
|
2408
|
+
{
|
|
2409
|
+
name: "label",
|
|
2410
|
+
type: "string",
|
|
2411
|
+
required: true,
|
|
2412
|
+
description: "Item display label."
|
|
2413
|
+
},
|
|
2414
|
+
{
|
|
2415
|
+
name: "description",
|
|
2416
|
+
type: "string",
|
|
2417
|
+
required: false,
|
|
2418
|
+
description: "Optional description.",
|
|
2419
|
+
defaultValue: ""
|
|
2420
|
+
},
|
|
2421
|
+
{
|
|
2422
|
+
name: "sourceEntityType",
|
|
2423
|
+
type: "string|null",
|
|
2424
|
+
required: false,
|
|
2425
|
+
description: "Optional linked Forge entity type.",
|
|
2426
|
+
defaultValue: null,
|
|
2427
|
+
nullable: true
|
|
2428
|
+
},
|
|
2429
|
+
{
|
|
2430
|
+
name: "sourceEntityId",
|
|
2431
|
+
type: "string|null",
|
|
2432
|
+
required: false,
|
|
2433
|
+
description: "Optional linked Forge entity id.",
|
|
2434
|
+
defaultValue: null,
|
|
2435
|
+
nullable: true
|
|
2436
|
+
},
|
|
2437
|
+
{
|
|
2438
|
+
name: "tags",
|
|
2439
|
+
type: "string[]",
|
|
2440
|
+
required: false,
|
|
2441
|
+
description: "Optional tags.",
|
|
2442
|
+
defaultValue: []
|
|
2443
|
+
}
|
|
2444
|
+
]
|
|
2445
|
+
}),
|
|
2446
|
+
enrichOnboardingEntityGuide({
|
|
2447
|
+
entityType: "questionnaire_instrument",
|
|
2448
|
+
purpose: "A reusable Psyche questionnaire instrument with versions, scoring rules, and provenance.",
|
|
2449
|
+
minimumCreateFields: [
|
|
2450
|
+
"title",
|
|
2451
|
+
"sourceClass",
|
|
2452
|
+
"availability",
|
|
2453
|
+
"isSelfReport",
|
|
2454
|
+
"versionLabel",
|
|
2455
|
+
"definition",
|
|
2456
|
+
"scoring",
|
|
2457
|
+
"provenance"
|
|
2458
|
+
],
|
|
2459
|
+
relationshipRules: [
|
|
2460
|
+
"Questionnaire instruments now default to batch CRUD for normal create, update, delete, and search work.",
|
|
2461
|
+
"Clone, ensure draft, and publish remain specialized actions because they operate on instrument version state."
|
|
2462
|
+
],
|
|
2463
|
+
searchHints: [
|
|
2464
|
+
"Search by title or key before creating a new custom instrument."
|
|
2465
|
+
],
|
|
2466
|
+
examples: [
|
|
2467
|
+
'{"title":"Tiny weekly check-in","sourceClass":"secondary_verified","availability":"custom","isSelfReport":true,"versionLabel":"Draft 1","definition":{"locale":"en","instructions":"Rate how present this feels today.","completionNote":"","presentationMode":"single_question","responseStyle":"four_point_frequency","itemIds":[],"items":[],"sections":[],"pageSize":null},"scoring":{"scores":[]},"provenance":{"retrievalDate":"2026-04-06","sourceClass":"secondary_verified","scoringNotes":"","sources":[]}}'
|
|
2468
|
+
],
|
|
2469
|
+
fieldGuide: [
|
|
2470
|
+
{
|
|
2471
|
+
name: "title",
|
|
2472
|
+
type: "string",
|
|
2473
|
+
required: true,
|
|
2474
|
+
description: "Instrument title."
|
|
2475
|
+
},
|
|
2476
|
+
{
|
|
2477
|
+
name: "sourceClass",
|
|
2478
|
+
type: "string",
|
|
2479
|
+
required: true,
|
|
2480
|
+
description: "Evidence or provenance class."
|
|
2481
|
+
},
|
|
2482
|
+
{
|
|
2483
|
+
name: "availability",
|
|
2484
|
+
type: "string",
|
|
2485
|
+
required: true,
|
|
2486
|
+
description: "System or custom availability mode."
|
|
2487
|
+
},
|
|
2488
|
+
{
|
|
2489
|
+
name: "isSelfReport",
|
|
2490
|
+
type: "boolean",
|
|
2491
|
+
required: true,
|
|
2492
|
+
description: "Whether the instrument is self-report."
|
|
2493
|
+
},
|
|
2494
|
+
{
|
|
2495
|
+
name: "versionLabel",
|
|
2496
|
+
type: "string",
|
|
2497
|
+
required: true,
|
|
2498
|
+
description: "Initial draft version label on create."
|
|
2499
|
+
},
|
|
2500
|
+
{
|
|
2501
|
+
name: "definition",
|
|
2502
|
+
type: "object",
|
|
2503
|
+
required: true,
|
|
2504
|
+
description: "Questionnaire definition payload."
|
|
2505
|
+
},
|
|
2506
|
+
{
|
|
2507
|
+
name: "scoring",
|
|
2508
|
+
type: "object",
|
|
2509
|
+
required: true,
|
|
2510
|
+
description: "Scoring payload."
|
|
2511
|
+
},
|
|
2512
|
+
{
|
|
2513
|
+
name: "provenance",
|
|
2514
|
+
type: "object",
|
|
2515
|
+
required: true,
|
|
2516
|
+
description: "Provenance payload."
|
|
2517
|
+
}
|
|
2518
|
+
]
|
|
2519
|
+
}),
|
|
2520
|
+
enrichOnboardingEntityGuide({
|
|
2521
|
+
entityType: "task_run",
|
|
2522
|
+
purpose: "A live timed work session attached to a task.",
|
|
2523
|
+
minimumCreateFields: [],
|
|
2524
|
+
relationshipRules: [
|
|
2525
|
+
"Task runs are action-heavy records. Do not model them as ordinary CRUD entities.",
|
|
2526
|
+
"Start, focus, heartbeat, complete, or release them through the dedicated task-run routes."
|
|
2527
|
+
],
|
|
2528
|
+
searchHints: [
|
|
2529
|
+
"Read operator context before starting or altering live work."
|
|
2530
|
+
],
|
|
2531
|
+
fieldGuide: []
|
|
2532
|
+
}),
|
|
2533
|
+
enrichOnboardingEntityGuide({
|
|
2534
|
+
entityType: "work_adjustment",
|
|
2535
|
+
purpose: "A truthful signed minute correction on an existing task or project when work happened outside a live run.",
|
|
2536
|
+
minimumCreateFields: [],
|
|
2537
|
+
relationshipRules: [
|
|
2538
|
+
"Work adjustments are action-heavy corrections, not normal CRUD entities.",
|
|
2539
|
+
"Use forge_adjust_work_minutes when the target task or project already exists and only tracked minutes need to change."
|
|
2540
|
+
],
|
|
2541
|
+
searchHints: [
|
|
2542
|
+
"Confirm the target task or project first, then apply only the signed minute delta that is actually true."
|
|
2543
|
+
],
|
|
2544
|
+
fieldGuide: []
|
|
2545
|
+
}),
|
|
2546
|
+
enrichOnboardingEntityGuide({
|
|
2547
|
+
entityType: "questionnaire_run",
|
|
2548
|
+
purpose: "One user-owned answer session against a questionnaire instrument version.",
|
|
2549
|
+
minimumCreateFields: [],
|
|
2550
|
+
relationshipRules: [
|
|
2551
|
+
"Questionnaire runs are action-heavy records with a lifecycle of start, patch answers, and complete.",
|
|
2552
|
+
"Use the run routes instead of batch CRUD."
|
|
2553
|
+
],
|
|
2554
|
+
searchHints: [
|
|
2555
|
+
"Read the run detail when continuing or reviewing an in-flight answer session."
|
|
2556
|
+
],
|
|
2557
|
+
fieldGuide: []
|
|
2558
|
+
}),
|
|
2559
|
+
enrichOnboardingEntityGuide({
|
|
2560
|
+
entityType: "preference_judgment",
|
|
2561
|
+
purpose: "One pairwise preference outcome between two items inside a domain and context.",
|
|
2562
|
+
minimumCreateFields: [],
|
|
2563
|
+
relationshipRules: [
|
|
2564
|
+
"Preference judgments are action-heavy records, not batch CRUD entities.",
|
|
2565
|
+
"Use the dedicated judgment route so the profile, evidence, and comparison history stay aligned."
|
|
2566
|
+
],
|
|
2567
|
+
searchHints: [
|
|
2568
|
+
"Confirm the left and right items, the outcome, and the active context before storing a new judgment."
|
|
2569
|
+
],
|
|
2570
|
+
fieldGuide: []
|
|
2571
|
+
}),
|
|
2572
|
+
enrichOnboardingEntityGuide({
|
|
2573
|
+
entityType: "preference_signal",
|
|
2574
|
+
purpose: "One direct preference signal such as favorite, veto, bookmark, neutral, or compare-later.",
|
|
2575
|
+
minimumCreateFields: [],
|
|
2576
|
+
relationshipRules: [
|
|
2577
|
+
"Preference signals are action-heavy records, not batch CRUD entities.",
|
|
2578
|
+
"Use the dedicated signal route so the profile and evidence model stay aligned."
|
|
2579
|
+
],
|
|
2580
|
+
searchHints: [
|
|
2581
|
+
"Confirm the item, signal type, and context before storing a new direct signal."
|
|
2582
|
+
],
|
|
2583
|
+
fieldGuide: []
|
|
2584
|
+
}),
|
|
2585
|
+
enrichOnboardingEntityGuide({
|
|
2586
|
+
entityType: "calendar_connection",
|
|
2587
|
+
purpose: "A stored external calendar provider connection and its selected calendars.",
|
|
2588
|
+
minimumCreateFields: [],
|
|
2589
|
+
relationshipRules: [
|
|
2590
|
+
"Calendar connections use specialized setup and sync flows rather than batch CRUD.",
|
|
2591
|
+
"Provider auth and writable Forge-calendar selection are part of the same specialized surface."
|
|
2592
|
+
],
|
|
2593
|
+
searchHints: [
|
|
2594
|
+
"Read the calendar overview before changing connections or sync state."
|
|
2595
|
+
],
|
|
2596
|
+
fieldGuide: []
|
|
2597
|
+
}),
|
|
2598
|
+
enrichOnboardingEntityGuide({
|
|
2599
|
+
entityType: "wiki_page",
|
|
2600
|
+
purpose: "A file-backed Forge wiki page or evidence page.",
|
|
2601
|
+
minimumCreateFields: ["title", "contentMarkdown"],
|
|
2602
|
+
relationshipRules: [
|
|
2603
|
+
"Wiki pages live on the wiki surface and use specialized page upsert routes rather than batch CRUD.",
|
|
2604
|
+
"Entity links remain explicit inside the page link model."
|
|
2605
|
+
],
|
|
2606
|
+
searchHints: [
|
|
2607
|
+
"Search or list wiki pages before creating another page with the same topic."
|
|
2608
|
+
],
|
|
2609
|
+
fieldGuide: [
|
|
2610
|
+
{
|
|
2611
|
+
name: "title",
|
|
2612
|
+
type: "string",
|
|
2613
|
+
required: true,
|
|
2614
|
+
description: "Page title."
|
|
1880
2615
|
},
|
|
1881
2616
|
{
|
|
1882
|
-
name: "
|
|
1883
|
-
type: "string
|
|
1884
|
-
required:
|
|
1885
|
-
description: "
|
|
1886
|
-
defaultValue: []
|
|
2617
|
+
name: "contentMarkdown",
|
|
2618
|
+
type: "string",
|
|
2619
|
+
required: true,
|
|
2620
|
+
description: "Markdown body."
|
|
1887
2621
|
}
|
|
1888
2622
|
]
|
|
1889
|
-
}
|
|
2623
|
+
}),
|
|
2624
|
+
enrichOnboardingEntityGuide({
|
|
2625
|
+
entityType: "movement",
|
|
2626
|
+
purpose: "The specialized Movement surface for day, month, all-time, timeline, trip, place, selection, and manual overlay work.",
|
|
2627
|
+
minimumCreateFields: [],
|
|
2628
|
+
relationshipRules: [
|
|
2629
|
+
"Movement is a specialized domain surface, not a normal batch CRUD entity family.",
|
|
2630
|
+
"Read and mutate it through the dedicated movement routes published under specializedDomainSurfaces."
|
|
2631
|
+
],
|
|
2632
|
+
searchHints: [
|
|
2633
|
+
"Clarify whether the user wants a behavioral query, one trip or place, a missing-gap overlay, a manual add or update, or a link before choosing the route."
|
|
2634
|
+
],
|
|
2635
|
+
fieldGuide: []
|
|
2636
|
+
}),
|
|
2637
|
+
enrichOnboardingEntityGuide({
|
|
2638
|
+
entityType: "life_force",
|
|
2639
|
+
purpose: "The specialized Life Force surface for the current energy overview, profile edits, weekday templates, and fatigue signals.",
|
|
2640
|
+
minimumCreateFields: [],
|
|
2641
|
+
relationshipRules: [
|
|
2642
|
+
"Life Force is a specialized domain surface, not a normal batch CRUD entity family.",
|
|
2643
|
+
"Use the dedicated overview, profile, weekday-template, and fatigue-signal routes."
|
|
2644
|
+
],
|
|
2645
|
+
searchHints: [
|
|
2646
|
+
"Clarify whether the user wants explanation, durable model changes, or a real-time tired or recovered signal before choosing the route."
|
|
2647
|
+
],
|
|
2648
|
+
fieldGuide: []
|
|
2649
|
+
}),
|
|
2650
|
+
enrichOnboardingEntityGuide({
|
|
2651
|
+
entityType: "workbench",
|
|
2652
|
+
purpose: "The specialized Workbench surface for flow catalog work, flow CRUD, execution, run history, published outputs, node results, and latest-node-output reads.",
|
|
2653
|
+
minimumCreateFields: [],
|
|
2654
|
+
relationshipRules: [
|
|
2655
|
+
"Workbench is a specialized execution surface, not a normal batch CRUD entity family.",
|
|
2656
|
+
"Use the dedicated workbench flow, run, output, and node-result routes."
|
|
2657
|
+
],
|
|
2658
|
+
searchHints: [
|
|
2659
|
+
"Clarify whether the user wants flow discovery, editing, execution, published output, run inspection, or node-level output before choosing the route."
|
|
2660
|
+
],
|
|
2661
|
+
fieldGuide: []
|
|
2662
|
+
}),
|
|
2663
|
+
enrichOnboardingEntityGuide({
|
|
2664
|
+
entityType: "self_observation",
|
|
2665
|
+
purpose: "The note-backed Psyche self-observation calendar surface for observed events and reflections.",
|
|
2666
|
+
minimumCreateFields: [],
|
|
2667
|
+
relationshipRules: [
|
|
2668
|
+
"This is a read model, not a standalone CRUD entity.",
|
|
2669
|
+
"Mutate it by creating or updating a note with frontmatter.observedAt."
|
|
2670
|
+
],
|
|
2671
|
+
searchHints: [
|
|
2672
|
+
"Read the self-observation calendar before proposing new reflected notes or edits."
|
|
2673
|
+
],
|
|
2674
|
+
fieldGuide: []
|
|
2675
|
+
}),
|
|
2676
|
+
enrichOnboardingEntityGuide({
|
|
2677
|
+
entityType: "sleep_overview",
|
|
2678
|
+
purpose: "The read-model sleep workspace that summarizes recent sleep sessions, trends, and stage averages.",
|
|
2679
|
+
minimumCreateFields: [],
|
|
2680
|
+
relationshipRules: [
|
|
2681
|
+
"Use this surface for review.",
|
|
2682
|
+
"Create, update, delete, or search the underlying sleep_session records through batch CRUD by default."
|
|
2683
|
+
],
|
|
2684
|
+
searchHints: [
|
|
2685
|
+
"Read this surface before suggesting reflective edits or health-planning follow-up."
|
|
2686
|
+
],
|
|
2687
|
+
fieldGuide: []
|
|
2688
|
+
}),
|
|
2689
|
+
enrichOnboardingEntityGuide({
|
|
2690
|
+
entityType: "sports_overview",
|
|
2691
|
+
purpose: "The read-model sports workspace that summarizes recent workout sessions and training load.",
|
|
2692
|
+
minimumCreateFields: [],
|
|
2693
|
+
relationshipRules: [
|
|
2694
|
+
"Use this surface for review.",
|
|
2695
|
+
"Create, update, delete, or search the underlying workout_session records through batch CRUD by default."
|
|
2696
|
+
],
|
|
2697
|
+
searchHints: [
|
|
2698
|
+
"Read this surface before suggesting workout reflections or recovery follow-up."
|
|
2699
|
+
],
|
|
2700
|
+
fieldGuide: []
|
|
2701
|
+
})
|
|
1890
2702
|
];
|
|
1891
2703
|
const AGENT_ONBOARDING_CONVERSATION_RULES = [
|
|
1892
2704
|
"Ask only for what is missing or unclear instead of walking the user through every optional field.",
|
|
2705
|
+
"Start by saying what seems to matter here or what the record is becoming, then ask the next useful question.",
|
|
2706
|
+
"Whenever possible, make the direction of the intake visible before the question by naming what you think the user is trying to preserve, clarify, decide, schedule, or make easier.",
|
|
2707
|
+
"Before each question, decide the one missing thing you are trying to clarify and why it matters for the record.",
|
|
1893
2708
|
"Use a progression of concrete example or intent, working name, purpose or meaning, placement in Forge, operational details, and linked context.",
|
|
1894
2709
|
"Ask one to three focused questions at a time. One is usually best when the user is uncertain or emotionally loaded.",
|
|
2710
|
+
"One focused question is the default. Only stack a second question when both serve the same clarification job and the user is steady enough for it.",
|
|
1895
2711
|
"If the user already answered the normal opening question, do not repeat it. Move to the next missing clarification.",
|
|
1896
2712
|
"Do not over-therapize logistical entities. For tasks, calendar events, work blocks, timeboxes, and task runs, one brief confirming sentence plus one question is usually enough.",
|
|
2713
|
+
"After each substantive answer, briefly say what is becoming clearer and ask only for the next thing that still changes the record shape or usefulness.",
|
|
2714
|
+
"For strategic, reflective, or emotionally meaningful non-Psyche records, ask what feels important to keep true before you ask for labels, dates, or taxonomy.",
|
|
2715
|
+
"For reusable records such as tags, event types, emotion definitions, preference contexts, or questionnaires, ask what distinction or decision the record should help with before you ask for wording.",
|
|
2716
|
+
"When useful, help the user name, define, and connect the record in that order: offer a working label, clarify what belongs inside it, then ask about links only after the record itself feels steady.",
|
|
2717
|
+
"When the meaning is clearer than the wording, offer a tentative title or formulation yourself and invite correction instead of forcing the user to wordsmith alone.",
|
|
1897
2718
|
"Before saving, briefly summarize the working formulation in the user's own language when that would reduce ambiguity.",
|
|
1898
|
-
"
|
|
2719
|
+
"Once the record is clear enough to name, stop exploring broadly and ask only for the last structural detail that still matters.",
|
|
2720
|
+
"If the record is already clear enough to save, save it instead of performing a ceremonial extra question.",
|
|
2721
|
+
"If the user accepts the wording or record shape, move to the write instead of reopening the intake.",
|
|
2722
|
+
"When updating an entity, start with what is changing, what should stay true, and what prompted the update now.",
|
|
2723
|
+
"For action-heavy flows such as work adjustments, preference judgments, preference signals, and specialized Movement, Life Force, or Workbench work, first ask what the user is trying to understand, change, add, update, link, or run, then choose the dedicated action or surface route instead of forcing the request into generic CRUD.",
|
|
2724
|
+
"For Movement specifically, treat missing-data corrections as user-defined overlay boxes unless the user is editing an already-recorded stay or trip. When the user already gave a clear instruction like 'that missing block was home', act after only the last ambiguity is resolved."
|
|
1899
2725
|
];
|
|
1900
2726
|
const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
1901
2727
|
{
|
|
1902
2728
|
focus: "goal",
|
|
1903
|
-
openingQuestion: "What direction
|
|
2729
|
+
openingQuestion: "What direction are you trying to keep hold of here?",
|
|
1904
2730
|
coachingGoal: "Clarify the direction and why it matters, not just produce a title.",
|
|
1905
2731
|
askSequence: [
|
|
1906
2732
|
"Ask what direction or outcome the user wants to keep in view.",
|
|
2733
|
+
"Reflect the deeper stake in plain language before moving on.",
|
|
1907
2734
|
"Ask why it matters now.",
|
|
1908
2735
|
"Distinguish the goal from a project or task.",
|
|
1909
2736
|
"Clarify horizon and status only after the meaning is clear."
|
|
@@ -1911,11 +2738,13 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
|
1911
2738
|
},
|
|
1912
2739
|
{
|
|
1913
2740
|
focus: "project",
|
|
1914
|
-
openingQuestion: "If this became a real project, what would you be trying to make true?",
|
|
2741
|
+
openingQuestion: "If this became a real project, what would you be trying to make true in your life or work?",
|
|
1915
2742
|
coachingGoal: "Turn an intention into a bounded workstream with a clear outcome.",
|
|
1916
2743
|
askSequence: [
|
|
1917
2744
|
"Ask what this piece of work is trying to make true.",
|
|
2745
|
+
"Reflect the emerging boundary so the user can hear what is in scope.",
|
|
1918
2746
|
"Ask what outcome would make the project feel real or complete for now.",
|
|
2747
|
+
"Ask what belongs inside the boundary and what can stay out if the scope still feels muddy.",
|
|
1919
2748
|
"Ask which goal it belongs under.",
|
|
1920
2749
|
"Land on a working name once the scope is clear.",
|
|
1921
2750
|
"Clarify status, owner, and notes only after the scope is clear."
|
|
@@ -1923,10 +2752,11 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
|
1923
2752
|
},
|
|
1924
2753
|
{
|
|
1925
2754
|
focus: "strategy",
|
|
1926
|
-
openingQuestion: "What future state
|
|
2755
|
+
openingQuestion: "What future state are you actually trying to arrive at with this strategy?",
|
|
1927
2756
|
coachingGoal: "Turn a vague plan into a deliberate sequence toward a real end state.",
|
|
1928
2757
|
askSequence: [
|
|
1929
2758
|
"Ask what end state the strategy is trying to land.",
|
|
2759
|
+
"Reflect the destination in plain language so the user can correct it early.",
|
|
1930
2760
|
"Ask which goals or projects are the true targets.",
|
|
1931
2761
|
"Ask what the major steps or nodes are.",
|
|
1932
2762
|
"Ask about order, dependencies, and anything that must not be skipped."
|
|
@@ -1934,7 +2764,7 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
|
1934
2764
|
},
|
|
1935
2765
|
{
|
|
1936
2766
|
focus: "task",
|
|
1937
|
-
openingQuestion: "What is the next concrete move
|
|
2767
|
+
openingQuestion: "What is the next concrete move here?",
|
|
1938
2768
|
coachingGoal: "Identify the next concrete move, not just capture a vague obligation.",
|
|
1939
2769
|
askSequence: [
|
|
1940
2770
|
"Ask what the next concrete action is.",
|
|
@@ -1944,29 +2774,53 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
|
1944
2774
|
},
|
|
1945
2775
|
{
|
|
1946
2776
|
focus: "habit",
|
|
1947
|
-
openingQuestion: "What recurring move are you trying to strengthen or
|
|
2777
|
+
openingQuestion: "What recurring move are you trying to strengthen or interrupt?",
|
|
1948
2778
|
coachingGoal: "Define the recurring behavior and cadence clearly enough for honest later check-ins.",
|
|
1949
2779
|
askSequence: [
|
|
1950
2780
|
"Ask what the recurring behavior is in plain language.",
|
|
1951
2781
|
"Ask whether doing it is aligned or a slip.",
|
|
2782
|
+
"Ask what an honest hit or miss would look like in an ordinary week.",
|
|
1952
2783
|
"Ask about cadence and what counts as an honest check-in in practice.",
|
|
1953
2784
|
"Ask about links only if they will help later review."
|
|
1954
2785
|
]
|
|
1955
2786
|
},
|
|
2787
|
+
{
|
|
2788
|
+
focus: "tag",
|
|
2789
|
+
openingQuestion: "What do you want this tag to help you notice or find again later?",
|
|
2790
|
+
coachingGoal: "Create a label that helps later retrieval or grouping instead of another vague bucket.",
|
|
2791
|
+
askSequence: [
|
|
2792
|
+
"Ask what the tag should help the user notice, group, or find later.",
|
|
2793
|
+
"Ask what kinds of records should belong under it and what should stay outside it.",
|
|
2794
|
+
"Offer a concise label if the grouping meaning is clearer than the wording.",
|
|
2795
|
+
"Ask about color, kind, or parent grouping only if that changes how the tag will be used."
|
|
2796
|
+
]
|
|
2797
|
+
},
|
|
1956
2798
|
{
|
|
1957
2799
|
focus: "note",
|
|
1958
|
-
openingQuestion: "What feels
|
|
2800
|
+
openingQuestion: "What about this feels worth preserving in a note?",
|
|
1959
2801
|
coachingGoal: "Preserve the useful context and link it to the right places without turning the note into a dump.",
|
|
1960
2802
|
askSequence: [
|
|
1961
2803
|
"Ask what the note needs to preserve.",
|
|
2804
|
+
"Ask what sentence future-you would need to recover from this note later.",
|
|
1962
2805
|
"Ask what entities it should stay attached to.",
|
|
1963
2806
|
"Ask whether it should be durable or temporary.",
|
|
1964
2807
|
"Ask about tags or author only if they help retrieval or handoff."
|
|
1965
2808
|
]
|
|
1966
2809
|
},
|
|
2810
|
+
{
|
|
2811
|
+
focus: "wiki_page",
|
|
2812
|
+
openingQuestion: "What should this page become the main reference for?",
|
|
2813
|
+
coachingGoal: "Create a durable reference page with a clear scope instead of dumping raw notes into the wiki.",
|
|
2814
|
+
askSequence: [
|
|
2815
|
+
"Ask what topic this page should become the canonical place for.",
|
|
2816
|
+
"Ask whether it is a durable wiki page or supporting evidence.",
|
|
2817
|
+
"Ask what future lookup, decision, or collaboration this page should support.",
|
|
2818
|
+
"Ask about linked entities, aliases, or tags only if they will make the page more navigable later."
|
|
2819
|
+
]
|
|
2820
|
+
},
|
|
1967
2821
|
{
|
|
1968
2822
|
focus: "insight",
|
|
1969
|
-
openingQuestion: "What
|
|
2823
|
+
openingQuestion: "What is the clearest thing you want future-you or the agent to remember from this?",
|
|
1970
2824
|
coachingGoal: "Capture one grounded observation or recommendation clearly enough that it remains useful later.",
|
|
1971
2825
|
askSequence: [
|
|
1972
2826
|
"Ask what pattern, tension, or observation should be remembered.",
|
|
@@ -2006,23 +2860,218 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
|
|
|
2006
2860
|
"Ask about source or override reason only when that context matters."
|
|
2007
2861
|
]
|
|
2008
2862
|
},
|
|
2863
|
+
{
|
|
2864
|
+
focus: "calendar_connection",
|
|
2865
|
+
openingQuestion: "Which calendar provider are you trying to connect, and what do you want Forge to do with it?",
|
|
2866
|
+
coachingGoal: "Connect the right provider deliberately without turning setup into a credential dump.",
|
|
2867
|
+
askSequence: [
|
|
2868
|
+
"Ask which provider the user wants to connect and what they want Forge to do with it.",
|
|
2869
|
+
"Ask whether the goal is read-only visibility, writable planning, or both.",
|
|
2870
|
+
"Ask only for the next provider-specific step that still matters, such as auth flow, label, or calendar selection.",
|
|
2871
|
+
"Move into the actual connection flow once the setup goal is clear."
|
|
2872
|
+
]
|
|
2873
|
+
},
|
|
2874
|
+
{
|
|
2875
|
+
focus: "task_run",
|
|
2876
|
+
openingQuestion: "Which task should I start?",
|
|
2877
|
+
coachingGoal: "Start truthful live work with as little friction as possible while still knowing what is being worked on and by whom.",
|
|
2878
|
+
askSequence: [
|
|
2879
|
+
"Confirm the task.",
|
|
2880
|
+
"Confirm the actor only if it is not already obvious.",
|
|
2881
|
+
"Ask whether the run should be planned or unlimited only if that changes the action.",
|
|
2882
|
+
"Start the run instead of turning it into a longer intake."
|
|
2883
|
+
]
|
|
2884
|
+
},
|
|
2885
|
+
{
|
|
2886
|
+
focus: "work_adjustment",
|
|
2887
|
+
openingQuestion: "Which task or project should this time correction belong to?",
|
|
2888
|
+
coachingGoal: "Correct tracked minutes truthfully without pretending a live run happened.",
|
|
2889
|
+
askSequence: [
|
|
2890
|
+
"Ask what existing task or project the minutes belong to.",
|
|
2891
|
+
"Ask whether time should be added or removed.",
|
|
2892
|
+
"Ask what real work or correction the adjustment is meant to capture.",
|
|
2893
|
+
"Ask for a short audit note only if the reason would otherwise be unclear later."
|
|
2894
|
+
]
|
|
2895
|
+
},
|
|
2896
|
+
{
|
|
2897
|
+
focus: "self_observation",
|
|
2898
|
+
openingQuestion: "What did you notice most clearly in that moment?",
|
|
2899
|
+
coachingGoal: "Capture one observation clearly enough that it can support later reflection without pretending it is already a full interpretation.",
|
|
2900
|
+
askSequence: [
|
|
2901
|
+
"Ask what was observed.",
|
|
2902
|
+
"Reflect the moment without pretending it is already a finished interpretation.",
|
|
2903
|
+
"Ask for the smallest concrete slice if the observation still feels vague or global.",
|
|
2904
|
+
"Ask when it happened or became noticeable unless timing is already clear.",
|
|
2905
|
+
"Ask what it may connect to: pattern, belief, value, mode, task, project, or note.",
|
|
2906
|
+
"Ask for tags or extra context only if that will help later review."
|
|
2907
|
+
]
|
|
2908
|
+
},
|
|
2909
|
+
{
|
|
2910
|
+
focus: "sleep_session",
|
|
2911
|
+
openingQuestion: "What about this night feels important enough to remember or connect?",
|
|
2912
|
+
coachingGoal: "Enrich one night's record with reflective context instead of treating it like a generic note.",
|
|
2913
|
+
askSequence: [
|
|
2914
|
+
"Ask what about the night feels worth capturing.",
|
|
2915
|
+
"Ask whether the main point is quality, pattern, context, meaning, or links.",
|
|
2916
|
+
"Ask what goal, project, task, habit, or Psyche record it should stay connected to.",
|
|
2917
|
+
"Ask about tags only if they will help later review."
|
|
2918
|
+
]
|
|
2919
|
+
},
|
|
2920
|
+
{
|
|
2921
|
+
focus: "workout_session",
|
|
2922
|
+
openingQuestion: "What about this workout feels most worth remembering or connecting?",
|
|
2923
|
+
coachingGoal: "Enrich one workout with subjective effort, mood, meaning, or linked context.",
|
|
2924
|
+
askSequence: [
|
|
2925
|
+
"Ask what about the session the user wants to preserve.",
|
|
2926
|
+
"Ask whether the key layer is effort, mood, meaning, social context, or links.",
|
|
2927
|
+
"Ask what it connects to in Forge if links matter.",
|
|
2928
|
+
"Ask about tags only if they help later retrieval."
|
|
2929
|
+
]
|
|
2930
|
+
},
|
|
2931
|
+
{
|
|
2932
|
+
focus: "preference_catalog",
|
|
2933
|
+
openingQuestion: "What decision or taste question should this catalog help with?",
|
|
2934
|
+
coachingGoal: "Define a useful comparison pool rather than a list with no decision purpose.",
|
|
2935
|
+
askSequence: [
|
|
2936
|
+
"Ask what preference question this catalog is meant to support.",
|
|
2937
|
+
"Ask what domain or concept area it belongs to.",
|
|
2938
|
+
"Ask what kinds of items should be included or excluded.",
|
|
2939
|
+
"Offer a working catalog name once the purpose is clear."
|
|
2940
|
+
]
|
|
2941
|
+
},
|
|
2942
|
+
{
|
|
2943
|
+
focus: "preference_catalog_item",
|
|
2944
|
+
openingQuestion: "What makes this option meaningfully worth comparing?",
|
|
2945
|
+
coachingGoal: "Add one candidate in a way that will make later comparisons feel clear and fair.",
|
|
2946
|
+
askSequence: [
|
|
2947
|
+
"Ask what makes this item worth including in the catalog.",
|
|
2948
|
+
"Ask what catalog or domain it belongs to if that is still unclear.",
|
|
2949
|
+
"Ask what would make the comparison confusing or unfair if the label stayed as-is.",
|
|
2950
|
+
"Ask for a short clarifying description only if the label would be ambiguous later.",
|
|
2951
|
+
"Ask about aliases or tags only if they help retrieval."
|
|
2952
|
+
]
|
|
2953
|
+
},
|
|
2954
|
+
{
|
|
2955
|
+
focus: "preference_context",
|
|
2956
|
+
openingQuestion: "In what situation should Forge treat your preferences differently here?",
|
|
2957
|
+
coachingGoal: "Define a real operating mode for preferences instead of a decorative label.",
|
|
2958
|
+
askSequence: [
|
|
2959
|
+
"Ask what situation or mode this context is meant to represent.",
|
|
2960
|
+
"Ask what decisions or comparisons should feel different inside that context.",
|
|
2961
|
+
"Ask what should count inside that context and what should stay outside it.",
|
|
2962
|
+
"Ask whether it should be active, default, or kept separate from other evidence.",
|
|
2963
|
+
"Offer a concise name if the mode is clearer than the wording."
|
|
2964
|
+
]
|
|
2965
|
+
},
|
|
2966
|
+
{
|
|
2967
|
+
focus: "preference_item",
|
|
2968
|
+
openingQuestion: "What preference are you trying to make clearer by saving this item?",
|
|
2969
|
+
coachingGoal: "Save one concrete preference candidate or signal without losing the context that makes it meaningful.",
|
|
2970
|
+
askSequence: [
|
|
2971
|
+
"Ask what preference or taste question this item belongs to.",
|
|
2972
|
+
"Ask what domain or context it should live in.",
|
|
2973
|
+
"Ask whether the user is saving a comparison candidate or a direct signal such as favorite, veto, or compare-later.",
|
|
2974
|
+
"Ask what makes the item distinct enough to compare usefully only if it is still a comparison candidate."
|
|
2975
|
+
]
|
|
2976
|
+
},
|
|
2977
|
+
{
|
|
2978
|
+
focus: "preference_judgment",
|
|
2979
|
+
openingQuestion: "What comparison are you actually trying to settle here?",
|
|
2980
|
+
coachingGoal: "Capture one pairwise preference decision with the right context instead of only logging a left-versus-right click.",
|
|
2981
|
+
askSequence: [
|
|
2982
|
+
"Ask what comparison the user is actually trying to settle.",
|
|
2983
|
+
"Ask which context or domain this judgment belongs to.",
|
|
2984
|
+
"Ask whether the result is left, right, tie, or skip.",
|
|
2985
|
+
"Ask for reason tags or strength only if they will improve later interpretation."
|
|
2986
|
+
]
|
|
2987
|
+
},
|
|
2988
|
+
{
|
|
2989
|
+
focus: "preference_signal",
|
|
2990
|
+
openingQuestion: "What do you want Forge to remember about this item right now?",
|
|
2991
|
+
coachingGoal: "Store a direct preference signal such as favorite, veto, bookmark, or compare-later with enough context to interpret it later.",
|
|
2992
|
+
askSequence: [
|
|
2993
|
+
"Ask what item the user wants to mark.",
|
|
2994
|
+
"Ask what signal they want to give it.",
|
|
2995
|
+
"Ask what domain or context this belongs to if that is still unclear.",
|
|
2996
|
+
"Ask about strength only if the user is expressing a gradient rather than a simple mark."
|
|
2997
|
+
]
|
|
2998
|
+
},
|
|
2999
|
+
{
|
|
3000
|
+
focus: "questionnaire_instrument",
|
|
3001
|
+
openingQuestion: "What would this questionnaire help someone notice or track?",
|
|
3002
|
+
coachingGoal: "Clarify whether the user is authoring a reusable questionnaire and what the instrument is for.",
|
|
3003
|
+
askSequence: [
|
|
3004
|
+
"Ask what the questionnaire is meant to measure or surface.",
|
|
3005
|
+
"Ask who it is for and when it should be used.",
|
|
3006
|
+
"Ask what kind of honest moment or decision it should help someone answer before getting into item wording.",
|
|
3007
|
+
"Reflect the practical use case back in plain language.",
|
|
3008
|
+
"Move to draft creation once the purpose is clear."
|
|
3009
|
+
]
|
|
3010
|
+
},
|
|
3011
|
+
{
|
|
3012
|
+
focus: "questionnaire_run",
|
|
3013
|
+
openingQuestion: "Do you want to start, continue, review, or finish a questionnaire run?",
|
|
3014
|
+
coachingGoal: "Clarify whether the user wants to start, continue, or complete one answer session.",
|
|
3015
|
+
askSequence: [
|
|
3016
|
+
"Ask which questionnaire run this is about.",
|
|
3017
|
+
"Ask whether the user wants to start, continue, review, or complete it.",
|
|
3018
|
+
"If the user wants to continue or finish, ask what state they are in right now before asking for more content.",
|
|
3019
|
+
"If answering is still in progress, ask only for the next answer or note that matters."
|
|
3020
|
+
]
|
|
3021
|
+
},
|
|
3022
|
+
{
|
|
3023
|
+
focus: "movement",
|
|
3024
|
+
openingQuestion: "Are you trying to understand where you stayed and traveled, change one stay or trip, or answer a question about your movement behavior?",
|
|
3025
|
+
coachingGoal: "Clarify whether the user wants a time-in-place query, travel-history review, a missing-gap overlay, one stay or trip change, one place summary, or a link before choosing the dedicated movement route.",
|
|
3026
|
+
askSequence: [
|
|
3027
|
+
"Ask whether the user is trying to query behavior, add something manually, update an existing movement item, or link movement to another Forge entity.",
|
|
3028
|
+
"Ask whether the focus is a stay, a trip, a place, a timeline window, or a selected span.",
|
|
3029
|
+
"Ask for the time window, place, or movement item that makes the question concrete.",
|
|
3030
|
+
"Ask what they are trying to notice, preserve, or answer through that movement context.",
|
|
3031
|
+
"If the request is filling a missing-data gap, use a user-defined movement box rather than a raw stay or trip patch.",
|
|
3032
|
+
"When the user already gave a concrete correction like 'I stayed home during that missing block', confirm only the interval or place if needed, then create the overlay and read the timeline back."
|
|
3033
|
+
]
|
|
3034
|
+
},
|
|
3035
|
+
{
|
|
3036
|
+
focus: "life_force",
|
|
3037
|
+
openingQuestion: "Do you want to understand the current energy picture, change how Forge models it, or log how you feel right now?",
|
|
3038
|
+
coachingGoal: "Clarify whether the job is overview, profile change, weekday-template editing, or a real-time fatigue signal before choosing the dedicated life-force route.",
|
|
3039
|
+
askSequence: [
|
|
3040
|
+
"Ask whether the job is overview, profile change, weekday-template change, or fatigue signaling.",
|
|
3041
|
+
"Ask what part of the current energy picture feels most important or inaccurate.",
|
|
3042
|
+
"Ask what should stay true if they are changing profile or template assumptions.",
|
|
3043
|
+
"Route to the dedicated life-force path once the lane is clear."
|
|
3044
|
+
]
|
|
3045
|
+
},
|
|
3046
|
+
{
|
|
3047
|
+
focus: "workbench",
|
|
3048
|
+
openingQuestion: "Are you trying to inspect a flow, change it, run it, or inspect one run's outputs?",
|
|
3049
|
+
coachingGoal: "Clarify whether the user wants flow discovery, editing, execution, run history, published outputs, or node-level inspection before using the dedicated workbench route family.",
|
|
3050
|
+
askSequence: [
|
|
3051
|
+
"Ask whether the job is flow discovery, one flow edit, execution, run history, published output, node-level inspection, or latest-node-output lookup.",
|
|
3052
|
+
"Ask which flow, slug, run, or node the request is about.",
|
|
3053
|
+
"Ask what the user is trying to learn, repair, or publish through that flow.",
|
|
3054
|
+
"Route to the dedicated workbench route family once the execution lane is clear."
|
|
3055
|
+
]
|
|
3056
|
+
},
|
|
2009
3057
|
{
|
|
2010
3058
|
focus: "event_type",
|
|
2011
|
-
openingQuestion: "
|
|
3059
|
+
openingQuestion: "What kind of moment keeps happening that you want future reports to name the same way each time?",
|
|
2012
3060
|
coachingGoal: "Create a reusable incident category that will actually help future reports stay consistent.",
|
|
2013
3061
|
askSequence: [
|
|
2014
|
-
"Ask what
|
|
3062
|
+
"Ask what kind of moment or incident this label should capture in lived terms.",
|
|
2015
3063
|
"Ask how narrow or broad it should be.",
|
|
3064
|
+
"Ask what would count as inside versus outside the category if that boundary is still fuzzy.",
|
|
2016
3065
|
"Ask for a short description only if the label could be ambiguous later."
|
|
2017
3066
|
]
|
|
2018
3067
|
},
|
|
2019
3068
|
{
|
|
2020
3069
|
focus: "emotion_definition",
|
|
2021
|
-
openingQuestion: "
|
|
3070
|
+
openingQuestion: "When this feeling is present, what tells you it is this feeling and not a nearby one?",
|
|
2022
3071
|
coachingGoal: "Create a reusable emotion label with enough clarity to use consistently later.",
|
|
2023
3072
|
askSequence: [
|
|
2024
|
-
"Ask what
|
|
2025
|
-
"Ask what distinguishes it from nearby emotions.",
|
|
3073
|
+
"Ask what this feeling is like in lived terms when the user says it.",
|
|
3074
|
+
"Ask what distinguishes it from nearby emotions if that matters.",
|
|
2026
3075
|
"Ask for a broader category only if it will help later browsing or reporting."
|
|
2027
3076
|
]
|
|
2028
3077
|
}
|
|
@@ -2059,6 +3108,7 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
|
|
|
2059
3108
|
notes: [
|
|
2060
3109
|
"Use an ACT-style values clarification stance: values are directions to live toward, not boxes to complete.",
|
|
2061
3110
|
"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.",
|
|
3111
|
+
"Reflect the pain, longing, or importance that makes the value alive before narrowing to action.",
|
|
2062
3112
|
"If the user says they want to understand it first, start with one orienting question before offering a formulation or save suggestion."
|
|
2063
3113
|
]
|
|
2064
3114
|
},
|
|
@@ -2143,7 +3193,8 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
|
|
|
2143
3193
|
"Keep the user close to observable behavior rather than jumping straight to labels.",
|
|
2144
3194
|
"When the behavior clearly belongs inside a larger loop, suggest linking or also mapping the related behavior_pattern.",
|
|
2145
3195
|
"If the user asks for understanding before storage, ask about the recent example and function of the move before classifying it.",
|
|
2146
|
-
"Ask what the move is trying to do for the user before moving into replacement planning."
|
|
3196
|
+
"Ask what the move is trying to do for the user before moving into replacement planning.",
|
|
3197
|
+
"Name the immediate protective job before discussing costs or alternatives."
|
|
2147
3198
|
]
|
|
2148
3199
|
},
|
|
2149
3200
|
{
|
|
@@ -2331,7 +3382,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
2331
3382
|
{
|
|
2332
3383
|
toolName: "forge_create_entities",
|
|
2333
3384
|
summary: "Create one or more entities in one ordered batch.",
|
|
2334
|
-
whenToUse: "Use after explicit save intent and after duplicate checks when needed.",
|
|
3385
|
+
whenToUse: "Use after explicit save intent and after duplicate checks when needed. This is the default create path for simple Forge entities; do not spray one-off direct mutation routes when the batch contract already covers the record.",
|
|
2335
3386
|
inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, clientRef?: string, data: object }> }",
|
|
2336
3387
|
requiredFields: [
|
|
2337
3388
|
"operations",
|
|
@@ -2342,7 +3393,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
2342
3393
|
"entityType alone is never enough; full data is required.",
|
|
2343
3394
|
"Batch multiple related creates together when they come from one user ask.",
|
|
2344
3395
|
"Goal, project, and task creates can include notes: [{ contentMarkdown, author?, tags?, destroyAt?, links? }] and Forge will auto-link those notes to the newly created entity.",
|
|
2345
|
-
"The same batch create route also handles calendar_event, work_block_template, task_timebox, preference_catalog, preference_catalog_item, preference_context, preference_item, and questionnaire_instrument.",
|
|
3396
|
+
"The same batch create route also handles calendar_event, work_block_template, task_timebox, sleep_session, workout_session, preference_catalog, preference_catalog_item, preference_context, preference_item, and questionnaire_instrument.",
|
|
2346
3397
|
"Calendar-event creates still trigger downstream projection sync when a writable provider calendar is selected."
|
|
2347
3398
|
],
|
|
2348
3399
|
example: '{"operations":[{"entityType":"task","data":{"title":"Write the public release notes","projectId":"project_123","status":"focus","notes":[{"contentMarkdown":"Starting from the changelog draft and the last QA pass."}]},"clientRef":"task-1"}]}'
|
|
@@ -2350,7 +3401,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
2350
3401
|
{
|
|
2351
3402
|
toolName: "forge_update_entities",
|
|
2352
3403
|
summary: "Patch one or more entities in one ordered batch.",
|
|
2353
|
-
whenToUse: "Use when ids are known and the user explicitly wants a change persisted.",
|
|
3404
|
+
whenToUse: "Use when ids are known and the user explicitly wants a change persisted. This is the default update path for simple Forge entities, including manual health-session CRUD.",
|
|
2354
3405
|
inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, id: string, clientRef?: string, patch: object }> }",
|
|
2355
3406
|
requiredFields: [
|
|
2356
3407
|
"operations",
|
|
@@ -2363,7 +3414,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
2363
3414
|
"Project lifecycle is status-driven: patch project.status to active, paused, or completed instead of looking for separate suspend, restart, or finish routes.",
|
|
2364
3415
|
"Setting project.status to completed finishes the project and auto-completes linked unfinished tasks through the normal task completion path.",
|
|
2365
3416
|
"Task and project scheduling rules stay on these same entity patches. Update task.schedulingRules, task.plannedDurationSeconds, or project.schedulingRules here.",
|
|
2366
|
-
"Use this same route to move or relink calendar_event records, edit work_block_template or
|
|
3417
|
+
"Use this same route to move or relink calendar_event records, edit work_block_template, task_timebox, sleep_session, or workout_session records, and do normal field updates on preference_catalog, preference_catalog_item, preference_context, preference_item, and questionnaire_instrument."
|
|
2367
3418
|
],
|
|
2368
3419
|
example: '{"operations":[{"entityType":"project","id":"project_123","patch":{"status":"completed"},"clientRef":"project-finish-1"}]}'
|
|
2369
3420
|
},
|
|
@@ -2381,7 +3432,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
2381
3432
|
"Delete defaults to soft.",
|
|
2382
3433
|
"Use mode=hard only for explicit permanent removal.",
|
|
2383
3434
|
"Restoration is only possible after soft delete.",
|
|
2384
|
-
"calendar_event, work_block_template, and
|
|
3435
|
+
"calendar_event, work_block_template, task_timebox, sleep_session, and workout_session are immediate deletions: calendar events delete remote projections too, and these records do not go through the settings bin."
|
|
2385
3436
|
],
|
|
2386
3437
|
example: '{"operations":[{"entityType":"task","id":"task_123","mode":"soft","reason":"Merged into another task"}]}'
|
|
2387
3438
|
},
|
|
@@ -2532,7 +3583,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
2532
3583
|
{
|
|
2533
3584
|
toolName: "forge_update_sleep_session",
|
|
2534
3585
|
summary: "Patch one sleep session with reflective notes, tags, or linked Forge context.",
|
|
2535
|
-
whenToUse: "Use after reviewing a specific night when the operator wants richer context stored on that sleep record.",
|
|
3586
|
+
whenToUse: "Use after reviewing a specific night when the operator wants richer context stored on that sleep record. Do not use this as the primary CRUD path when batch entity mutation already fits the job.",
|
|
2536
3587
|
inputShape: "{ sleepId: string, qualitySummary?: string, notes?: string, tags?: string[], links?: Array<{ entityType, entityId, relationshipType? }> }",
|
|
2537
3588
|
requiredFields: ["sleepId"],
|
|
2538
3589
|
notes: [
|
|
@@ -2544,7 +3595,7 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
2544
3595
|
{
|
|
2545
3596
|
toolName: "forge_update_workout_session",
|
|
2546
3597
|
summary: "Patch one workout session with subjective effort, mood, meaning, tags, or linked Forge context.",
|
|
2547
|
-
whenToUse: "Use after reviewing one sports session when the operator wants the workout record to carry narrative or planning context.",
|
|
3598
|
+
whenToUse: "Use after reviewing one sports session when the operator wants the workout record to carry narrative or planning context. Do not use this as the primary CRUD path when batch entity mutation already fits the job.",
|
|
2548
3599
|
inputShape: "{ workoutId: string, subjectiveEffort?: integer|null, moodBefore?: string, moodAfter?: string, meaningText?: string, plannedContext?: string, socialContext?: string, tags?: string[], links?: Array<{ entityType, entityId, relationshipType? }> }",
|
|
2549
3600
|
requiredFields: ["workoutId"],
|
|
2550
3601
|
notes: [
|
|
@@ -2805,6 +3856,10 @@ function buildAgentOnboardingPayload(request) {
|
|
|
2805
3856
|
calendar: "A connected calendar source mirrored into Forge. Calendar state combines provider events, recurring work blocks, and task timeboxes.",
|
|
2806
3857
|
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.",
|
|
2807
3858
|
taskTimebox: "A planned or live calendar slot tied to a task. Timeboxes can be suggested in advance or created automatically from active task runs.",
|
|
3859
|
+
workAdjustment: "A work adjustment is a truthful signed minute correction on an existing task or project when real work happened but no live run was active.",
|
|
3860
|
+
movement: "Forge Movement is the first-class mobility surface. It is a timeline of stays and trips: stays capture time spent in the same place, and trips capture travel between places. Use it for time-in-place questions, travel-history review, specific stay or trip edits, selected-span aggregates, known places, and links to other Forge records rather than pretending stays and trips are normal batch CRUD entities.",
|
|
3861
|
+
lifeForce: "Life Force is Forge's energy-budget and fatigue model. Read it through the dedicated life-force payload and update it through focused profile, weekday-template, and fatigue-signal routes rather than generic entity CRUD.",
|
|
3862
|
+
workbench: "Workbench is Forge's graph-flow execution system. Treat flows, runs, published outputs, node results, and latest-node-output reads as a dedicated API family instead of a normal entity-batch surface.",
|
|
2808
3863
|
psyche: "Forge Psyche is the reflective domain for values, patterns, behaviors, beliefs, modes, and trigger reports. It is sensitive and should be handled deliberately."
|
|
2809
3864
|
},
|
|
2810
3865
|
psycheSubmoduleModel: {
|
|
@@ -2864,7 +3919,9 @@ function buildAgentOnboardingPayload(request) {
|
|
|
2864
3919
|
"preference_catalog_item",
|
|
2865
3920
|
"preference_context",
|
|
2866
3921
|
"preference_item",
|
|
2867
|
-
"questionnaire_instrument"
|
|
3922
|
+
"questionnaire_instrument",
|
|
3923
|
+
"sleep_session",
|
|
3924
|
+
"workout_session"
|
|
2868
3925
|
],
|
|
2869
3926
|
batchRoutes: {
|
|
2870
3927
|
search: "/api/v1/entities/search",
|
|
@@ -2873,6 +3930,19 @@ function buildAgentOnboardingPayload(request) {
|
|
|
2873
3930
|
delete: "/api/v1/entities/delete",
|
|
2874
3931
|
restore: "/api/v1/entities/restore"
|
|
2875
3932
|
},
|
|
3933
|
+
specializedCrudEntities: {
|
|
3934
|
+
wiki_page: {
|
|
3935
|
+
create: "/api/v1/wiki/pages",
|
|
3936
|
+
update: "/api/v1/wiki/pages/:id",
|
|
3937
|
+
read: "/api/v1/wiki/pages/:id"
|
|
3938
|
+
},
|
|
3939
|
+
calendar_connection: {
|
|
3940
|
+
list: "/api/v1/calendar/connections",
|
|
3941
|
+
create: "/api/v1/calendar/connections",
|
|
3942
|
+
update: "/api/v1/calendar/connections/:id",
|
|
3943
|
+
delete: "/api/v1/calendar/connections/:id"
|
|
3944
|
+
}
|
|
3945
|
+
},
|
|
2876
3946
|
actionEntities: {
|
|
2877
3947
|
task_run: {
|
|
2878
3948
|
readModel: "/api/v1/operator/context",
|
|
@@ -2915,15 +3985,91 @@ function buildAgentOnboardingPayload(request) {
|
|
|
2915
3985
|
selfObservation: {
|
|
2916
3986
|
read: "/api/v1/psyche/self-observation/calendar",
|
|
2917
3987
|
writeModel: "Create or update an observed note with frontmatter.observedAt. Manual reflections usually carry the Self-observation tag, while movement sync can also publish rolling observed notes tagged movement."
|
|
3988
|
+
}
|
|
3989
|
+
},
|
|
3990
|
+
specializedDomainSurfaces: {
|
|
3991
|
+
movement: {
|
|
3992
|
+
summary: "Dedicated movement workspace API. Use these routes for stays, trips, time-in-place questions, visited places, trip detail, selected-span aggregates, and user-defined overlays.",
|
|
3993
|
+
readRoutes: {
|
|
3994
|
+
day: "/api/v1/movement/day",
|
|
3995
|
+
month: "/api/v1/movement/month",
|
|
3996
|
+
allTime: "/api/v1/movement/all-time",
|
|
3997
|
+
timeline: "/api/v1/movement/timeline",
|
|
3998
|
+
places: "/api/v1/movement/places",
|
|
3999
|
+
tripDetail: "/api/v1/movement/trips/:id",
|
|
4000
|
+
selection: "/api/v1/movement/selection",
|
|
4001
|
+
settings: "/api/v1/movement/settings"
|
|
4002
|
+
},
|
|
4003
|
+
writeRoutes: {
|
|
4004
|
+
placeCreate: "/api/v1/movement/places",
|
|
4005
|
+
placeUpdate: "/api/v1/movement/places/:id",
|
|
4006
|
+
userBoxCreate: "/api/v1/movement/user-boxes",
|
|
4007
|
+
userBoxPreflight: "/api/v1/movement/user-boxes/preflight",
|
|
4008
|
+
userBoxUpdate: "/api/v1/movement/user-boxes/:id",
|
|
4009
|
+
automaticBoxInvalidate: "/api/v1/movement/automatic-boxes/:id/invalidate",
|
|
4010
|
+
stayUpdate: "/api/v1/movement/stays/:id",
|
|
4011
|
+
tripUpdate: "/api/v1/movement/trips/:id",
|
|
4012
|
+
tripPointUpdate: "/api/v1/movement/trips/:id/points/:pointId"
|
|
4013
|
+
},
|
|
4014
|
+
notes: [
|
|
4015
|
+
"Movement is not a normal batch CRUD entity family. It is a dedicated record of stays and trips: a stay means the user remained in the same place for a span of time, and a trip means they traveled between places.",
|
|
4016
|
+
"Use /api/v1/movement/day, /month, /all-time, /timeline, or /selection when the user wants behavioral answers such as how long they stayed at home, when they traveled, which places dominated a period, or what happened across a selected span.",
|
|
4017
|
+
"Use the movement write routes when the user wants to add a place or manual overlay, update a specific stay or trip, or attach movement context to another Forge record. If the user is filling a missing-data gap, the usual write path is a user-defined overlay box rather than a raw stay or trip patch.",
|
|
4018
|
+
"For an explicit statement like 'that missing block was me staying home', do not reopen broad intake. Preflight only if timing overlap is unclear, then create a user-defined `stay` box for that interval and read the updated timeline back."
|
|
4019
|
+
]
|
|
2918
4020
|
},
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
4021
|
+
lifeForce: {
|
|
4022
|
+
summary: "Dedicated life-force API. Use it to read the current energy budget, drains, recommendations, and warnings, then patch only the parts that are meant to be user-controlled.",
|
|
4023
|
+
readRoutes: {
|
|
4024
|
+
overview: "/api/v1/life-force"
|
|
4025
|
+
},
|
|
4026
|
+
writeRoutes: {
|
|
4027
|
+
profile: "/api/v1/life-force/profile",
|
|
4028
|
+
weekdayTemplate: "/api/v1/life-force/templates/:weekday",
|
|
4029
|
+
fatigueSignal: "/api/v1/life-force/fatigue-signals"
|
|
4030
|
+
},
|
|
4031
|
+
notes: [
|
|
4032
|
+
"Life Force is a focused domain surface, not a batch CRUD entity type.",
|
|
4033
|
+
"Use GET /api/v1/life-force for the current overview payload with stats, drains, recommendations, and current-curve state.",
|
|
4034
|
+
"Patch the profile only for durable personal settings, update weekday templates only for the curve itself, and post fatigue signals for real-time tired or recovered observations."
|
|
4035
|
+
]
|
|
2922
4036
|
},
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
4037
|
+
workbench: {
|
|
4038
|
+
summary: "Dedicated graph-flow API. Use it for flow catalog reads, flow CRUD, execution, run history, published outputs, node results, and latest successful node outputs.",
|
|
4039
|
+
readRoutes: {
|
|
4040
|
+
listFlows: "/api/v1/workbench/flows",
|
|
4041
|
+
flowById: "/api/v1/workbench/flows/:id",
|
|
4042
|
+
flowBySlug: "/api/v1/workbench/flows/by-slug/:slug",
|
|
4043
|
+
publishedOutput: "/api/v1/workbench/flows/:id/output",
|
|
4044
|
+
runs: "/api/v1/workbench/flows/:id/runs",
|
|
4045
|
+
runDetail: "/api/v1/workbench/flows/:id/runs/:runId",
|
|
4046
|
+
runNodes: "/api/v1/workbench/flows/:id/runs/:runId/nodes",
|
|
4047
|
+
nodeResult: "/api/v1/workbench/flows/:id/runs/:runId/nodes/:nodeId",
|
|
4048
|
+
latestNodeOutput: "/api/v1/workbench/flows/:id/nodes/:nodeId/output",
|
|
4049
|
+
boxCatalog: "/api/v1/workbench/catalog/boxes"
|
|
4050
|
+
},
|
|
4051
|
+
writeRoutes: {
|
|
4052
|
+
createFlow: "/api/v1/workbench/flows",
|
|
4053
|
+
updateFlow: "/api/v1/workbench/flows/:id",
|
|
4054
|
+
deleteFlow: "/api/v1/workbench/flows/:id",
|
|
4055
|
+
runFlow: "/api/v1/workbench/flows/:id/run",
|
|
4056
|
+
runByPayload: "/api/v1/workbench/run",
|
|
4057
|
+
chatFlow: "/api/v1/workbench/flows/:id/chat"
|
|
4058
|
+
},
|
|
4059
|
+
notes: [
|
|
4060
|
+
"Workbench is a dedicated execution surface, not a batch CRUD entity family.",
|
|
4061
|
+
"Use the flow routes when the agent needs stable public input contracts, published outputs, node-level results, or reusable execution history.",
|
|
4062
|
+
"Prefer the dedicated output and node-result routes over reverse-engineering raw traces."
|
|
4063
|
+
]
|
|
2926
4064
|
}
|
|
4065
|
+
},
|
|
4066
|
+
readModelOnlySurfaces: {
|
|
4067
|
+
sleepOverview: "/api/v1/health/sleep",
|
|
4068
|
+
sportsOverview: "/api/v1/health/fitness",
|
|
4069
|
+
selfObservation: "/api/v1/psyche/self-observation/calendar",
|
|
4070
|
+
calendarOverview: "/api/v1/calendar/overview",
|
|
4071
|
+
operatorOverview: "/api/v1/operator/overview",
|
|
4072
|
+
operatorContext: "/api/v1/operator/context"
|
|
2927
4073
|
}
|
|
2928
4074
|
},
|
|
2929
4075
|
multiUserModel: {
|
|
@@ -3000,6 +4146,10 @@ function buildAgentOnboardingPayload(request) {
|
|
|
3000
4146
|
weeklyReview: "/api/v1/reviews/weekly",
|
|
3001
4147
|
sleepOverview: "/api/v1/health/sleep",
|
|
3002
4148
|
sportsOverview: "/api/v1/health/fitness",
|
|
4149
|
+
lifeForce: "/api/v1/life-force",
|
|
4150
|
+
movementTimeline: "/api/v1/movement/timeline",
|
|
4151
|
+
movementAllTime: "/api/v1/movement/all-time",
|
|
4152
|
+
workbenchFlows: "/api/v1/workbench/flows",
|
|
3003
4153
|
wikiSettings: "/api/v1/wiki/settings",
|
|
3004
4154
|
wikiSearch: "/api/v1/wiki/search",
|
|
3005
4155
|
wikiHealth: "/api/v1/wiki/health",
|
|
@@ -3072,11 +4222,11 @@ function buildAgentOnboardingPayload(request) {
|
|
|
3072
4222
|
saveSuggestionPlacement: "end_of_message",
|
|
3073
4223
|
saveSuggestionTone: "gentle_optional",
|
|
3074
4224
|
maxQuestionsPerTurn: 1,
|
|
3075
|
-
psycheExplorationRule: "When a Psyche entity needs understanding first, begin with one exploratory question before any working formulation, replacement belief, suggested title, or save pitch. Keep the opening reflection to one or two short sentences, stay in plain prose instead of bullets or numbered lists, keep that first reply short, do not mention Forge search or save structure yet, avoid colons or list-shaped phrasing, prefer what/when/how over why until the experience is grounded,
|
|
3076
|
-
psycheOpeningQuestionRule: "Prefer a concrete opening question tied to the entity: ask when the value mattered, what happened the last time the pattern appeared, what cue or body signal came first before the behavior, what the belief starts saying about self or outcome, what feels most at risk inside the mode, what the part is trying to get the user to do or stop doing, or where the shift began in the incident. Reflect briefly before the question
|
|
4225
|
+
psycheExplorationRule: "When a Psyche entity needs understanding first, begin with one exploratory question before any working formulation, replacement belief, suggested title, or save pitch. Keep the opening reflection to one or two short sentences, stay in plain prose instead of bullets or numbered lists, keep that first reply short, do not mention Forge search or save structure yet, avoid colons or list-shaped phrasing, prefer what/when/how over why until the experience is grounded, wait for the user's answer before offering a fuller formulation, ask permission before moving from charged exploration into naming or challenge when needed, do not widen into adjacent entities until the current one has a working sentence the user recognizes, and once the lived experience is coherent stop deepening and help the user name it cleanly. If the user accepts the wording, move toward the save instead of reopening deeper exploration.",
|
|
4226
|
+
psycheOpeningQuestionRule: "Prefer a concrete opening question tied to the entity: ask when the value mattered, what happened the last time the pattern appeared, what cue or body signal came first before the behavior, what the belief starts saying about self or outcome, what feels most at risk inside the mode, what the part is trying to get the user to do or stop doing, or where the shift began in the incident. Reflect briefly before the question, choose one follow-up lane at a time, say what is becoming clearer before the next deeper question, and if several Psyche entities are visible hold the adjacent ones lightly until the main container is clear.",
|
|
3077
4227
|
duplicateCheckRoute: "/api/v1/entities/search",
|
|
3078
4228
|
uiSuggestionRule: "offer_visual_ui_when_review_or_editing_would_be_easier",
|
|
3079
|
-
browserFallbackRule: "Do not open the Forge UI or a browser just to create or update normal entities when the batch entity tools can do the job.",
|
|
4229
|
+
browserFallbackRule: "Do not open the Forge UI or a browser just to create or update normal entities when the batch entity tools can do the job. Batch CRUD is the default for simple entities; avoid spamming the agent with a large one-route-per-entity mental model.",
|
|
3080
4230
|
writeConsentRule: "If an entity is only implied, keep helping in the main conversation and offer Forge lightly at the end. Only write after explicit save intent or after the user accepts the Forge save offer."
|
|
3081
4231
|
},
|
|
3082
4232
|
mutationGuidance: {
|
|
@@ -3089,12 +4239,12 @@ function buildAgentOnboardingPayload(request) {
|
|
|
3089
4239
|
},
|
|
3090
4240
|
deleteDefault: "soft",
|
|
3091
4241
|
hardDeleteRequiresExplicitMode: true,
|
|
3092
|
-
restoreSummary: "Restore soft-deleted entities through the restore route or the settings bin.
|
|
3093
|
-
entityDeleteSummary: "Entity DELETE routes default to soft delete. Pass mode=hard only when permanent removal is intended.
|
|
3094
|
-
batchingRule: "forge_create_entities, forge_update_entities, forge_delete_entities, and forge_restore_entities all accept operations as arrays. Batch multiple related mutations together
|
|
4242
|
+
restoreSummary: "Restore soft-deleted entities through the restore route or the settings bin. Immediate-delete entities such as calendar_event, work_block_template, task_timebox, sleep_session, and workout_session do not enter the bin.",
|
|
4243
|
+
entityDeleteSummary: "Entity DELETE routes default to soft delete. Pass mode=hard only when permanent removal is intended. Immediate-delete entities skip the bin, and calendar-event deletes still remove remote projections downstream.",
|
|
4244
|
+
batchingRule: "forge_create_entities, forge_update_entities, forge_delete_entities, and forge_restore_entities all accept operations as arrays. Batch CRUD is the default for simple entities, so batch multiple related mutations together instead of reaching for a long list of entity-specific routes.",
|
|
3095
4245
|
searchRule: "forge_search_entities accepts searches as an array. Search before create or update when duplicate risk exists.",
|
|
3096
|
-
createRule: "Each create operation must include entityType and full data. entityType alone is not enough. This includes calendar_event, work_block_template, and
|
|
3097
|
-
updateRule: "Each update operation must include entityType, id, and patch. For projects, lifecycle changes are status patches: active to restart, paused to suspend, completed to finish. Keep task and project scheduling rules on those same patch payloads. Calendar-event updates still run downstream provider projection sync.",
|
|
4246
|
+
createRule: "Each create operation must include entityType and full data. entityType alone is not enough. This includes calendar_event, work_block_template, task_timebox, sleep_session, workout_session, preference CRUD entities, and questionnaire_instrument alongside the usual planning and Psyche entities.",
|
|
4247
|
+
updateRule: "Each update operation must include entityType, id, and patch. For projects, lifecycle changes are status patches: active to restart, paused to suspend, completed to finish. Keep task and project scheduling rules on those same patch payloads. Calendar-event updates still run downstream provider projection sync, and manual health-session field edits belong on the batch route by default rather than on the reflective review helpers.",
|
|
3098
4248
|
createExample: '{"operations":[{"entityType":"goal","data":{"title":"Create meaningfully"},"clientRef":"goal-create-1"},{"entityType":"goal","data":{"title":"Build a beautiful family"},"clientRef":"goal-create-2"}]}',
|
|
3099
4249
|
updateExample: '{"operations":[{"entityType":"project","id":"project_123","patch":{"status":"paused","schedulingRules":{"blockWorkBlockKinds":["main_activity"],"allowWorkBlockKinds":["secondary_activity"]}},"clientRef":"project-suspend-1"},{"entityType":"task","id":"task_456","patch":{"plannedDurationSeconds":5400,"schedulingRules":{"allowEventKeywords":["creative"],"blockEventKeywords":["clinic"]}},"clientRef":"task-scheduling-1"}]}'
|
|
3100
4250
|
}
|
|
@@ -3309,6 +4459,7 @@ function buildV1Context(userIds) {
|
|
|
3309
4459
|
const tasks = filterOwnedEntities("task", listTasks(), userIds);
|
|
3310
4460
|
const habits = filterOwnedEntities("habit", listHabits(), userIds);
|
|
3311
4461
|
const users = listUsers();
|
|
4462
|
+
const dashboard = getDashboard({ userIds });
|
|
3312
4463
|
const selectedUsers = userIds && userIds.length > 0
|
|
3313
4464
|
? users.filter((user) => userIds.includes(user.id))
|
|
3314
4465
|
: users;
|
|
@@ -3321,7 +4472,7 @@ function buildV1Context(userIds) {
|
|
|
3321
4472
|
mode: "transitional-node"
|
|
3322
4473
|
},
|
|
3323
4474
|
metrics: buildGamificationProfile(goals, tasks, habits),
|
|
3324
|
-
dashboard
|
|
4475
|
+
dashboard,
|
|
3325
4476
|
overview: getOverviewContext(new Date(), { userIds }),
|
|
3326
4477
|
today: getTodayContext(new Date(), { userIds }),
|
|
3327
4478
|
risk: getRiskContext(new Date(), { userIds }),
|
|
@@ -3337,7 +4488,8 @@ function buildV1Context(userIds) {
|
|
|
3337
4488
|
selectedUsers
|
|
3338
4489
|
},
|
|
3339
4490
|
activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
|
|
3340
|
-
activity:
|
|
4491
|
+
activity: dashboard.recentActivity,
|
|
4492
|
+
lifeForce: buildLifeForcePayload(new Date(), userIds)
|
|
3341
4493
|
};
|
|
3342
4494
|
}
|
|
3343
4495
|
function buildXpMetricsPayload() {
|
|
@@ -3614,6 +4766,7 @@ export async function buildServer(options = {}) {
|
|
|
3614
4766
|
configureDatabaseSeeding(options.seedDemoData ?? false);
|
|
3615
4767
|
await managers.migration.initialize();
|
|
3616
4768
|
ensureSystemUsers();
|
|
4769
|
+
getSettings();
|
|
3617
4770
|
const app = Fastify({
|
|
3618
4771
|
logger: false,
|
|
3619
4772
|
rewriteUrl: (request) => rewriteMountPath(request.url ?? "/")
|
|
@@ -3654,7 +4807,8 @@ export async function buildServer(options = {}) {
|
|
|
3654
4807
|
}
|
|
3655
4808
|
try {
|
|
3656
4809
|
const interval = CronExpressionParser.parse(processor.cronExpression, {
|
|
3657
|
-
currentDate: processor.lastRunAt &&
|
|
4810
|
+
currentDate: processor.lastRunAt &&
|
|
4811
|
+
Number.isFinite(Date.parse(processor.lastRunAt))
|
|
3658
4812
|
? processor.lastRunAt
|
|
3659
4813
|
: new Date(now.getTime() - 60_000).toISOString()
|
|
3660
4814
|
});
|
|
@@ -3676,9 +4830,19 @@ export async function buildServer(options = {}) {
|
|
|
3676
4830
|
}
|
|
3677
4831
|
}, 30_000);
|
|
3678
4832
|
cronSchedulerTimer.unref?.();
|
|
4833
|
+
const dataBackupTimer = setInterval(() => {
|
|
4834
|
+
void maybeRunAutomaticBackup().catch(() => {
|
|
4835
|
+
// Automatic backup sweeps should never crash the runtime loop.
|
|
4836
|
+
});
|
|
4837
|
+
}, 5 * 60 * 1000);
|
|
4838
|
+
dataBackupTimer.unref?.();
|
|
4839
|
+
void maybeRunAutomaticBackup().catch(() => {
|
|
4840
|
+
// Ignore startup backup failures; the Data settings surface exposes recovery.
|
|
4841
|
+
});
|
|
3679
4842
|
app.addHook("onClose", async () => {
|
|
3680
4843
|
clearInterval(diagnosticRetentionTimer);
|
|
3681
4844
|
clearInterval(cronSchedulerTimer);
|
|
4845
|
+
clearInterval(dataBackupTimer);
|
|
3682
4846
|
taskRunWatchdog?.stop();
|
|
3683
4847
|
await managers.backgroundJobs.stop();
|
|
3684
4848
|
});
|
|
@@ -3706,8 +4870,7 @@ export async function buildServer(options = {}) {
|
|
|
3706
4870
|
url.startsWith("/api/v1/events/meta"));
|
|
3707
4871
|
};
|
|
3708
4872
|
app.addHook("onRequest", async (request) => {
|
|
3709
|
-
request.diagnosticStartedAt =
|
|
3710
|
-
process.hrtime.bigint();
|
|
4873
|
+
request.diagnosticStartedAt = process.hrtime.bigint();
|
|
3711
4874
|
});
|
|
3712
4875
|
app.addHook("onResponse", async (request, reply) => {
|
|
3713
4876
|
const routeUrl = request.routeOptions.url || request.url;
|
|
@@ -3984,12 +5147,59 @@ export async function buildServer(options = {}) {
|
|
|
3984
5147
|
? {
|
|
3985
5148
|
runtime: {
|
|
3986
5149
|
pid: process.pid,
|
|
3987
|
-
storageRoot:
|
|
5150
|
+
storageRoot: getEffectiveDataRoot(),
|
|
3988
5151
|
basePath: runtimeConfig.basePath
|
|
3989
5152
|
}
|
|
3990
5153
|
}
|
|
3991
5154
|
: {})
|
|
3992
5155
|
}));
|
|
5156
|
+
app.get("/api/v1/doctor", async (request) => {
|
|
5157
|
+
requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/doctor" });
|
|
5158
|
+
const settings = getSettings();
|
|
5159
|
+
const settingsFile = getSettingsFileStatus();
|
|
5160
|
+
const runtime = {
|
|
5161
|
+
pid: process.pid,
|
|
5162
|
+
storageRoot: getEffectiveDataRoot(),
|
|
5163
|
+
dataDir: resolveDataDir(),
|
|
5164
|
+
databasePath: resolveDatabasePathForDataRoot(),
|
|
5165
|
+
basePath: runtimeConfig.basePath,
|
|
5166
|
+
devWebOrigin: process.env.FORGE_DEV_WEB_ORIGIN?.trim() || null
|
|
5167
|
+
};
|
|
5168
|
+
const health = buildHealthPayload(taskRunWatchdog, {
|
|
5169
|
+
apiVersion: "v1",
|
|
5170
|
+
backend: "forge-node-runtime",
|
|
5171
|
+
runtime
|
|
5172
|
+
});
|
|
5173
|
+
const warnings = [];
|
|
5174
|
+
if (!settingsFile.valid) {
|
|
5175
|
+
warnings.push(`forge.json is invalid at ${settingsFile.path}. Forge ignored file precedence until the JSON is repaired or rewritten.`);
|
|
5176
|
+
}
|
|
5177
|
+
if (settingsFile.syncState === "applied_file_overrides") {
|
|
5178
|
+
warnings.push("forge.json overrode one or more persisted database settings on this run.");
|
|
5179
|
+
}
|
|
5180
|
+
if (health.ok === false) {
|
|
5181
|
+
warnings.push("The task-run watchdog reported degraded health.");
|
|
5182
|
+
}
|
|
5183
|
+
return {
|
|
5184
|
+
doctor: {
|
|
5185
|
+
ok: health.ok && settingsFile.valid,
|
|
5186
|
+
now: new Date().toISOString(),
|
|
5187
|
+
runtime,
|
|
5188
|
+
health,
|
|
5189
|
+
settingsFile,
|
|
5190
|
+
settingsSummary: {
|
|
5191
|
+
themePreference: settings.themePreference,
|
|
5192
|
+
localePreference: settings.localePreference,
|
|
5193
|
+
operatorName: settings.profile.operatorName,
|
|
5194
|
+
maxActiveTasks: settings.execution.maxActiveTasks,
|
|
5195
|
+
timeAccountingMode: settings.execution.timeAccountingMode,
|
|
5196
|
+
psycheAuthRequired: settings.security.psycheAuthRequired,
|
|
5197
|
+
webAppUrl: `http://127.0.0.1:${runtimeConfig.port}${runtimeConfig.basePath}`
|
|
5198
|
+
},
|
|
5199
|
+
warnings
|
|
5200
|
+
}
|
|
5201
|
+
};
|
|
5202
|
+
});
|
|
3993
5203
|
app.get("/api/v1/auth/operator-session", async (request, reply) => ({
|
|
3994
5204
|
session: managers.session.ensureLocalOperatorSession(request.headers, reply)
|
|
3995
5205
|
}));
|
|
@@ -3998,15 +5208,116 @@ export async function buildServer(options = {}) {
|
|
|
3998
5208
|
}));
|
|
3999
5209
|
app.get("/api/v1/openapi.json", async () => buildOpenApiDocument());
|
|
4000
5210
|
app.get("/api/v1/context", async (request) => buildV1Context(resolveScopedUserIds(request.query)));
|
|
5211
|
+
app.get("/api/v1/life-force", async (request) => ({
|
|
5212
|
+
lifeForce: buildLifeForcePayload(new Date(), resolveScopedUserIds(request.query)),
|
|
5213
|
+
templates: listLifeForceTemplates(resolveLifeForceUser(resolveScopedUserIds(request.query)).id)
|
|
5214
|
+
}));
|
|
5215
|
+
app.patch("/api/v1/life-force/profile", async (request) => {
|
|
5216
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/life-force/profile" });
|
|
5217
|
+
const userId = resolveLifeForceUser(resolveScopedUserIds(request.query)).id;
|
|
5218
|
+
return {
|
|
5219
|
+
lifeForce: updateLifeForceProfile(userId, lifeForceProfilePatchSchema.parse(request.body ?? {})),
|
|
5220
|
+
actor: auth.session?.actorLabel ?? auth.actor ?? "Forge"
|
|
5221
|
+
};
|
|
5222
|
+
});
|
|
5223
|
+
app.put("/api/v1/life-force/templates/:weekday", async (request) => {
|
|
5224
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/life-force/templates/:weekday" });
|
|
5225
|
+
const weekday = Number(request.params.weekday);
|
|
5226
|
+
return {
|
|
5227
|
+
weekday,
|
|
5228
|
+
points: updateLifeForceTemplate(resolveLifeForceUser(resolveScopedUserIds(request.query)).id, weekday, lifeForceTemplateUpdateSchema.parse(request.body ?? {})),
|
|
5229
|
+
actor: auth.session?.actorLabel ?? auth.actor ?? "Forge"
|
|
5230
|
+
};
|
|
5231
|
+
});
|
|
5232
|
+
app.post("/api/v1/life-force/fatigue-signals", async (request) => {
|
|
5233
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/life-force/fatigue-signals" });
|
|
5234
|
+
return {
|
|
5235
|
+
lifeForce: createFatigueSignal(resolveLifeForceUser(resolveScopedUserIds(request.query)).id, fatigueSignalCreateSchema.parse(request.body ?? {})),
|
|
5236
|
+
actor: auth.session?.actorLabel ?? auth.actor ?? "Forge"
|
|
5237
|
+
};
|
|
5238
|
+
});
|
|
5239
|
+
app.get("/api/v1/knowledge-graph", async (request) => {
|
|
5240
|
+
const query = request.query;
|
|
5241
|
+
const readString = (value) => typeof value === "string" ? value.trim() : "";
|
|
5242
|
+
const readList = (key) => {
|
|
5243
|
+
const value = query[key];
|
|
5244
|
+
const values = Array.isArray(value) ? value : [value];
|
|
5245
|
+
return values
|
|
5246
|
+
.flatMap((entry) => (typeof entry === "string" ? entry.split(",") : []))
|
|
5247
|
+
.map((entry) => entry.trim())
|
|
5248
|
+
.filter(Boolean);
|
|
5249
|
+
};
|
|
5250
|
+
const limitRaw = readString(query.limit);
|
|
5251
|
+
const limit = limitRaw.length > 0 && Number.isFinite(Number(limitRaw))
|
|
5252
|
+
? Math.max(1, Math.min(2000, Math.round(Number(limitRaw))))
|
|
5253
|
+
: null;
|
|
5254
|
+
return {
|
|
5255
|
+
graph: buildKnowledgeGraph(resolveScopedUserIds(query), {
|
|
5256
|
+
q: readString(query.q) || null,
|
|
5257
|
+
entityKinds: readList("entityKind"),
|
|
5258
|
+
relationKinds: readList("relationKind"),
|
|
5259
|
+
tags: readList("tag"),
|
|
5260
|
+
owners: readList("owner"),
|
|
5261
|
+
updatedFrom: readString(query.updatedFrom) || null,
|
|
5262
|
+
updatedTo: readString(query.updatedTo) || null,
|
|
5263
|
+
limit,
|
|
5264
|
+
focusNodeId: readString(query.focusNodeId) || null
|
|
5265
|
+
})
|
|
5266
|
+
};
|
|
5267
|
+
});
|
|
5268
|
+
app.get("/api/v1/knowledge-graph/focus", async (request, reply) => {
|
|
5269
|
+
const query = request.query;
|
|
5270
|
+
const entityType = typeof query.entityType === "string" ? query.entityType.trim() : "";
|
|
5271
|
+
const entityId = typeof query.entityId === "string" ? query.entityId.trim() : "";
|
|
5272
|
+
if (!entityType || !entityId) {
|
|
5273
|
+
reply.code(400);
|
|
5274
|
+
return {
|
|
5275
|
+
error: "entityType and entityId are required."
|
|
5276
|
+
};
|
|
5277
|
+
}
|
|
5278
|
+
return {
|
|
5279
|
+
focus: buildKnowledgeGraphFocus(entityType, entityId, resolveScopedUserIds(query))
|
|
5280
|
+
};
|
|
5281
|
+
});
|
|
4001
5282
|
app.get("/api/v1/health/overview", async (request) => ({
|
|
4002
5283
|
overview: getCompanionOverview(resolveScopedUserIds(request.query))
|
|
4003
5284
|
}));
|
|
4004
5285
|
app.get("/api/v1/health/sleep", async (request) => ({
|
|
4005
5286
|
sleep: getSleepViewData(resolveScopedUserIds(request.query))
|
|
4006
5287
|
}));
|
|
5288
|
+
app.post("/api/v1/health/sleep", async (request, reply) => {
|
|
5289
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/health/sleep" });
|
|
5290
|
+
const sleep = createSleepSession(createSleepSessionSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
5291
|
+
reply.code(201);
|
|
5292
|
+
return { sleep };
|
|
5293
|
+
});
|
|
5294
|
+
app.get("/api/v1/health/sleep/:id", async (request, reply) => {
|
|
5295
|
+
const { id } = request.params;
|
|
5296
|
+
const sleep = getSleepSessionById(id);
|
|
5297
|
+
if (!sleep) {
|
|
5298
|
+
reply.code(404);
|
|
5299
|
+
return { error: "Sleep session not found" };
|
|
5300
|
+
}
|
|
5301
|
+
return { sleep };
|
|
5302
|
+
});
|
|
4007
5303
|
app.get("/api/v1/health/fitness", async (request) => ({
|
|
4008
5304
|
fitness: getFitnessViewData(resolveScopedUserIds(request.query))
|
|
4009
5305
|
}));
|
|
5306
|
+
app.post("/api/v1/health/workouts", async (request, reply) => {
|
|
5307
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/health/workouts" });
|
|
5308
|
+
const workout = createWorkoutSession(createWorkoutSessionSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
5309
|
+
reply.code(201);
|
|
5310
|
+
return { workout };
|
|
5311
|
+
});
|
|
5312
|
+
app.get("/api/v1/health/workouts/:id", async (request, reply) => {
|
|
5313
|
+
const { id } = request.params;
|
|
5314
|
+
const workout = getWorkoutSessionById(id);
|
|
5315
|
+
if (!workout) {
|
|
5316
|
+
reply.code(404);
|
|
5317
|
+
return { error: "Workout session not found" };
|
|
5318
|
+
}
|
|
5319
|
+
return { workout };
|
|
5320
|
+
});
|
|
4010
5321
|
app.get("/api/v1/movement/day", async (request) => {
|
|
4011
5322
|
const query = request.query;
|
|
4012
5323
|
return {
|
|
@@ -4028,6 +5339,40 @@ export async function buildServer(options = {}) {
|
|
|
4028
5339
|
app.get("/api/v1/movement/all-time", async (request) => ({
|
|
4029
5340
|
movement: getMovementAllTimeSummary(resolveScopedUserIds(request.query))
|
|
4030
5341
|
}));
|
|
5342
|
+
app.get("/api/v1/screen-time/day", async (request) => {
|
|
5343
|
+
const query = request.query;
|
|
5344
|
+
return {
|
|
5345
|
+
screenTime: getScreenTimeDayDetail({
|
|
5346
|
+
date: typeof query.date === "string" ? query.date : undefined,
|
|
5347
|
+
userIds: resolveScopedUserIds(query)
|
|
5348
|
+
})
|
|
5349
|
+
};
|
|
5350
|
+
});
|
|
5351
|
+
app.get("/api/v1/screen-time/month", async (request) => {
|
|
5352
|
+
const query = request.query;
|
|
5353
|
+
return {
|
|
5354
|
+
screenTime: getScreenTimeMonthSummary({
|
|
5355
|
+
month: typeof query.month === "string" ? query.month : undefined,
|
|
5356
|
+
userIds: resolveScopedUserIds(query)
|
|
5357
|
+
})
|
|
5358
|
+
};
|
|
5359
|
+
});
|
|
5360
|
+
app.get("/api/v1/screen-time/all-time", async (request) => ({
|
|
5361
|
+
screenTime: getScreenTimeAllTimeSummary(resolveScopedUserIds(request.query))
|
|
5362
|
+
}));
|
|
5363
|
+
app.get("/api/v1/screen-time/settings", async (request) => ({
|
|
5364
|
+
settings: getScreenTimeSettings(resolveScopedUserIds(request.query))
|
|
5365
|
+
}));
|
|
5366
|
+
app.patch("/api/v1/screen-time/settings", async (request) => {
|
|
5367
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
5368
|
+
route: "/api/v1/screen-time/settings"
|
|
5369
|
+
});
|
|
5370
|
+
const userId = resolveScopedUserIds(request.query)?.[0] ??
|
|
5371
|
+
getDefaultUser().id;
|
|
5372
|
+
return {
|
|
5373
|
+
settings: updateScreenTimeSettings(userId, screenTimeSettingsPatchSchema.parse(request.body ?? {}))
|
|
5374
|
+
};
|
|
5375
|
+
});
|
|
4031
5376
|
app.get("/api/v1/movement/timeline", async (request) => {
|
|
4032
5377
|
const parsed = movementTimelineQuerySchema.parse(request.query ?? {});
|
|
4033
5378
|
return {
|
|
@@ -4035,7 +5380,8 @@ export async function buildServer(options = {}) {
|
|
|
4035
5380
|
...parsed,
|
|
4036
5381
|
userIds: parsed.userIds.length > 0
|
|
4037
5382
|
? parsed.userIds
|
|
4038
|
-
: (resolveScopedUserIds(request.query) ??
|
|
5383
|
+
: (resolveScopedUserIds(request.query) ??
|
|
5384
|
+
[])
|
|
4039
5385
|
})
|
|
4040
5386
|
};
|
|
4041
5387
|
});
|
|
@@ -4066,15 +5412,80 @@ export async function buildServer(options = {}) {
|
|
|
4066
5412
|
}, toActivityContext(auth))
|
|
4067
5413
|
};
|
|
4068
5414
|
});
|
|
4069
|
-
app.patch("/api/v1/movement/places/:id", async (request, reply) => {
|
|
4070
|
-
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/places/:id" });
|
|
5415
|
+
app.patch("/api/v1/movement/places/:id", async (request, reply) => {
|
|
5416
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/places/:id" });
|
|
5417
|
+
const { id } = request.params;
|
|
5418
|
+
const place = updateMovementPlace(id, movementPlacePatchSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
5419
|
+
if (!place) {
|
|
5420
|
+
reply.code(404);
|
|
5421
|
+
return { error: "Movement place not found" };
|
|
5422
|
+
}
|
|
5423
|
+
return { place };
|
|
5424
|
+
});
|
|
5425
|
+
app.post("/api/v1/movement/user-boxes", async (request, reply) => {
|
|
5426
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/user-boxes" });
|
|
5427
|
+
const userId = resolveScopedUserIds(request.query)?.[0] ??
|
|
5428
|
+
getDefaultUser().id;
|
|
5429
|
+
reply.code(201);
|
|
5430
|
+
const created = createMovementUserBox({
|
|
5431
|
+
...movementUserBoxCreateSchema.parse(request.body ?? {}),
|
|
5432
|
+
userId
|
|
5433
|
+
}, toActivityContext(auth));
|
|
5434
|
+
return {
|
|
5435
|
+
box: resolveMovementTimelineSegmentForBox(userId, created.id) ?? created
|
|
5436
|
+
};
|
|
5437
|
+
});
|
|
5438
|
+
app.post("/api/v1/movement/user-boxes/preflight", async (request) => {
|
|
5439
|
+
requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/user-boxes/preflight" });
|
|
5440
|
+
const userId = resolveScopedUserIds(request.query)?.[0] ??
|
|
5441
|
+
getDefaultUser().id;
|
|
5442
|
+
return {
|
|
5443
|
+
preflight: analyzeMovementUserBoxPreflight({
|
|
5444
|
+
...movementUserBoxPreflightSchema.parse(request.body ?? {}),
|
|
5445
|
+
userId
|
|
5446
|
+
})
|
|
5447
|
+
};
|
|
5448
|
+
});
|
|
5449
|
+
app.patch("/api/v1/movement/user-boxes/:id", async (request, reply) => {
|
|
5450
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/user-boxes/:id" });
|
|
5451
|
+
const userId = resolveScopedUserIds(request.query)?.[0] ??
|
|
5452
|
+
getDefaultUser().id;
|
|
5453
|
+
const { id } = request.params;
|
|
5454
|
+
const box = updateMovementUserBox(id, movementUserBoxPatchSchema.parse(request.body ?? {}), toActivityContext(auth), { userId });
|
|
5455
|
+
if (!box) {
|
|
5456
|
+
reply.code(404);
|
|
5457
|
+
return { error: "Movement user box not found" };
|
|
5458
|
+
}
|
|
5459
|
+
return {
|
|
5460
|
+
box: resolveMovementTimelineSegmentForBox(userId, box.id) ?? box
|
|
5461
|
+
};
|
|
5462
|
+
});
|
|
5463
|
+
app.delete("/api/v1/movement/user-boxes/:id", async (request, reply) => {
|
|
5464
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/user-boxes/:id" });
|
|
5465
|
+
const userId = resolveScopedUserIds(request.query)?.[0] ??
|
|
5466
|
+
getDefaultUser().id;
|
|
5467
|
+
const { id } = request.params;
|
|
5468
|
+
const result = deleteMovementUserBox(id, toActivityContext(auth), { userId });
|
|
5469
|
+
if (!result) {
|
|
5470
|
+
reply.code(404);
|
|
5471
|
+
return { error: "Movement user box not found" };
|
|
5472
|
+
}
|
|
5473
|
+
return result;
|
|
5474
|
+
});
|
|
5475
|
+
app.post("/api/v1/movement/automatic-boxes/:id/invalidate", async (request, reply) => {
|
|
5476
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/automatic-boxes/:id/invalidate" });
|
|
5477
|
+
const userId = resolveScopedUserIds(request.query)?.[0] ??
|
|
5478
|
+
getDefaultUser().id;
|
|
4071
5479
|
const { id } = request.params;
|
|
4072
|
-
const
|
|
4073
|
-
if (!
|
|
5480
|
+
const result = invalidateAutomaticMovementBox(id, movementAutomaticBoxInvalidateSchema.parse(request.body ?? {}), toActivityContext(auth), { userId });
|
|
5481
|
+
if (!result) {
|
|
4074
5482
|
reply.code(404);
|
|
4075
|
-
return { error: "
|
|
5483
|
+
return { error: "Automatic movement box not found" };
|
|
4076
5484
|
}
|
|
4077
|
-
|
|
5485
|
+
reply.code(201);
|
|
5486
|
+
return {
|
|
5487
|
+
box: resolveMovementTimelineSegmentForBox(userId, result.box.id) ?? result.box
|
|
5488
|
+
};
|
|
4078
5489
|
});
|
|
4079
5490
|
app.patch("/api/v1/movement/stays/:id", async (request, reply) => {
|
|
4080
5491
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/movement/stays/:id" });
|
|
@@ -4173,6 +5584,23 @@ export async function buildServer(options = {}) {
|
|
|
4173
5584
|
}
|
|
4174
5585
|
return { session };
|
|
4175
5586
|
});
|
|
5587
|
+
app.patch("/api/v1/health/pairing-sessions/:id/sources/:source", async (request, reply) => {
|
|
5588
|
+
requireOperatorSession(request.headers, {
|
|
5589
|
+
route: "/api/v1/health/pairing-sessions/:id/sources/:source"
|
|
5590
|
+
});
|
|
5591
|
+
const params = z
|
|
5592
|
+
.object({
|
|
5593
|
+
id: z.string().trim().min(1),
|
|
5594
|
+
source: companionSourceKeySchema
|
|
5595
|
+
})
|
|
5596
|
+
.parse(request.params ?? {});
|
|
5597
|
+
const session = patchCompanionPairingSourceState(params.id, params.source, patchCompanionPairingSourceStateSchema.parse(request.body ?? {}));
|
|
5598
|
+
if (!session) {
|
|
5599
|
+
reply.code(404);
|
|
5600
|
+
return { error: "Companion pairing session not found" };
|
|
5601
|
+
}
|
|
5602
|
+
return { session };
|
|
5603
|
+
});
|
|
4176
5604
|
app.post("/api/v1/health/pairing-sessions/revoke-all", async (request) => {
|
|
4177
5605
|
const auth = requireOperatorSession(request.headers, {
|
|
4178
5606
|
route: "/api/v1/health/pairing-sessions/revoke-all"
|
|
@@ -4189,9 +5617,13 @@ export async function buildServer(options = {}) {
|
|
|
4189
5617
|
const parsed = movementMobileBootstrapSchema.parse(request.body ?? {});
|
|
4190
5618
|
const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
|
|
4191
5619
|
return {
|
|
5620
|
+
pairingSession: getCompanionPairingSessionById(pairing.id),
|
|
4192
5621
|
movement: getMovementMobileBootstrap(pairing)
|
|
4193
5622
|
};
|
|
4194
5623
|
});
|
|
5624
|
+
app.post("/api/v1/mobile/source-state", async (request) => ({
|
|
5625
|
+
pairingSession: updateMobileCompanionSourceState(updateMobileCompanionSourceStateSchema.parse(request.body ?? {}))
|
|
5626
|
+
}));
|
|
4195
5627
|
app.post("/api/v1/mobile/movement/places", async (request, reply) => {
|
|
4196
5628
|
const parsed = movementMobilePlaceMutationSchema.parse(request.body ?? {});
|
|
4197
5629
|
const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
|
|
@@ -4218,33 +5650,90 @@ export async function buildServer(options = {}) {
|
|
|
4218
5650
|
})
|
|
4219
5651
|
};
|
|
4220
5652
|
});
|
|
4221
|
-
app.
|
|
4222
|
-
const parsed =
|
|
5653
|
+
app.post("/api/v1/mobile/movement/user-boxes", async (request, reply) => {
|
|
5654
|
+
const parsed = movementMobileUserBoxCreateSchema.parse(request.body ?? {});
|
|
5655
|
+
const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
|
|
5656
|
+
reply.code(201);
|
|
5657
|
+
const created = createMovementUserBox({
|
|
5658
|
+
...parsed.box,
|
|
5659
|
+
userId: pairing.user_id
|
|
5660
|
+
}, {
|
|
5661
|
+
actor: "Forge Companion",
|
|
5662
|
+
source: "system"
|
|
5663
|
+
});
|
|
5664
|
+
return {
|
|
5665
|
+
box: resolveMovementTimelineSegmentForBox(pairing.user_id, created.id) ?? created
|
|
5666
|
+
};
|
|
5667
|
+
});
|
|
5668
|
+
app.post("/api/v1/mobile/movement/user-boxes/preflight", async (request) => {
|
|
5669
|
+
const parsed = movementMobileUserBoxPreflightSchema.parse(request.body ?? {});
|
|
5670
|
+
const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
|
|
5671
|
+
return {
|
|
5672
|
+
preflight: analyzeMovementUserBoxPreflight({
|
|
5673
|
+
...parsed.draft,
|
|
5674
|
+
userId: pairing.user_id
|
|
5675
|
+
})
|
|
5676
|
+
};
|
|
5677
|
+
});
|
|
5678
|
+
app.patch("/api/v1/mobile/movement/user-boxes/:id", async (request, reply) => {
|
|
5679
|
+
const parsed = movementMobileUserBoxPatchSchema.parse(request.body ?? {});
|
|
4223
5680
|
const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
|
|
4224
5681
|
const { id } = request.params;
|
|
4225
|
-
const
|
|
5682
|
+
const box = updateMovementUserBox(id, parsed.patch, {
|
|
4226
5683
|
actor: "Forge Companion",
|
|
4227
5684
|
source: "system"
|
|
4228
5685
|
}, { userId: pairing.user_id });
|
|
4229
|
-
if (!
|
|
5686
|
+
if (!box) {
|
|
4230
5687
|
reply.code(404);
|
|
4231
|
-
return { error: "Movement
|
|
5688
|
+
return { error: "Movement user box not found" };
|
|
4232
5689
|
}
|
|
4233
|
-
return {
|
|
5690
|
+
return {
|
|
5691
|
+
box: resolveMovementTimelineSegmentForBox(pairing.user_id, box.id) ?? box
|
|
5692
|
+
};
|
|
4234
5693
|
});
|
|
4235
|
-
app.
|
|
4236
|
-
const parsed =
|
|
5694
|
+
app.delete("/api/v1/mobile/movement/user-boxes/:id", async (request, reply) => {
|
|
5695
|
+
const parsed = movementMobileBootstrapSchema.parse(request.body ?? {});
|
|
4237
5696
|
const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
|
|
4238
5697
|
const { id } = request.params;
|
|
4239
|
-
const
|
|
5698
|
+
const result = deleteMovementUserBox(id, {
|
|
4240
5699
|
actor: "Forge Companion",
|
|
4241
5700
|
source: "system"
|
|
4242
5701
|
}, { userId: pairing.user_id });
|
|
4243
|
-
if (!
|
|
5702
|
+
if (!result) {
|
|
4244
5703
|
reply.code(404);
|
|
4245
|
-
return { error: "Movement
|
|
5704
|
+
return { error: "Movement user box not found" };
|
|
4246
5705
|
}
|
|
4247
|
-
return
|
|
5706
|
+
return result;
|
|
5707
|
+
});
|
|
5708
|
+
app.post("/api/v1/mobile/movement/automatic-boxes/:id/invalidate", async (request, reply) => {
|
|
5709
|
+
const parsed = movementMobileAutomaticBoxInvalidateSchema.parse(request.body ?? {});
|
|
5710
|
+
const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
|
|
5711
|
+
const { id } = request.params;
|
|
5712
|
+
const result = invalidateAutomaticMovementBox(id, parsed.invalidate, {
|
|
5713
|
+
actor: "Forge Companion",
|
|
5714
|
+
source: "system"
|
|
5715
|
+
}, { userId: pairing.user_id });
|
|
5716
|
+
if (!result) {
|
|
5717
|
+
reply.code(404);
|
|
5718
|
+
return { error: "Automatic movement box not found" };
|
|
5719
|
+
}
|
|
5720
|
+
reply.code(201);
|
|
5721
|
+
return {
|
|
5722
|
+
box: resolveMovementTimelineSegmentForBox(pairing.user_id, result.box.id) ??
|
|
5723
|
+
result.box
|
|
5724
|
+
};
|
|
5725
|
+
});
|
|
5726
|
+
app.patch("/api/v1/mobile/movement/stays/:id", async (request, reply) => {
|
|
5727
|
+
reply.code(409);
|
|
5728
|
+
return {
|
|
5729
|
+
error: "Recorded stays are immutable in product UI. Create or edit a user-defined movement box instead."
|
|
5730
|
+
};
|
|
5731
|
+
});
|
|
5732
|
+
app.patch("/api/v1/mobile/movement/trips/:id", async (request, reply) => {
|
|
5733
|
+
reply.code(409);
|
|
5734
|
+
return {
|
|
5735
|
+
error: "Recorded moves are immutable in product UI. Create or edit a user-defined movement box instead."
|
|
5736
|
+
};
|
|
4248
5737
|
});
|
|
4249
5738
|
app.post("/api/v1/mobile/watch/bootstrap", async (request) => {
|
|
4250
5739
|
const parsed = mobileWatchBootstrapSchema.parse(request.body ?? {});
|
|
@@ -4298,6 +5787,16 @@ export async function buildServer(options = {}) {
|
|
|
4298
5787
|
}
|
|
4299
5788
|
return { workout };
|
|
4300
5789
|
});
|
|
5790
|
+
app.delete("/api/v1/health/workouts/:id", async (request, reply) => {
|
|
5791
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/health/workouts/:id" });
|
|
5792
|
+
const { id } = request.params;
|
|
5793
|
+
const workout = deleteWorkoutSession(id, toActivityContext(auth));
|
|
5794
|
+
if (!workout) {
|
|
5795
|
+
reply.code(404);
|
|
5796
|
+
return { error: "Workout session not found" };
|
|
5797
|
+
}
|
|
5798
|
+
return { workout };
|
|
5799
|
+
});
|
|
4301
5800
|
app.patch("/api/v1/health/sleep/:id", async (request, reply) => {
|
|
4302
5801
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/health/sleep/:id" });
|
|
4303
5802
|
const { id } = request.params;
|
|
@@ -4308,6 +5807,16 @@ export async function buildServer(options = {}) {
|
|
|
4308
5807
|
}
|
|
4309
5808
|
return { sleep };
|
|
4310
5809
|
});
|
|
5810
|
+
app.delete("/api/v1/health/sleep/:id", async (request, reply) => {
|
|
5811
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/health/sleep/:id" });
|
|
5812
|
+
const { id } = request.params;
|
|
5813
|
+
const sleep = deleteSleepSession(id, toActivityContext(auth));
|
|
5814
|
+
if (!sleep) {
|
|
5815
|
+
reply.code(404);
|
|
5816
|
+
return { error: "Sleep session not found" };
|
|
5817
|
+
}
|
|
5818
|
+
return { sleep };
|
|
5819
|
+
});
|
|
4311
5820
|
app.get("/api/v1/operator/context", async (request) => {
|
|
4312
5821
|
requireOperatorSession(request.headers, {
|
|
4313
5822
|
route: "/api/v1/operator/context"
|
|
@@ -4355,6 +5864,26 @@ export async function buildServer(options = {}) {
|
|
|
4355
5864
|
const userIds = resolveScopedUserIds(request.query);
|
|
4356
5865
|
return getQuestionnaireInstrumentDetail(id, { userIds });
|
|
4357
5866
|
});
|
|
5867
|
+
app.patch("/api/v1/psyche/questionnaires/:id", async (request, reply) => {
|
|
5868
|
+
const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/questionnaires/:id" });
|
|
5869
|
+
const { id } = request.params;
|
|
5870
|
+
const instrument = updateQuestionnaireInstrument(id, updateQuestionnaireInstrumentSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
5871
|
+
if (!instrument) {
|
|
5872
|
+
reply.code(404);
|
|
5873
|
+
return { error: "Questionnaire instrument not found" };
|
|
5874
|
+
}
|
|
5875
|
+
return { instrument };
|
|
5876
|
+
});
|
|
5877
|
+
app.delete("/api/v1/psyche/questionnaires/:id", async (request, reply) => {
|
|
5878
|
+
const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/questionnaires/:id" });
|
|
5879
|
+
const { id } = request.params;
|
|
5880
|
+
const instrument = deleteQuestionnaireInstrument(id, toActivityContext(auth));
|
|
5881
|
+
if (!instrument) {
|
|
5882
|
+
reply.code(404);
|
|
5883
|
+
return { error: "Questionnaire instrument not found" };
|
|
5884
|
+
}
|
|
5885
|
+
return { instrument };
|
|
5886
|
+
});
|
|
4358
5887
|
app.post("/api/v1/psyche/questionnaires/:id/clone", async (request, reply) => {
|
|
4359
5888
|
const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/questionnaires/:id/clone" });
|
|
4360
5889
|
const { id } = request.params;
|
|
@@ -4420,6 +5949,29 @@ export async function buildServer(options = {}) {
|
|
|
4420
5949
|
})
|
|
4421
5950
|
};
|
|
4422
5951
|
});
|
|
5952
|
+
app.get("/api/v1/psyche/self-observation/calendar/export", async (request, reply) => {
|
|
5953
|
+
requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/self-observation/calendar/export" });
|
|
5954
|
+
const query = psycheObservationCalendarExportQuerySchema.parse(request.query ?? {});
|
|
5955
|
+
const now = new Date();
|
|
5956
|
+
const from = query.from ??
|
|
5957
|
+
new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
5958
|
+
const to = query.to ??
|
|
5959
|
+
new Date(now.getTime() + 21 * 24 * 60 * 60 * 1000).toISOString();
|
|
5960
|
+
const exported = exportPsycheObservationCalendar({
|
|
5961
|
+
from,
|
|
5962
|
+
to,
|
|
5963
|
+
userIds: query.userIds,
|
|
5964
|
+
format: query.format,
|
|
5965
|
+
tags: query.tags,
|
|
5966
|
+
includeObservations: query.includeObservations,
|
|
5967
|
+
includeActivity: query.includeActivity,
|
|
5968
|
+
onlyHumanOwned: query.onlyHumanOwned,
|
|
5969
|
+
search: query.search
|
|
5970
|
+
});
|
|
5971
|
+
reply.header("Content-Disposition", `attachment; filename="${exported.fileName}"`);
|
|
5972
|
+
reply.type(exported.mimeType);
|
|
5973
|
+
return reply.send(exported.body);
|
|
5974
|
+
});
|
|
4423
5975
|
app.get("/api/v1/psyche/values", async (request) => {
|
|
4424
5976
|
requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/values" });
|
|
4425
5977
|
const userIds = resolveScopedUserIds(request.query);
|
|
@@ -5329,6 +6881,7 @@ export async function buildServer(options = {}) {
|
|
|
5329
6881
|
plannedDurationSeconds: null,
|
|
5330
6882
|
schedulingRules: null,
|
|
5331
6883
|
tagIds: readStringArrayField(suggestedFields, "tagIds"),
|
|
6884
|
+
actionCostBand: "standard",
|
|
5332
6885
|
notes: []
|
|
5333
6886
|
}, toActivityContext(auth));
|
|
5334
6887
|
return { entityType: "task", entityId: task.id };
|
|
@@ -5557,7 +7110,9 @@ export async function buildServer(options = {}) {
|
|
|
5557
7110
|
return job;
|
|
5558
7111
|
});
|
|
5559
7112
|
app.post("/api/v1/wiki/ingest-jobs/:id/rerun", async (request, reply) => {
|
|
5560
|
-
requireScopedAccess(request.headers, ["write"], {
|
|
7113
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
7114
|
+
route: "/api/v1/wiki/ingest-jobs/:id/rerun"
|
|
7115
|
+
});
|
|
5561
7116
|
const { id } = request.params;
|
|
5562
7117
|
try {
|
|
5563
7118
|
const result = await rerunWikiIngestJob(id, {
|
|
@@ -5584,7 +7139,9 @@ export async function buildServer(options = {}) {
|
|
|
5584
7139
|
}
|
|
5585
7140
|
});
|
|
5586
7141
|
app.post("/api/v1/wiki/ingest-jobs/:id/resume", async (request, reply) => {
|
|
5587
|
-
requireScopedAccess(request.headers, ["write"], {
|
|
7142
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
7143
|
+
route: "/api/v1/wiki/ingest-jobs/:id/resume"
|
|
7144
|
+
});
|
|
5588
7145
|
const { id } = request.params;
|
|
5589
7146
|
const job = getWikiIngestJob(id);
|
|
5590
7147
|
if (!job) {
|
|
@@ -5614,7 +7171,9 @@ export async function buildServer(options = {}) {
|
|
|
5614
7171
|
};
|
|
5615
7172
|
});
|
|
5616
7173
|
app.delete("/api/v1/wiki/ingest-jobs/:id", async (request, reply) => {
|
|
5617
|
-
requireScopedAccess(request.headers, ["write"], {
|
|
7174
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
7175
|
+
route: "/api/v1/wiki/ingest-jobs/:id"
|
|
7176
|
+
});
|
|
5618
7177
|
const { id } = request.params;
|
|
5619
7178
|
try {
|
|
5620
7179
|
const deleted = deleteWikiIngestJob(id);
|
|
@@ -5716,8 +7275,8 @@ export async function buildServer(options = {}) {
|
|
|
5716
7275
|
route: "/api/v1/calendar/oauth/google/start"
|
|
5717
7276
|
});
|
|
5718
7277
|
return await startGoogleCalendarOauth(startGoogleCalendarOauthSchema.parse(request.body ?? {}), {
|
|
5719
|
-
browserOrigin: typeof request.body
|
|
5720
|
-
"string"
|
|
7278
|
+
browserOrigin: typeof request.body
|
|
7279
|
+
?.browserOrigin === "string"
|
|
5721
7280
|
? request.body.browserOrigin
|
|
5722
7281
|
: null,
|
|
5723
7282
|
openerOrigin: typeof request.headers.origin === "string"
|
|
@@ -5995,6 +7554,9 @@ export async function buildServer(options = {}) {
|
|
|
5995
7554
|
workspace: startPreferenceGame(startPreferenceGameSchema.parse(request.body ?? {}))
|
|
5996
7555
|
};
|
|
5997
7556
|
});
|
|
7557
|
+
app.get("/api/v1/preferences/catalogs", async () => ({
|
|
7558
|
+
catalogs: listPreferenceCatalogs()
|
|
7559
|
+
}));
|
|
5998
7560
|
app.post("/api/v1/preferences/catalogs", async (request, reply) => {
|
|
5999
7561
|
requireScopedAccess(request.headers, ["write"], {
|
|
6000
7562
|
route: "/api/v1/preferences/catalogs"
|
|
@@ -6003,6 +7565,15 @@ export async function buildServer(options = {}) {
|
|
|
6003
7565
|
reply.code(201);
|
|
6004
7566
|
return { catalog };
|
|
6005
7567
|
});
|
|
7568
|
+
app.get("/api/v1/preferences/catalogs/:id", async (request, reply) => {
|
|
7569
|
+
const { id } = request.params;
|
|
7570
|
+
const catalog = getPreferenceCatalogById(id);
|
|
7571
|
+
if (!catalog) {
|
|
7572
|
+
reply.code(404);
|
|
7573
|
+
return { error: "Preferences catalog not found" };
|
|
7574
|
+
}
|
|
7575
|
+
return { catalog };
|
|
7576
|
+
});
|
|
6006
7577
|
app.patch("/api/v1/preferences/catalogs/:id", async (request) => {
|
|
6007
7578
|
requireScopedAccess(request.headers, ["write"], {
|
|
6008
7579
|
route: "/api/v1/preferences/catalogs/:id"
|
|
@@ -6019,6 +7590,9 @@ export async function buildServer(options = {}) {
|
|
|
6019
7590
|
const { id } = request.params;
|
|
6020
7591
|
return { catalog: deletePreferenceCatalog(id) };
|
|
6021
7592
|
});
|
|
7593
|
+
app.get("/api/v1/preferences/catalog-items", async () => ({
|
|
7594
|
+
items: listPreferenceCatalogItems()
|
|
7595
|
+
}));
|
|
6022
7596
|
app.post("/api/v1/preferences/catalog-items", async (request, reply) => {
|
|
6023
7597
|
requireScopedAccess(request.headers, ["write"], {
|
|
6024
7598
|
route: "/api/v1/preferences/catalog-items"
|
|
@@ -6027,6 +7601,15 @@ export async function buildServer(options = {}) {
|
|
|
6027
7601
|
reply.code(201);
|
|
6028
7602
|
return { item };
|
|
6029
7603
|
});
|
|
7604
|
+
app.get("/api/v1/preferences/catalog-items/:id", async (request, reply) => {
|
|
7605
|
+
const { id } = request.params;
|
|
7606
|
+
const item = getPreferenceCatalogItemById(id);
|
|
7607
|
+
if (!item) {
|
|
7608
|
+
reply.code(404);
|
|
7609
|
+
return { error: "Preferences catalog item not found" };
|
|
7610
|
+
}
|
|
7611
|
+
return { item };
|
|
7612
|
+
});
|
|
6030
7613
|
app.patch("/api/v1/preferences/catalog-items/:id", async (request) => {
|
|
6031
7614
|
requireScopedAccess(request.headers, ["write"], {
|
|
6032
7615
|
route: "/api/v1/preferences/catalog-items/:id"
|
|
@@ -6043,6 +7626,9 @@ export async function buildServer(options = {}) {
|
|
|
6043
7626
|
const { id } = request.params;
|
|
6044
7627
|
return { item: deletePreferenceCatalogItem(id) };
|
|
6045
7628
|
});
|
|
7629
|
+
app.get("/api/v1/preferences/contexts", async () => ({
|
|
7630
|
+
contexts: listPreferenceContexts()
|
|
7631
|
+
}));
|
|
6046
7632
|
app.post("/api/v1/preferences/contexts", async (request, reply) => {
|
|
6047
7633
|
requireScopedAccess(request.headers, ["write"], {
|
|
6048
7634
|
route: "/api/v1/preferences/contexts"
|
|
@@ -6051,6 +7637,15 @@ export async function buildServer(options = {}) {
|
|
|
6051
7637
|
reply.code(201);
|
|
6052
7638
|
return { context };
|
|
6053
7639
|
});
|
|
7640
|
+
app.get("/api/v1/preferences/contexts/:id", async (request, reply) => {
|
|
7641
|
+
const { id } = request.params;
|
|
7642
|
+
const context = getPreferenceContextById(id);
|
|
7643
|
+
if (!context) {
|
|
7644
|
+
reply.code(404);
|
|
7645
|
+
return { error: "Preferences context not found" };
|
|
7646
|
+
}
|
|
7647
|
+
return { context };
|
|
7648
|
+
});
|
|
6054
7649
|
app.patch("/api/v1/preferences/contexts/:id", async (request) => {
|
|
6055
7650
|
requireScopedAccess(request.headers, ["write"], {
|
|
6056
7651
|
route: "/api/v1/preferences/contexts/:id"
|
|
@@ -6060,6 +7655,13 @@ export async function buildServer(options = {}) {
|
|
|
6060
7655
|
context: updatePreferenceContext(id, updatePreferenceContextSchema.parse(request.body ?? {}))
|
|
6061
7656
|
};
|
|
6062
7657
|
});
|
|
7658
|
+
app.delete("/api/v1/preferences/contexts/:id", async (request) => {
|
|
7659
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
7660
|
+
route: "/api/v1/preferences/contexts/:id"
|
|
7661
|
+
});
|
|
7662
|
+
const { id } = request.params;
|
|
7663
|
+
return { context: deletePreferenceContext(id) };
|
|
7664
|
+
});
|
|
6063
7665
|
app.post("/api/v1/preferences/contexts/merge", async (request) => {
|
|
6064
7666
|
requireScopedAccess(request.headers, ["write"], {
|
|
6065
7667
|
route: "/api/v1/preferences/contexts/merge"
|
|
@@ -6068,6 +7670,9 @@ export async function buildServer(options = {}) {
|
|
|
6068
7670
|
merge: mergePreferenceContexts(mergePreferenceContextsSchema.parse(request.body ?? {}))
|
|
6069
7671
|
};
|
|
6070
7672
|
});
|
|
7673
|
+
app.get("/api/v1/preferences/items", async () => ({
|
|
7674
|
+
items: listPreferenceItems()
|
|
7675
|
+
}));
|
|
6071
7676
|
app.post("/api/v1/preferences/items", async (request, reply) => {
|
|
6072
7677
|
requireScopedAccess(request.headers, ["write"], {
|
|
6073
7678
|
route: "/api/v1/preferences/items"
|
|
@@ -6076,6 +7681,15 @@ export async function buildServer(options = {}) {
|
|
|
6076
7681
|
reply.code(201);
|
|
6077
7682
|
return { item };
|
|
6078
7683
|
});
|
|
7684
|
+
app.get("/api/v1/preferences/items/:id", async (request, reply) => {
|
|
7685
|
+
const { id } = request.params;
|
|
7686
|
+
const item = getPreferenceItemById(id);
|
|
7687
|
+
if (!item) {
|
|
7688
|
+
reply.code(404);
|
|
7689
|
+
return { error: "Preferences item not found" };
|
|
7690
|
+
}
|
|
7691
|
+
return { item };
|
|
7692
|
+
});
|
|
6079
7693
|
app.patch("/api/v1/preferences/items/:id", async (request) => {
|
|
6080
7694
|
requireScopedAccess(request.headers, ["write"], {
|
|
6081
7695
|
route: "/api/v1/preferences/items/:id"
|
|
@@ -6085,6 +7699,13 @@ export async function buildServer(options = {}) {
|
|
|
6085
7699
|
item: updatePreferenceItem(id, updatePreferenceItemSchema.parse(request.body ?? {}))
|
|
6086
7700
|
};
|
|
6087
7701
|
});
|
|
7702
|
+
app.delete("/api/v1/preferences/items/:id", async (request) => {
|
|
7703
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
7704
|
+
route: "/api/v1/preferences/items/:id"
|
|
7705
|
+
});
|
|
7706
|
+
const { id } = request.params;
|
|
7707
|
+
return { item: deletePreferenceItem(id) };
|
|
7708
|
+
});
|
|
6088
7709
|
app.post("/api/v1/preferences/items/from-entity", async (request, reply) => {
|
|
6089
7710
|
requireScopedAccess(request.headers, ["write"], {
|
|
6090
7711
|
route: "/api/v1/preferences/items/from-entity"
|
|
@@ -6400,6 +8021,59 @@ export async function buildServer(options = {}) {
|
|
|
6400
8021
|
requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/settings/bin" });
|
|
6401
8022
|
return { bin: getSettingsBinPayload() };
|
|
6402
8023
|
});
|
|
8024
|
+
app.get("/api/v1/settings/data", async (request) => {
|
|
8025
|
+
requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/settings/data" });
|
|
8026
|
+
return { data: await getDataManagementState() };
|
|
8027
|
+
});
|
|
8028
|
+
app.patch("/api/v1/settings/data", async (request) => {
|
|
8029
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
8030
|
+
route: "/api/v1/settings/data"
|
|
8031
|
+
});
|
|
8032
|
+
return {
|
|
8033
|
+
settings: await updateDataManagementSettings(updateDataManagementSettingsSchema.parse(request.body ?? {})),
|
|
8034
|
+
data: await getDataManagementState()
|
|
8035
|
+
};
|
|
8036
|
+
});
|
|
8037
|
+
app.post("/api/v1/settings/data/scan", async (request) => {
|
|
8038
|
+
requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/settings/data/scan" });
|
|
8039
|
+
return { candidates: await scanForDataRecoveryCandidates() };
|
|
8040
|
+
});
|
|
8041
|
+
app.post("/api/v1/settings/data/backups", async (request, reply) => {
|
|
8042
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
8043
|
+
route: "/api/v1/settings/data/backups"
|
|
8044
|
+
});
|
|
8045
|
+
const backup = await createDataBackup(createDataBackupSchema.parse(request.body ?? {}));
|
|
8046
|
+
reply.code(201);
|
|
8047
|
+
return {
|
|
8048
|
+
backup,
|
|
8049
|
+
data: await getDataManagementState()
|
|
8050
|
+
};
|
|
8051
|
+
});
|
|
8052
|
+
app.post("/api/v1/settings/data/backups/:id/restore", async (request) => {
|
|
8053
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
8054
|
+
route: "/api/v1/settings/data/backups/:id/restore"
|
|
8055
|
+
});
|
|
8056
|
+
const { id } = request.params;
|
|
8057
|
+
return {
|
|
8058
|
+
data: await restoreDataBackup(id, restoreDataBackupSchema.parse(request.body ?? {}), { secretsManager: managers.secrets })
|
|
8059
|
+
};
|
|
8060
|
+
});
|
|
8061
|
+
app.post("/api/v1/settings/data/switch-root", async (request) => {
|
|
8062
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
8063
|
+
route: "/api/v1/settings/data/switch-root"
|
|
8064
|
+
});
|
|
8065
|
+
return {
|
|
8066
|
+
data: await switchDataRoot(switchDataRootSchema.parse(request.body ?? {}), { secretsManager: managers.secrets })
|
|
8067
|
+
};
|
|
8068
|
+
});
|
|
8069
|
+
app.get("/api/v1/settings/data/export", async (request, reply) => {
|
|
8070
|
+
requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/settings/data/export" });
|
|
8071
|
+
const query = dataExportQuerySchema.parse(request.query ?? {});
|
|
8072
|
+
const exported = await exportData(query.format);
|
|
8073
|
+
reply.header("Content-Disposition", `attachment; filename="${exported.fileName}"`);
|
|
8074
|
+
reply.type(exported.mimeType);
|
|
8075
|
+
return reply.send(exported.body);
|
|
8076
|
+
});
|
|
6403
8077
|
app.post("/api/v1/projects", async (request, reply) => {
|
|
6404
8078
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/projects" });
|
|
6405
8079
|
const project = createProject(createProjectSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
@@ -6488,6 +8162,15 @@ export async function buildServer(options = {}) {
|
|
|
6488
8162
|
userIds: resolveScopedUserIds(request.query)
|
|
6489
8163
|
})
|
|
6490
8164
|
}));
|
|
8165
|
+
app.get("/api/v1/calendar/work-block-templates/:id", async (request, reply) => {
|
|
8166
|
+
const { id } = request.params;
|
|
8167
|
+
const template = getWorkBlockTemplateById(id);
|
|
8168
|
+
if (!template) {
|
|
8169
|
+
reply.code(404);
|
|
8170
|
+
return { error: "Work block template not found" };
|
|
8171
|
+
}
|
|
8172
|
+
return { template };
|
|
8173
|
+
});
|
|
6491
8174
|
app.post("/api/v1/calendar/work-block-templates", async (request, reply) => {
|
|
6492
8175
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/work-block-templates" });
|
|
6493
8176
|
const template = createWorkBlockTemplate(createWorkBlockTemplateSchema.parse(request.body ?? {}));
|
|
@@ -6563,6 +8246,15 @@ export async function buildServer(options = {}) {
|
|
|
6563
8246
|
timeboxes: listTaskTimeboxes({ from, to, userIds: query.userIds })
|
|
6564
8247
|
};
|
|
6565
8248
|
});
|
|
8249
|
+
app.get("/api/v1/calendar/timeboxes/:id", async (request, reply) => {
|
|
8250
|
+
const { id } = request.params;
|
|
8251
|
+
const timebox = getTaskTimeboxById(id);
|
|
8252
|
+
if (!timebox) {
|
|
8253
|
+
reply.code(404);
|
|
8254
|
+
return { error: "Task timebox not found" };
|
|
8255
|
+
}
|
|
8256
|
+
return { timebox };
|
|
8257
|
+
});
|
|
6566
8258
|
app.post("/api/v1/calendar/timeboxes", async (request, reply) => {
|
|
6567
8259
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/timeboxes" });
|
|
6568
8260
|
const timebox = createTaskTimebox(createTaskTimeboxSchema.parse(request.body ?? {}));
|
|
@@ -6637,6 +8329,17 @@ export async function buildServer(options = {}) {
|
|
|
6637
8329
|
})
|
|
6638
8330
|
};
|
|
6639
8331
|
});
|
|
8332
|
+
app.get("/api/v1/calendar/events", async (request) => {
|
|
8333
|
+
const query = calendarOverviewQuerySchema.parse(request.query ?? {});
|
|
8334
|
+
const now = new Date();
|
|
8335
|
+
const from = query.from ??
|
|
8336
|
+
new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
8337
|
+
const to = query.to ??
|
|
8338
|
+
new Date(now.getTime() + 21 * 24 * 60 * 60 * 1000).toISOString();
|
|
8339
|
+
return {
|
|
8340
|
+
events: listCalendarEvents({ from, to, userIds: query.userIds })
|
|
8341
|
+
};
|
|
8342
|
+
});
|
|
6640
8343
|
app.post("/api/v1/calendar/events", async (request, reply) => {
|
|
6641
8344
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/events" });
|
|
6642
8345
|
const event = createCalendarEvent(createCalendarEventSchema.parse(request.body ?? {}));
|
|
@@ -6658,6 +8361,15 @@ export async function buildServer(options = {}) {
|
|
|
6658
8361
|
reply.code(201);
|
|
6659
8362
|
return { event: refreshed };
|
|
6660
8363
|
});
|
|
8364
|
+
app.get("/api/v1/calendar/events/:id", async (request, reply) => {
|
|
8365
|
+
const { id } = request.params;
|
|
8366
|
+
const event = getCalendarEventById(id);
|
|
8367
|
+
if (!event) {
|
|
8368
|
+
reply.code(404);
|
|
8369
|
+
return { error: "Calendar event not found" };
|
|
8370
|
+
}
|
|
8371
|
+
return { event };
|
|
8372
|
+
});
|
|
6661
8373
|
app.patch("/api/v1/calendar/events/:id", async (request, reply) => {
|
|
6662
8374
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/events/:id" });
|
|
6663
8375
|
const { id } = request.params;
|
|
@@ -6829,6 +8541,7 @@ export async function buildServer(options = {}) {
|
|
|
6829
8541
|
}
|
|
6830
8542
|
}, { secrets: managers.secrets });
|
|
6831
8543
|
}
|
|
8544
|
+
mirrorSettingsFileFromCurrentState();
|
|
6832
8545
|
reply.code(201);
|
|
6833
8546
|
return { connection };
|
|
6834
8547
|
});
|
|
@@ -6839,6 +8552,7 @@ export async function buildServer(options = {}) {
|
|
|
6839
8552
|
reply.code(404);
|
|
6840
8553
|
return { error: "AI model connection not found" };
|
|
6841
8554
|
}
|
|
8555
|
+
mirrorSettingsFileFromCurrentState();
|
|
6842
8556
|
return { deletedId };
|
|
6843
8557
|
});
|
|
6844
8558
|
app.post("/api/v1/settings/models/connections/test", async (request, reply) => {
|
|
@@ -6869,7 +8583,9 @@ export async function buildServer(options = {}) {
|
|
|
6869
8583
|
recordDiagnosticLog({
|
|
6870
8584
|
level,
|
|
6871
8585
|
source: normalizeDiagnosticSource(request.headers["x-forge-source"]),
|
|
6872
|
-
scope: typeof details.scope === "string"
|
|
8586
|
+
scope: typeof details.scope === "string"
|
|
8587
|
+
? details.scope
|
|
8588
|
+
: "model_settings",
|
|
6873
8589
|
eventKey: typeof details.eventKey === "string"
|
|
6874
8590
|
? details.eventKey
|
|
6875
8591
|
: "model_connection_test",
|
|
@@ -6905,7 +8621,9 @@ export async function buildServer(options = {}) {
|
|
|
6905
8621
|
}
|
|
6906
8622
|
});
|
|
6907
8623
|
app.post("/api/v1/settings/models/oauth/openai-codex/session/:id/manual", async (request) => {
|
|
6908
|
-
requireScopedAccess(request.headers, ["write"], {
|
|
8624
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
8625
|
+
route: "/api/v1/settings/models/oauth/openai-codex/session/:id/manual"
|
|
8626
|
+
});
|
|
6909
8627
|
return {
|
|
6910
8628
|
session: submitOpenAiCodexOauthManualInput(request.params.id, submitOpenAiCodexOauthManualCodeSchema.parse(request.body ?? {})
|
|
6911
8629
|
.codeOrUrl)
|
|
@@ -7175,6 +8893,87 @@ export async function buildServer(options = {}) {
|
|
|
7175
8893
|
runs: listAiConnectorRuns(connector.id)
|
|
7176
8894
|
};
|
|
7177
8895
|
});
|
|
8896
|
+
app.get(`${basePath}/:id/runs/:runId`, async (request, reply) => {
|
|
8897
|
+
requireScopedAccess(request.headers, ["read"], {
|
|
8898
|
+
route: `${basePath}/:id/runs/:runId`
|
|
8899
|
+
});
|
|
8900
|
+
const params = request.params;
|
|
8901
|
+
const connector = getAiConnectorById(params.id);
|
|
8902
|
+
if (!connector) {
|
|
8903
|
+
reply.code(404);
|
|
8904
|
+
return { error: `${noun} not found` };
|
|
8905
|
+
}
|
|
8906
|
+
const run = getAiConnectorRunById(connector.id, params.runId);
|
|
8907
|
+
if (!run) {
|
|
8908
|
+
reply.code(404);
|
|
8909
|
+
return { error: `${noun} run not found` };
|
|
8910
|
+
}
|
|
8911
|
+
return {
|
|
8912
|
+
[singularKey]: connector,
|
|
8913
|
+
run
|
|
8914
|
+
};
|
|
8915
|
+
});
|
|
8916
|
+
app.get(`${basePath}/:id/runs/:runId/nodes`, async (request, reply) => {
|
|
8917
|
+
requireScopedAccess(request.headers, ["read"], {
|
|
8918
|
+
route: `${basePath}/:id/runs/:runId/nodes`
|
|
8919
|
+
});
|
|
8920
|
+
const params = request.params;
|
|
8921
|
+
const connector = getAiConnectorById(params.id);
|
|
8922
|
+
if (!connector) {
|
|
8923
|
+
reply.code(404);
|
|
8924
|
+
return { error: `${noun} not found` };
|
|
8925
|
+
}
|
|
8926
|
+
const nodeResults = getAiConnectorRunNodeResults(connector.id, params.runId);
|
|
8927
|
+
if (!nodeResults) {
|
|
8928
|
+
reply.code(404);
|
|
8929
|
+
return { error: `${noun} run not found` };
|
|
8930
|
+
}
|
|
8931
|
+
return {
|
|
8932
|
+
[singularKey]: connector,
|
|
8933
|
+
nodeResults
|
|
8934
|
+
};
|
|
8935
|
+
});
|
|
8936
|
+
app.get(`${basePath}/:id/runs/:runId/nodes/:nodeId`, async (request, reply) => {
|
|
8937
|
+
requireScopedAccess(request.headers, ["read"], {
|
|
8938
|
+
route: `${basePath}/:id/runs/:runId/nodes/:nodeId`
|
|
8939
|
+
});
|
|
8940
|
+
const params = request.params;
|
|
8941
|
+
const connector = getAiConnectorById(params.id);
|
|
8942
|
+
if (!connector) {
|
|
8943
|
+
reply.code(404);
|
|
8944
|
+
return { error: `${noun} not found` };
|
|
8945
|
+
}
|
|
8946
|
+
const nodeResult = getAiConnectorRunNodeResult(connector.id, params.runId, params.nodeId);
|
|
8947
|
+
if (!nodeResult) {
|
|
8948
|
+
reply.code(404);
|
|
8949
|
+
return { error: `${noun} node result not found` };
|
|
8950
|
+
}
|
|
8951
|
+
return {
|
|
8952
|
+
[singularKey]: connector,
|
|
8953
|
+
nodeResult
|
|
8954
|
+
};
|
|
8955
|
+
});
|
|
8956
|
+
app.get(`${basePath}/:id/nodes/:nodeId/output`, async (request, reply) => {
|
|
8957
|
+
requireScopedAccess(request.headers, ["read"], {
|
|
8958
|
+
route: `${basePath}/:id/nodes/:nodeId/output`
|
|
8959
|
+
});
|
|
8960
|
+
const params = request.params;
|
|
8961
|
+
const connector = getAiConnectorById(params.id);
|
|
8962
|
+
if (!connector) {
|
|
8963
|
+
reply.code(404);
|
|
8964
|
+
return { error: `${noun} not found` };
|
|
8965
|
+
}
|
|
8966
|
+
const latest = getLatestAiConnectorNodeOutput(connector.id, params.nodeId);
|
|
8967
|
+
if (!latest) {
|
|
8968
|
+
reply.code(404);
|
|
8969
|
+
return { error: `${noun} node output not found` };
|
|
8970
|
+
}
|
|
8971
|
+
return {
|
|
8972
|
+
[singularKey]: connector,
|
|
8973
|
+
run: latest.run,
|
|
8974
|
+
nodeResult: latest.nodeResult
|
|
8975
|
+
};
|
|
8976
|
+
});
|
|
7178
8977
|
};
|
|
7179
8978
|
registerFlowApiRoutes("/api/v1/workbench/flows", "Workbench flow", {
|
|
7180
8979
|
collectionKey: "flows",
|
|
@@ -7577,6 +9376,16 @@ export async function buildServer(options = {}) {
|
|
|
7577
9376
|
}
|
|
7578
9377
|
return { task };
|
|
7579
9378
|
});
|
|
9379
|
+
app.post("/api/v1/tasks/:id/split", async (request, reply) => {
|
|
9380
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/tasks/:id/split" });
|
|
9381
|
+
const { id } = request.params;
|
|
9382
|
+
const result = splitTask(id, taskSplitCreateSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
9383
|
+
if (!result) {
|
|
9384
|
+
reply.code(404);
|
|
9385
|
+
return { error: "Task not found" };
|
|
9386
|
+
}
|
|
9387
|
+
return result;
|
|
9388
|
+
});
|
|
7580
9389
|
app.delete("/api/v1/tasks/:id", async (request, reply) => {
|
|
7581
9390
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/tasks/:id" });
|
|
7582
9391
|
const { id } = request.params;
|