forge-openclaw-plugin 0.2.15 → 0.2.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +39 -4
- package/dist/assets/{board-C_m78kvK.js → board-8L3uX7_O.js} +2 -2
- package/dist/assets/{board-C_m78kvK.js.map → board-8L3uX7_O.js.map} +1 -1
- package/dist/assets/index-Cj1IBH_w.js +36 -0
- package/dist/assets/index-Cj1IBH_w.js.map +1 -0
- package/dist/assets/index-DQT6EbuS.css +1 -0
- package/dist/assets/{motion-CpZvZumD.js → motion-1GAqqi8M.js} +2 -2
- package/dist/assets/{motion-CpZvZumD.js.map → motion-1GAqqi8M.js.map} +1 -1
- package/dist/assets/{table-DtyXTw03.js → table-DBGlgRjk.js} +2 -2
- package/dist/assets/{table-DtyXTw03.js.map → table-DBGlgRjk.js.map} +1 -1
- package/dist/assets/{ui-BXbpiKyS.js → ui-iTluWjC4.js} +2 -2
- package/dist/assets/{ui-BXbpiKyS.js.map → ui-iTluWjC4.js.map} +1 -1
- package/dist/assets/{vendor-QBH6qVEe.js → vendor-BvM2F9Dp.js} +151 -81
- package/dist/assets/vendor-BvM2F9Dp.js.map +1 -0
- package/dist/assets/{viz-w-IMeueL.js → viz-CNeunkfu.js} +2 -2
- package/dist/assets/{viz-w-IMeueL.js.map → viz-CNeunkfu.js.map} +1 -1
- package/dist/index.html +8 -8
- package/dist/openclaw/local-runtime.js +142 -9
- package/dist/openclaw/parity.js +1 -0
- package/dist/openclaw/plugin-entry-shared.js +7 -1
- package/dist/openclaw/routes.js +7 -0
- package/dist/openclaw/tools.js +198 -16
- package/dist/server/app.js +2615 -251
- package/dist/server/managers/platform/secrets-manager.js +44 -1
- package/dist/server/managers/runtime.js +3 -1
- package/dist/server/openapi.js +2212 -170
- package/dist/server/repositories/calendar.js +1101 -0
- package/dist/server/repositories/deleted-entities.js +10 -2
- package/dist/server/repositories/habits.js +358 -0
- package/dist/server/repositories/notes.js +161 -28
- package/dist/server/repositories/projects.js +45 -13
- package/dist/server/repositories/rewards.js +176 -6
- package/dist/server/repositories/settings.js +47 -5
- package/dist/server/repositories/task-runs.js +46 -10
- package/dist/server/repositories/tasks.js +25 -9
- package/dist/server/repositories/weekly-reviews.js +109 -0
- package/dist/server/repositories/work-adjustments.js +105 -0
- package/dist/server/services/calendar-runtime.js +1301 -0
- package/dist/server/services/context.js +16 -6
- package/dist/server/services/dashboard.js +6 -3
- package/dist/server/services/entity-crud.js +116 -3
- package/dist/server/services/gamification.js +66 -18
- package/dist/server/services/insights.js +2 -1
- package/dist/server/services/projects.js +32 -8
- package/dist/server/services/reviews.js +17 -2
- package/dist/server/services/work-time.js +27 -0
- package/dist/server/types.js +1069 -45
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/server/migrations/003_habits.sql +30 -0
- package/server/migrations/004_habit_links.sql +8 -0
- package/server/migrations/005_habit_psyche_links.sql +24 -0
- package/server/migrations/006_work_adjustments.sql +14 -0
- package/server/migrations/007_weekly_review_closures.sql +17 -0
- package/server/migrations/008_calendar_execution.sql +147 -0
- package/server/migrations/009_true_calendar_events.sql +195 -0
- package/server/migrations/010_calendar_selection_state.sql +6 -0
- package/server/migrations/011_calendar_timezone_backfill.sql +11 -0
- package/server/migrations/012_work_block_ranges.sql +7 -0
- package/server/migrations/013_microsoft_local_auth_settings.sql +8 -0
- package/server/migrations/014_note_tags_and_ephemeral.sql +8 -0
- package/skills/forge-openclaw/SKILL.md +130 -10
- package/skills/forge-openclaw/cron_jobs.md +395 -0
- package/dist/assets/index-BWtLtXwb.js +0 -36
- package/dist/assets/index-BWtLtXwb.js.map +0 -1
- package/dist/assets/index-Dp5GXY_z.css +0 -1
- package/dist/assets/vendor-QBH6qVEe.js.map +0 -1
package/dist/server/app.js
CHANGED
|
@@ -1,33 +1,38 @@
|
|
|
1
1
|
import Fastify from "fastify";
|
|
2
2
|
import cors from "@fastify/cors";
|
|
3
3
|
import { ZodError } from "zod";
|
|
4
|
-
import { configureDatabase, configureDatabaseSeeding } from "./db.js";
|
|
5
|
-
import { isHttpError } from "./errors.js";
|
|
6
|
-
import { listActivityEvents, listActivityEventsForTask, removeActivityEvent } from "./repositories/activity-events.js";
|
|
4
|
+
import { configureDatabase, configureDatabaseSeeding, runInTransaction } from "./db.js";
|
|
5
|
+
import { HttpError, isHttpError } from "./errors.js";
|
|
6
|
+
import { listActivityEvents, listActivityEventsForTask, recordActivityEvent, removeActivityEvent } from "./repositories/activity-events.js";
|
|
7
7
|
import { approveApprovalRequest, createAgentAction, createInsight, createInsightFeedback, deleteInsight, getInsightById, listAgentActions, listApprovalRequests, listInsights, rejectApprovalRequest, updateInsight } from "./repositories/collaboration.js";
|
|
8
8
|
import { listEventLog } from "./repositories/event-log.js";
|
|
9
9
|
import { createGoal, getGoalById, listGoals, updateGoal } from "./repositories/goals.js";
|
|
10
|
+
import { createHabit, createHabitCheckIn, getHabitById, listHabits, updateHabit } from "./repositories/habits.js";
|
|
10
11
|
import { listDomains } from "./repositories/domains.js";
|
|
11
12
|
import { buildNotesSummaryByEntity, createNote, getNoteById, listNotes, updateNote } from "./repositories/notes.js";
|
|
12
13
|
import { createBehavior, createBehaviorPattern, createBeliefEntry, createEmotionDefinition, createEventType, createModeGuideSession, createModeProfile, createPsycheValue, createTriggerReport, getBehaviorById, getBehaviorPatternById, getBeliefEntryById, getEmotionDefinitionById, getEventTypeById, getModeGuideSessionById, getModeProfileById, getPsycheValueById, getTriggerReportById, listBehaviors, listBehaviorPatterns, listBeliefEntries, listEmotionDefinitions, listEventTypes, listModeGuideSessions, listModeProfiles, listPsycheValues, listSchemaCatalog, listTriggerReports, updateBehavior, updateBehaviorPattern, updateBeliefEntry, updateEmotionDefinition, updateEventType, updateModeGuideSession, updateModeProfile, updatePsycheValue, updateTriggerReport } from "./repositories/psyche.js";
|
|
13
14
|
import { createProject, updateProject } from "./repositories/projects.js";
|
|
14
|
-
import { createManualRewardGrant, getDailyAmbientXp, getRewardRuleById, listRewardLedger, listRewardRules, recordSessionEvent, updateRewardRule } from "./repositories/rewards.js";
|
|
15
|
+
import { createManualRewardGrant, getDailyAmbientXp, getRewardRuleById, listRewardLedger, listRewardRules, recordWorkAdjustmentReward, recordSessionEvent, updateRewardRule } from "./repositories/rewards.js";
|
|
15
16
|
import { listAgentIdentities, getSettings, isPsycheAuthRequired, updateSettings, verifyAgentToken } from "./repositories/settings.js";
|
|
16
17
|
import { createTag, getTagById, listTags, updateTag } from "./repositories/tags.js";
|
|
17
18
|
import { claimTaskRun, completeTaskRun, focusTaskRun, heartbeatTaskRun, listTaskRuns, recoverTimedOutTaskRuns, releaseTaskRun } from "./repositories/task-runs.js";
|
|
18
19
|
import { createTask, createTaskWithIdempotency, getTaskById, listTasks, uncompleteTask, updateTask } from "./repositories/tasks.js";
|
|
20
|
+
import { createWorkAdjustment } from "./repositories/work-adjustments.js";
|
|
21
|
+
import { createCalendarEvent, createTaskTimebox, createWorkBlockTemplate, deleteCalendarEvent, deleteTaskTimebox, deleteWorkBlockTemplate, getCalendarConnectionById, getCalendarEventById, listCalendars, listCalendarEvents, listTaskTimeboxes, suggestTaskTimeboxes, listWorkBlockInstances, listWorkBlockTemplates, updateCalendarEvent, updateTaskTimebox, updateWorkBlockTemplate } from "./repositories/calendar.js";
|
|
19
22
|
import { getDashboard } from "./services/dashboard.js";
|
|
20
23
|
import { getOverviewContext, getRiskContext, getTodayContext } from "./services/context.js";
|
|
21
24
|
import { buildGamificationOverview, buildGamificationProfile, buildXpMomentumPulse } from "./services/gamification.js";
|
|
22
25
|
import { getInsightsPayload } from "./services/insights.js";
|
|
23
26
|
import { createEntities, deleteEntities, deleteEntity, getSettingsBinPayload, restoreEntities, searchEntities, updateEntities } from "./services/entity-crud.js";
|
|
24
27
|
import { getPsycheOverview } from "./services/psyche.js";
|
|
25
|
-
import { getProjectBoard, listProjectSummaries } from "./services/projects.js";
|
|
28
|
+
import { getProjectBoard, getProjectSummary, listProjectSummaries } from "./services/projects.js";
|
|
26
29
|
import { getWeeklyReviewPayload } from "./services/reviews.js";
|
|
30
|
+
import { finalizeWeeklyReviewClosure } from "./repositories/weekly-reviews.js";
|
|
27
31
|
import { createTaskRunWatchdog } from "./services/task-run-watchdog.js";
|
|
28
32
|
import { suggestTags } from "./services/tagging.js";
|
|
33
|
+
import { CalendarConnectionConflictError, completeMicrosoftCalendarOauth, createCalendarConnection, deleteCalendarEventProjection, discoverCalendarConnection, discoverExistingCalendarConnection, getMicrosoftCalendarOauthSession, listConnectedCalendarConnections, removeCalendarConnection, pushCalendarEventUpdate, readCalendarOverview, syncCalendarConnection, startMicrosoftCalendarOauth, testMicrosoftCalendarOauthConfiguration, listCalendarProviderMetadata, updateCalendarConnectionSelection } from "./services/calendar-runtime.js";
|
|
29
34
|
import { PSYCHE_ENTITY_TYPES, createBehaviorSchema, createBeliefEntrySchema, createBehaviorPatternSchema, createEmotionDefinitionSchema, createEventTypeSchema, createModeGuideSessionSchema, createModeProfileSchema, createPsycheValueSchema, createTriggerReportSchema, updateBehaviorSchema, updateBeliefEntrySchema, updateBehaviorPatternSchema, updateEmotionDefinitionSchema, updateEventTypeSchema, updateModeGuideSessionSchema, updateModeProfileSchema, updatePsycheValueSchema, updateTriggerReportSchema } from "./psyche-types.js";
|
|
30
|
-
import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createSessionEventSchema, createTagSchema, notesListQuerySchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateInsightSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskSchema } from "./types.js";
|
|
35
|
+
import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createCalendarEventSchema, createHabitCheckInSchema, createCalendarConnectionSchema, discoverCalendarConnectionSchema, startMicrosoftCalendarOauthSchema, testMicrosoftCalendarOauthConfigurationSchema, createHabitSchema, createTaskTimeboxSchema, createWorkBlockTemplateSchema, createSessionEventSchema, createWorkAdjustmentSchema, createTagSchema, calendarOverviewQuerySchema, notesListQuerySchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, habitListQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateHabitSchema, updateInsightSchema, updateCalendarConnectionSchema, updateCalendarEventSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskTimeboxSchema, updateTaskSchema, updateWorkBlockTemplateSchema, workAdjustmentResultSchema, finalizeWeeklyReviewResultSchema, recommendTaskTimeboxesSchema } from "./types.js";
|
|
31
36
|
import { buildOpenApiDocument } from "./openapi.js";
|
|
32
37
|
import { registerWebRoutes } from "./web.js";
|
|
33
38
|
import { createManagerRuntime } from "./managers/runtime.js";
|
|
@@ -105,7 +110,9 @@ function readSingleForwardedHeader(value) {
|
|
|
105
110
|
return null;
|
|
106
111
|
}
|
|
107
112
|
function getRequestOrigin(request) {
|
|
108
|
-
const protocol = readSingleForwardedHeader(request.headers["x-forwarded-proto"]) ??
|
|
113
|
+
const protocol = readSingleForwardedHeader(request.headers["x-forwarded-proto"]) ??
|
|
114
|
+
request.protocol ??
|
|
115
|
+
"http";
|
|
109
116
|
const host = readSingleForwardedHeader(request.headers["x-forwarded-host"]) ??
|
|
110
117
|
readSingleForwardedHeader(request.headers.host) ??
|
|
111
118
|
request.hostname;
|
|
@@ -121,20 +128,72 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
121
128
|
"Projects should usually link to one goal through goalId.",
|
|
122
129
|
"Tasks can link directly to a goal when no project exists yet."
|
|
123
130
|
],
|
|
124
|
-
searchHints: [
|
|
131
|
+
searchHints: [
|
|
132
|
+
"Search by title before creating a new goal.",
|
|
133
|
+
"Use status filters when looking for paused or completed goals."
|
|
134
|
+
],
|
|
125
135
|
examples: [
|
|
126
136
|
'{"title":"Create meaningfully","horizon":"lifetime","description":"Make work that is honest, beautiful, and published."}',
|
|
127
137
|
'{"title":"Build a beautiful family","horizon":"lifetime","description":"Invest in love, stability, and shared rituals."}'
|
|
128
138
|
],
|
|
129
139
|
fieldGuide: [
|
|
130
|
-
{
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
{
|
|
137
|
-
|
|
140
|
+
{
|
|
141
|
+
name: "title",
|
|
142
|
+
type: "string",
|
|
143
|
+
required: true,
|
|
144
|
+
description: "Human-readable goal name."
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "description",
|
|
148
|
+
type: "string",
|
|
149
|
+
required: false,
|
|
150
|
+
description: "Markdown description for why the goal matters or what success looks like.",
|
|
151
|
+
defaultValue: ""
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "horizon",
|
|
155
|
+
type: "quarter|year|lifetime",
|
|
156
|
+
required: false,
|
|
157
|
+
description: "How far out the goal is meant to live.",
|
|
158
|
+
enumValues: ["quarter", "year", "lifetime"],
|
|
159
|
+
defaultValue: "year"
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "status",
|
|
163
|
+
type: "active|paused|completed",
|
|
164
|
+
required: false,
|
|
165
|
+
description: "Current lifecycle state for the goal.",
|
|
166
|
+
enumValues: ["active", "paused", "completed"],
|
|
167
|
+
defaultValue: "active"
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
name: "targetPoints",
|
|
171
|
+
type: "integer",
|
|
172
|
+
required: false,
|
|
173
|
+
description: "Approximate XP/point target for the goal.",
|
|
174
|
+
defaultValue: 400
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "themeColor",
|
|
178
|
+
type: "hex-color",
|
|
179
|
+
required: false,
|
|
180
|
+
description: "Visual color used in the UI.",
|
|
181
|
+
defaultValue: "#c8a46b"
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
name: "tagIds",
|
|
185
|
+
type: "string[]",
|
|
186
|
+
required: false,
|
|
187
|
+
description: "Existing tag ids linked to the goal.",
|
|
188
|
+
defaultValue: []
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: "notes",
|
|
192
|
+
type: "Array<{ contentMarkdown, author?, tags?, destroyAt?, links? }>",
|
|
193
|
+
required: false,
|
|
194
|
+
description: "Optional nested notes that will auto-link to the new goal.",
|
|
195
|
+
defaultValue: []
|
|
196
|
+
}
|
|
138
197
|
]
|
|
139
198
|
},
|
|
140
199
|
{
|
|
@@ -146,16 +205,61 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
146
205
|
"Tasks can link to a project through projectId.",
|
|
147
206
|
"Projects inherit strategic meaning from their parent goal."
|
|
148
207
|
],
|
|
149
|
-
searchHints: [
|
|
150
|
-
|
|
208
|
+
searchHints: [
|
|
209
|
+
"Search by title inside the target goal before creating a new project."
|
|
210
|
+
],
|
|
211
|
+
examples: [
|
|
212
|
+
'{"goalId":"goal_create_meaningfully","title":"Launch the public Forge plugin","description":"Ship a real public release that people can install."}'
|
|
213
|
+
],
|
|
151
214
|
fieldGuide: [
|
|
152
|
-
{
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
{
|
|
215
|
+
{
|
|
216
|
+
name: "goalId",
|
|
217
|
+
type: "string",
|
|
218
|
+
required: true,
|
|
219
|
+
description: "Existing parent goal id."
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
name: "title",
|
|
223
|
+
type: "string",
|
|
224
|
+
required: true,
|
|
225
|
+
description: "Project name."
|
|
226
|
+
},
|
|
227
|
+
{
|
|
228
|
+
name: "description",
|
|
229
|
+
type: "string",
|
|
230
|
+
required: false,
|
|
231
|
+
description: "Markdown description for the desired outcome or scope.",
|
|
232
|
+
defaultValue: ""
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
name: "status",
|
|
236
|
+
type: "active|paused|completed",
|
|
237
|
+
required: false,
|
|
238
|
+
description: "Lifecycle state.",
|
|
239
|
+
enumValues: ["active", "paused", "completed"],
|
|
240
|
+
defaultValue: "active"
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: "targetPoints",
|
|
244
|
+
type: "integer",
|
|
245
|
+
required: false,
|
|
246
|
+
description: "Approximate XP/point target for the project.",
|
|
247
|
+
defaultValue: 240
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: "themeColor",
|
|
251
|
+
type: "hex-color",
|
|
252
|
+
required: false,
|
|
253
|
+
description: "Visual color used in the UI.",
|
|
254
|
+
defaultValue: "#c0c1ff"
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
name: "notes",
|
|
258
|
+
type: "Array<{ contentMarkdown, author?, tags?, destroyAt?, links? }>",
|
|
259
|
+
required: false,
|
|
260
|
+
description: "Optional nested notes that will auto-link to the new project.",
|
|
261
|
+
defaultValue: []
|
|
262
|
+
}
|
|
159
263
|
]
|
|
160
264
|
},
|
|
161
265
|
{
|
|
@@ -167,42 +271,604 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
167
271
|
"Live work is tracked by task runs, not by task status alone.",
|
|
168
272
|
"A task status of in_progress does not guarantee a live active run."
|
|
169
273
|
],
|
|
170
|
-
searchHints: [
|
|
171
|
-
|
|
274
|
+
searchHints: [
|
|
275
|
+
"Search by title before creating a duplicate task.",
|
|
276
|
+
"Use linkedTo filters when you know the parent goal or project."
|
|
277
|
+
],
|
|
278
|
+
examples: [
|
|
279
|
+
'{"title":"Write the plugin release notes","projectId":"project_forge_plugin_launch","status":"focus","priority":"high"}'
|
|
280
|
+
],
|
|
281
|
+
fieldGuide: [
|
|
282
|
+
{
|
|
283
|
+
name: "title",
|
|
284
|
+
type: "string",
|
|
285
|
+
required: true,
|
|
286
|
+
description: "Concrete action label."
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: "description",
|
|
290
|
+
type: "string",
|
|
291
|
+
required: false,
|
|
292
|
+
description: "Markdown context, constraints, or acceptance notes.",
|
|
293
|
+
defaultValue: ""
|
|
294
|
+
},
|
|
295
|
+
{
|
|
296
|
+
name: "status",
|
|
297
|
+
type: "backlog|focus|in_progress|blocked|done",
|
|
298
|
+
required: false,
|
|
299
|
+
description: "Board lane or completion state.",
|
|
300
|
+
enumValues: ["backlog", "focus", "in_progress", "blocked", "done"],
|
|
301
|
+
defaultValue: "backlog"
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
name: "priority",
|
|
305
|
+
type: "low|medium|high|critical",
|
|
306
|
+
required: false,
|
|
307
|
+
description: "Relative urgency.",
|
|
308
|
+
enumValues: ["low", "medium", "high", "critical"],
|
|
309
|
+
defaultValue: "medium"
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: "owner",
|
|
313
|
+
type: "string",
|
|
314
|
+
required: false,
|
|
315
|
+
description: "Human-facing owner label.",
|
|
316
|
+
defaultValue: "Albert"
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
name: "goalId",
|
|
320
|
+
type: "string|null",
|
|
321
|
+
required: false,
|
|
322
|
+
description: "Linked goal id.",
|
|
323
|
+
defaultValue: null,
|
|
324
|
+
nullable: true
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: "projectId",
|
|
328
|
+
type: "string|null",
|
|
329
|
+
required: false,
|
|
330
|
+
description: "Linked project id.",
|
|
331
|
+
defaultValue: null,
|
|
332
|
+
nullable: true
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
name: "dueDate",
|
|
336
|
+
type: "YYYY-MM-DD|null",
|
|
337
|
+
required: false,
|
|
338
|
+
description: "Optional due date.",
|
|
339
|
+
defaultValue: null,
|
|
340
|
+
nullable: true
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
name: "effort",
|
|
344
|
+
type: "light|deep|marathon",
|
|
345
|
+
required: false,
|
|
346
|
+
description: "How heavy the task feels.",
|
|
347
|
+
enumValues: ["light", "deep", "marathon"],
|
|
348
|
+
defaultValue: "deep"
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "energy",
|
|
352
|
+
type: "low|steady|high",
|
|
353
|
+
required: false,
|
|
354
|
+
description: "Energy demand.",
|
|
355
|
+
enumValues: ["low", "steady", "high"],
|
|
356
|
+
defaultValue: "steady"
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: "points",
|
|
360
|
+
type: "integer",
|
|
361
|
+
required: false,
|
|
362
|
+
description: "Reward value for the task.",
|
|
363
|
+
defaultValue: 40
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
name: "sortOrder",
|
|
367
|
+
type: "integer",
|
|
368
|
+
required: false,
|
|
369
|
+
description: "Lane ordering hint when set explicitly."
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "tagIds",
|
|
373
|
+
type: "string[]",
|
|
374
|
+
required: false,
|
|
375
|
+
description: "Existing tag ids linked to the task.",
|
|
376
|
+
defaultValue: []
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: "notes",
|
|
380
|
+
type: "Array<{ contentMarkdown, author?, tags?, destroyAt?, links? }>",
|
|
381
|
+
required: false,
|
|
382
|
+
description: "Optional nested notes that will auto-link to the new task.",
|
|
383
|
+
defaultValue: []
|
|
384
|
+
}
|
|
385
|
+
]
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
entityType: "calendar_event",
|
|
389
|
+
purpose: "A canonical Forge calendar event that can live locally first and then project to connected provider calendars.",
|
|
390
|
+
minimumCreateFields: ["title", "startAt", "endAt"],
|
|
391
|
+
relationshipRules: [
|
|
392
|
+
"Forge stores the canonical event first; provider copies are downstream projections.",
|
|
393
|
+
"Use links to connect the event to goals, projects, tasks, habits, notes, or Psyche entities.",
|
|
394
|
+
"If preferredCalendarId is omitted, Forge uses the default writable connected calendar when one exists.",
|
|
395
|
+
"Set preferredCalendarId to null only when the user explicitly wants Forge-only storage."
|
|
396
|
+
],
|
|
397
|
+
searchHints: [
|
|
398
|
+
"Search by title or linked entity before creating a duplicate event.",
|
|
399
|
+
"Use linkedTo when you know the goal, project, task, or habit the event should already reference."
|
|
400
|
+
],
|
|
401
|
+
examples: [
|
|
402
|
+
'{"title":"Weekly research supervision","startAt":"2026-04-06T06:00:00.000Z","endAt":"2026-04-06T07:00:00.000Z","timezone":"Europe/Zurich","links":[{"entityType":"project","entityId":"project_123","relationshipType":"meeting_for"}]}'
|
|
403
|
+
],
|
|
404
|
+
fieldGuide: [
|
|
405
|
+
{
|
|
406
|
+
name: "title",
|
|
407
|
+
type: "string",
|
|
408
|
+
required: true,
|
|
409
|
+
description: "Human-readable event title."
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: "description",
|
|
413
|
+
type: "string",
|
|
414
|
+
required: false,
|
|
415
|
+
description: "Longer event description.",
|
|
416
|
+
defaultValue: ""
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
name: "location",
|
|
420
|
+
type: "string",
|
|
421
|
+
required: false,
|
|
422
|
+
description: "Location or meeting place.",
|
|
423
|
+
defaultValue: ""
|
|
424
|
+
},
|
|
425
|
+
{
|
|
426
|
+
name: "startAt",
|
|
427
|
+
type: "ISO datetime",
|
|
428
|
+
required: true,
|
|
429
|
+
description: "Start instant in ISO-8601 form."
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
name: "endAt",
|
|
433
|
+
type: "ISO datetime",
|
|
434
|
+
required: true,
|
|
435
|
+
description: "End instant in ISO-8601 form."
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: "timezone",
|
|
439
|
+
type: "string",
|
|
440
|
+
required: false,
|
|
441
|
+
description: "IANA timezone label.",
|
|
442
|
+
defaultValue: "UTC"
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
name: "isAllDay",
|
|
446
|
+
type: "boolean",
|
|
447
|
+
required: false,
|
|
448
|
+
description: "Whether this is an all-day event.",
|
|
449
|
+
defaultValue: false
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
name: "availability",
|
|
453
|
+
type: "busy|free",
|
|
454
|
+
required: false,
|
|
455
|
+
description: "Availability state exposed to scheduling rules.",
|
|
456
|
+
enumValues: ["busy", "free"],
|
|
457
|
+
defaultValue: "busy"
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
name: "eventType",
|
|
461
|
+
type: "string",
|
|
462
|
+
required: false,
|
|
463
|
+
description: "Optional event category label used by scheduling rules.",
|
|
464
|
+
defaultValue: ""
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
name: "categories",
|
|
468
|
+
type: "string[]",
|
|
469
|
+
required: false,
|
|
470
|
+
description: "Optional provider-style categories.",
|
|
471
|
+
defaultValue: []
|
|
472
|
+
},
|
|
473
|
+
{
|
|
474
|
+
name: "preferredCalendarId",
|
|
475
|
+
type: "string|null",
|
|
476
|
+
required: false,
|
|
477
|
+
description: "Writable connected calendar to project into. Omit it to use the default writable connected calendar. Set null only to force Forge-only storage.",
|
|
478
|
+
defaultValue: "default writable connected calendar when available",
|
|
479
|
+
nullable: true
|
|
480
|
+
},
|
|
481
|
+
{
|
|
482
|
+
name: "links",
|
|
483
|
+
type: "Array<{ entityType, entityId, relationshipType? }>",
|
|
484
|
+
required: false,
|
|
485
|
+
description: "Forge entities linked to this event.",
|
|
486
|
+
defaultValue: []
|
|
487
|
+
}
|
|
488
|
+
]
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
entityType: "work_block_template",
|
|
492
|
+
purpose: "A recurring work-availability template such as Main Activity, Secondary Activity, Third Activity, Rest, Holiday, or Custom.",
|
|
493
|
+
minimumCreateFields: [
|
|
494
|
+
"title",
|
|
495
|
+
"kind",
|
|
496
|
+
"timezone",
|
|
497
|
+
"weekDays",
|
|
498
|
+
"startMinute",
|
|
499
|
+
"endMinute",
|
|
500
|
+
"blockingState"
|
|
501
|
+
],
|
|
502
|
+
relationshipRules: [
|
|
503
|
+
"Work block templates derive visible calendar instances for the requested range instead of storing one repeated event per day.",
|
|
504
|
+
"startsOn and endsOn are optional active-date bounds. Leaving endsOn null makes the block repeat indefinitely.",
|
|
505
|
+
"They are Forge-owned scheduling structures, not mirrored provider events."
|
|
506
|
+
],
|
|
507
|
+
searchHints: [
|
|
508
|
+
"Search by title or kind before creating a duplicate recurring block."
|
|
509
|
+
],
|
|
510
|
+
examples: [
|
|
511
|
+
'{"title":"Main Activity","kind":"main_activity","color":"#f97316","timezone":"Europe/Zurich","weekDays":[1,2,3,4,5],"startMinute":480,"endMinute":720,"startsOn":"2026-04-06","endsOn":null,"blockingState":"blocked"}',
|
|
512
|
+
'{"title":"Summer holiday","kind":"holiday","color":"#14b8a6","timezone":"Europe/Zurich","weekDays":[0,1,2,3,4,5,6],"startMinute":0,"endMinute":1440,"startsOn":"2026-08-01","endsOn":"2026-08-16","blockingState":"blocked"}'
|
|
513
|
+
],
|
|
514
|
+
fieldGuide: [
|
|
515
|
+
{
|
|
516
|
+
name: "title",
|
|
517
|
+
type: "string",
|
|
518
|
+
required: true,
|
|
519
|
+
description: "Display name for the recurring block."
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
name: "kind",
|
|
523
|
+
type: "main_activity|secondary_activity|third_activity|rest|holiday|custom",
|
|
524
|
+
required: true,
|
|
525
|
+
description: "Preset or custom block type.",
|
|
526
|
+
enumValues: [
|
|
527
|
+
"main_activity",
|
|
528
|
+
"secondary_activity",
|
|
529
|
+
"third_activity",
|
|
530
|
+
"rest",
|
|
531
|
+
"holiday",
|
|
532
|
+
"custom"
|
|
533
|
+
]
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: "color",
|
|
537
|
+
type: "hex-color",
|
|
538
|
+
required: false,
|
|
539
|
+
description: "UI color for generated instances.",
|
|
540
|
+
defaultValue: "#60a5fa"
|
|
541
|
+
},
|
|
542
|
+
{
|
|
543
|
+
name: "timezone",
|
|
544
|
+
type: "string",
|
|
545
|
+
required: true,
|
|
546
|
+
description: "IANA timezone that defines the recurring window."
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
name: "weekDays",
|
|
550
|
+
type: "integer[]",
|
|
551
|
+
required: true,
|
|
552
|
+
description: "Weekday numbers where Sunday is 0 and Saturday is 6."
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
name: "startMinute",
|
|
556
|
+
type: "integer",
|
|
557
|
+
required: true,
|
|
558
|
+
description: "Minute from midnight where the block starts."
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: "endMinute",
|
|
562
|
+
type: "integer",
|
|
563
|
+
required: true,
|
|
564
|
+
description: "Minute from midnight where the block ends."
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
name: "startsOn",
|
|
568
|
+
type: "YYYY-MM-DD|null",
|
|
569
|
+
required: false,
|
|
570
|
+
description: "Optional first active date for the recurring block.",
|
|
571
|
+
defaultValue: null,
|
|
572
|
+
nullable: true
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
name: "endsOn",
|
|
576
|
+
type: "YYYY-MM-DD|null",
|
|
577
|
+
required: false,
|
|
578
|
+
description: "Optional last active date. Null means repeat indefinitely.",
|
|
579
|
+
defaultValue: null,
|
|
580
|
+
nullable: true
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
name: "blockingState",
|
|
584
|
+
type: "allowed|blocked",
|
|
585
|
+
required: true,
|
|
586
|
+
description: "Whether this block generally allows or blocks work.",
|
|
587
|
+
enumValues: ["allowed", "blocked"]
|
|
588
|
+
}
|
|
589
|
+
]
|
|
590
|
+
},
|
|
591
|
+
{
|
|
592
|
+
entityType: "task_timebox",
|
|
593
|
+
purpose: "A planned or live calendar slot attached to a task.",
|
|
594
|
+
minimumCreateFields: ["taskId", "title", "startsAt", "endsAt"],
|
|
595
|
+
relationshipRules: [
|
|
596
|
+
"Task timeboxes belong to a task and can optionally carry the parent project id.",
|
|
597
|
+
"Live task runs can attach to matching timeboxes later; creating a timebox does not start work by itself."
|
|
598
|
+
],
|
|
599
|
+
searchHints: [
|
|
600
|
+
"Search by task linkage or title before creating another slot for the same work block."
|
|
601
|
+
],
|
|
602
|
+
examples: [
|
|
603
|
+
'{"taskId":"task_123","projectId":"project_456","title":"Draft the methods section","startsAt":"2026-04-03T08:00:00.000Z","endsAt":"2026-04-03T09:30:00.000Z","source":"suggested"}'
|
|
604
|
+
],
|
|
172
605
|
fieldGuide: [
|
|
173
|
-
{
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
{
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
606
|
+
{
|
|
607
|
+
name: "taskId",
|
|
608
|
+
type: "string",
|
|
609
|
+
required: true,
|
|
610
|
+
description: "Linked task id."
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
name: "projectId",
|
|
614
|
+
type: "string|null",
|
|
615
|
+
required: false,
|
|
616
|
+
description: "Optional parent project id.",
|
|
617
|
+
defaultValue: null,
|
|
618
|
+
nullable: true
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
name: "title",
|
|
622
|
+
type: "string",
|
|
623
|
+
required: true,
|
|
624
|
+
description: "Timebox title shown on the calendar."
|
|
625
|
+
},
|
|
626
|
+
{
|
|
627
|
+
name: "startsAt",
|
|
628
|
+
type: "ISO datetime",
|
|
629
|
+
required: true,
|
|
630
|
+
description: "Start instant in ISO-8601 form."
|
|
631
|
+
},
|
|
632
|
+
{
|
|
633
|
+
name: "endsAt",
|
|
634
|
+
type: "ISO datetime",
|
|
635
|
+
required: true,
|
|
636
|
+
description: "End instant in ISO-8601 form."
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
name: "source",
|
|
640
|
+
type: "manual|suggested|live_run",
|
|
641
|
+
required: false,
|
|
642
|
+
description: "How the timebox was created.",
|
|
643
|
+
enumValues: ["manual", "suggested", "live_run"],
|
|
644
|
+
defaultValue: "manual"
|
|
645
|
+
},
|
|
646
|
+
{
|
|
647
|
+
name: "status",
|
|
648
|
+
type: "planned|active|completed|cancelled",
|
|
649
|
+
required: false,
|
|
650
|
+
description: "Current timebox state.",
|
|
651
|
+
enumValues: ["planned", "active", "completed", "cancelled"],
|
|
652
|
+
defaultValue: "planned"
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
name: "overrideReason",
|
|
656
|
+
type: "string|null",
|
|
657
|
+
required: false,
|
|
658
|
+
description: "Audited reason when the slot overrides a blocked context.",
|
|
659
|
+
defaultValue: null,
|
|
660
|
+
nullable: true
|
|
661
|
+
}
|
|
662
|
+
]
|
|
663
|
+
},
|
|
664
|
+
{
|
|
665
|
+
entityType: "habit",
|
|
666
|
+
purpose: "A recurring commitment or recurring slip with explicit cadence, graph links, and XP consequences.",
|
|
667
|
+
minimumCreateFields: ["title"],
|
|
668
|
+
relationshipRules: [
|
|
669
|
+
"Habits can link directly to goals, projects, tasks, values, patterns, behaviors, beliefs, modes, and trigger reports.",
|
|
670
|
+
"Habits are recurring records, not task variants, and they participate in search, notes, delete/restore, and XP.",
|
|
671
|
+
"linkedBehaviorId remains a compatibility alias; linkedBehaviorIds is the canonical array form."
|
|
672
|
+
],
|
|
673
|
+
searchHints: [
|
|
674
|
+
"Search by title before creating a duplicate habit.",
|
|
675
|
+
"Use linkedTo when the habit should already be attached to a goal, project, task, or Psyche entity."
|
|
676
|
+
],
|
|
677
|
+
examples: [
|
|
678
|
+
'{"title":"Morning training","frequency":"daily","polarity":"positive","linkedGoalIds":["goal_train_body"],"linkedValueIds":["value_steadiness"],"linkedBehaviorIds":["behavior_regulating_walk"]}'
|
|
679
|
+
],
|
|
680
|
+
fieldGuide: [
|
|
681
|
+
{
|
|
682
|
+
name: "title",
|
|
683
|
+
type: "string",
|
|
684
|
+
required: true,
|
|
685
|
+
description: "Concrete recurring behavior label."
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
name: "description",
|
|
689
|
+
type: "string",
|
|
690
|
+
required: false,
|
|
691
|
+
description: "Markdown definition of what counts as success or failure for this habit.",
|
|
692
|
+
defaultValue: ""
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
name: "status",
|
|
696
|
+
type: "active|paused|archived",
|
|
697
|
+
required: false,
|
|
698
|
+
description: "Lifecycle state.",
|
|
699
|
+
enumValues: ["active", "paused", "archived"],
|
|
700
|
+
defaultValue: "active"
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
name: "polarity",
|
|
704
|
+
type: "positive|negative",
|
|
705
|
+
required: false,
|
|
706
|
+
description: "Whether doing the behavior is aligned or misaligned.",
|
|
707
|
+
enumValues: ["positive", "negative"],
|
|
708
|
+
defaultValue: "positive"
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
name: "frequency",
|
|
712
|
+
type: "daily|weekly",
|
|
713
|
+
required: false,
|
|
714
|
+
description: "Recurrence cadence.",
|
|
715
|
+
enumValues: ["daily", "weekly"],
|
|
716
|
+
defaultValue: "daily"
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
name: "targetCount",
|
|
720
|
+
type: "integer",
|
|
721
|
+
required: false,
|
|
722
|
+
description: "How many repetitions define the cadence window.",
|
|
723
|
+
defaultValue: 1
|
|
724
|
+
},
|
|
725
|
+
{
|
|
726
|
+
name: "weekDays",
|
|
727
|
+
type: "integer[]",
|
|
728
|
+
required: false,
|
|
729
|
+
description: "Weekday numbers for weekly habits where Monday is 1 and Sunday is 0.",
|
|
730
|
+
defaultValue: []
|
|
731
|
+
},
|
|
732
|
+
{
|
|
733
|
+
name: "linkedGoalIds",
|
|
734
|
+
type: "string[]",
|
|
735
|
+
required: false,
|
|
736
|
+
description: "Linked goal ids.",
|
|
737
|
+
defaultValue: []
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
name: "linkedProjectIds",
|
|
741
|
+
type: "string[]",
|
|
742
|
+
required: false,
|
|
743
|
+
description: "Linked project ids.",
|
|
744
|
+
defaultValue: []
|
|
745
|
+
},
|
|
746
|
+
{
|
|
747
|
+
name: "linkedTaskIds",
|
|
748
|
+
type: "string[]",
|
|
749
|
+
required: false,
|
|
750
|
+
description: "Linked task ids.",
|
|
751
|
+
defaultValue: []
|
|
752
|
+
},
|
|
753
|
+
{
|
|
754
|
+
name: "linkedValueIds",
|
|
755
|
+
type: "string[]",
|
|
756
|
+
required: false,
|
|
757
|
+
description: "Linked value ids.",
|
|
758
|
+
defaultValue: []
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
name: "linkedPatternIds",
|
|
762
|
+
type: "string[]",
|
|
763
|
+
required: false,
|
|
764
|
+
description: "Linked pattern ids.",
|
|
765
|
+
defaultValue: []
|
|
766
|
+
},
|
|
767
|
+
{
|
|
768
|
+
name: "linkedBehaviorIds",
|
|
769
|
+
type: "string[]",
|
|
770
|
+
required: false,
|
|
771
|
+
description: "Canonical linked behavior ids.",
|
|
772
|
+
defaultValue: []
|
|
773
|
+
},
|
|
774
|
+
{
|
|
775
|
+
name: "linkedBehaviorId",
|
|
776
|
+
type: "string|null",
|
|
777
|
+
required: false,
|
|
778
|
+
description: "Compatibility alias for the first linked behavior id.",
|
|
779
|
+
defaultValue: null,
|
|
780
|
+
nullable: true
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
name: "linkedBeliefIds",
|
|
784
|
+
type: "string[]",
|
|
785
|
+
required: false,
|
|
786
|
+
description: "Linked belief ids.",
|
|
787
|
+
defaultValue: []
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
name: "linkedModeIds",
|
|
791
|
+
type: "string[]",
|
|
792
|
+
required: false,
|
|
793
|
+
description: "Linked mode ids.",
|
|
794
|
+
defaultValue: []
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
name: "linkedReportIds",
|
|
798
|
+
type: "string[]",
|
|
799
|
+
required: false,
|
|
800
|
+
description: "Linked trigger report ids.",
|
|
801
|
+
defaultValue: []
|
|
802
|
+
},
|
|
803
|
+
{
|
|
804
|
+
name: "rewardXp",
|
|
805
|
+
type: "integer",
|
|
806
|
+
required: false,
|
|
807
|
+
description: "XP granted on aligned check-ins.",
|
|
808
|
+
defaultValue: 12
|
|
809
|
+
},
|
|
810
|
+
{
|
|
811
|
+
name: "penaltyXp",
|
|
812
|
+
type: "integer",
|
|
813
|
+
required: false,
|
|
814
|
+
description: "XP removed on misaligned check-ins.",
|
|
815
|
+
defaultValue: 8
|
|
816
|
+
}
|
|
187
817
|
]
|
|
188
818
|
},
|
|
189
819
|
{
|
|
190
820
|
entityType: "note",
|
|
191
|
-
purpose: "A Markdown note that can link to one or many Forge entities.",
|
|
821
|
+
purpose: "A first-class Markdown note entity that can link to one or many Forge entities.",
|
|
192
822
|
minimumCreateFields: ["contentMarkdown", "links"],
|
|
193
823
|
relationshipRules: [
|
|
194
824
|
"Notes can link to goals, projects, tasks, Psyche records, and other supported Forge entities.",
|
|
195
|
-
"When nested under another create flow, notes auto-link to that new entity and can optionally include extra links."
|
|
825
|
+
"When nested under another create flow, notes auto-link to that new entity and can optionally include extra links.",
|
|
826
|
+
"Agents can also create standalone notes directly through forge_create_entities with entityType note."
|
|
827
|
+
],
|
|
828
|
+
searchHints: [
|
|
829
|
+
"Search by Markdown content, author, or linked entity before creating a duplicate note."
|
|
196
830
|
],
|
|
197
|
-
searchHints: ["Search by Markdown content, author, or linked entity before creating a duplicate note."],
|
|
198
831
|
examples: [
|
|
199
832
|
'{"contentMarkdown":"Finished the review pass and captured the remaining edge cases.","links":[{"entityType":"task","entityId":"task_123"}]}',
|
|
200
|
-
'{"contentMarkdown":"Observed a stronger protector response after the meeting.","author":"forge-agent","links":[{"entityType":"trigger_report","entityId":"report_123"},{"entityType":"behavior_pattern","entityId":"pattern_123"}]}'
|
|
833
|
+
'{"contentMarkdown":"Observed a stronger protector response after the meeting.","author":"forge-agent","tags":["Short-term memory","therapy"],"links":[{"entityType":"trigger_report","entityId":"report_123"},{"entityType":"behavior_pattern","entityId":"pattern_123"}]}',
|
|
834
|
+
'{"contentMarkdown":"Scratch capture for what I am actively holding in mind.","tags":["Working memory","handoff"],"destroyAt":"2026-04-04T12:00:00.000Z","links":[{"entityType":"task","entityId":"task_123"}]}'
|
|
201
835
|
],
|
|
202
836
|
fieldGuide: [
|
|
203
|
-
{
|
|
204
|
-
|
|
205
|
-
|
|
837
|
+
{
|
|
838
|
+
name: "contentMarkdown",
|
|
839
|
+
type: "string",
|
|
840
|
+
required: true,
|
|
841
|
+
description: "Markdown body of the note."
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
name: "author",
|
|
845
|
+
type: "string|null",
|
|
846
|
+
required: false,
|
|
847
|
+
description: "Optional display author for the note.",
|
|
848
|
+
defaultValue: null,
|
|
849
|
+
nullable: true
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
name: "tags",
|
|
853
|
+
type: "string[]",
|
|
854
|
+
required: false,
|
|
855
|
+
description: "Optional note-owned tags such as Working memory, Short-term memory, Episodic memory, Semantic memory, Procedural memory, or custom labels.",
|
|
856
|
+
defaultValue: []
|
|
857
|
+
},
|
|
858
|
+
{
|
|
859
|
+
name: "destroyAt",
|
|
860
|
+
type: "ISO datetime|null",
|
|
861
|
+
required: false,
|
|
862
|
+
description: "Optional auto-destroy timestamp. If set, Forge deletes the note after that time as ephemeral memory.",
|
|
863
|
+
defaultValue: null,
|
|
864
|
+
nullable: true
|
|
865
|
+
},
|
|
866
|
+
{
|
|
867
|
+
name: "links",
|
|
868
|
+
type: "Array<{ entityType, entityId, anchorKey? }>",
|
|
869
|
+
required: true,
|
|
870
|
+
description: "Entities this note should link to."
|
|
871
|
+
}
|
|
206
872
|
]
|
|
207
873
|
},
|
|
208
874
|
{
|
|
@@ -213,19 +879,83 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
213
879
|
"Insights can optionally point at one entity through entityType and entityId.",
|
|
214
880
|
"Use insights for interpretation or advice, not as a replacement for goals, tasks, or trigger reports."
|
|
215
881
|
],
|
|
216
|
-
searchHints: [
|
|
217
|
-
|
|
882
|
+
searchHints: [
|
|
883
|
+
"Search recent insights before posting a new one if the same pattern may already be captured."
|
|
884
|
+
],
|
|
885
|
+
examples: [
|
|
886
|
+
'{"entityType":"goal","entityId":"goal_create_meaningfully","title":"Admin drag is masking momentum","summary":"Creative progress is happening, but admin cleanup keeps interrupting it.","recommendation":"Protect one clean creative block and isolate admin into a separate recurring task."}'
|
|
887
|
+
],
|
|
218
888
|
fieldGuide: [
|
|
219
|
-
{
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
{
|
|
228
|
-
|
|
889
|
+
{
|
|
890
|
+
name: "entityType",
|
|
891
|
+
type: "string|null",
|
|
892
|
+
required: false,
|
|
893
|
+
description: "Optional linked entity type.",
|
|
894
|
+
defaultValue: null,
|
|
895
|
+
nullable: true
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
name: "entityId",
|
|
899
|
+
type: "string|null",
|
|
900
|
+
required: false,
|
|
901
|
+
description: "Optional linked entity id.",
|
|
902
|
+
defaultValue: null,
|
|
903
|
+
nullable: true
|
|
904
|
+
},
|
|
905
|
+
{
|
|
906
|
+
name: "timeframeLabel",
|
|
907
|
+
type: "string|null",
|
|
908
|
+
required: false,
|
|
909
|
+
description: "Optional time window label.",
|
|
910
|
+
defaultValue: null,
|
|
911
|
+
nullable: true
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
name: "title",
|
|
915
|
+
type: "string",
|
|
916
|
+
required: true,
|
|
917
|
+
description: "Insight title."
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
name: "summary",
|
|
921
|
+
type: "string",
|
|
922
|
+
required: true,
|
|
923
|
+
description: "Short explanation of the pattern or tension."
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
name: "recommendation",
|
|
927
|
+
type: "string",
|
|
928
|
+
required: true,
|
|
929
|
+
description: "Actionable next move or reframing."
|
|
930
|
+
},
|
|
931
|
+
{
|
|
932
|
+
name: "rationale",
|
|
933
|
+
type: "string",
|
|
934
|
+
required: false,
|
|
935
|
+
description: "Why this insight is grounded in the data.",
|
|
936
|
+
defaultValue: ""
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
name: "confidence",
|
|
940
|
+
type: "number",
|
|
941
|
+
required: false,
|
|
942
|
+
description: "Confidence from 0 to 1.",
|
|
943
|
+
defaultValue: 0.7
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
name: "visibility",
|
|
947
|
+
type: "string",
|
|
948
|
+
required: false,
|
|
949
|
+
description: "Visibility mode for the insight.",
|
|
950
|
+
defaultValue: "visible"
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
name: "ctaLabel",
|
|
954
|
+
type: "string",
|
|
955
|
+
required: false,
|
|
956
|
+
description: "CTA shown in the UI.",
|
|
957
|
+
defaultValue: "Review insight"
|
|
958
|
+
}
|
|
229
959
|
]
|
|
230
960
|
},
|
|
231
961
|
{
|
|
@@ -236,10 +966,24 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
236
966
|
"Trigger reports can reference one event type through eventTypeId.",
|
|
237
967
|
"Use event types to normalize repeated report categories instead of inventing new wording every time."
|
|
238
968
|
],
|
|
239
|
-
searchHints: [
|
|
969
|
+
searchHints: [
|
|
970
|
+
"Search by label before creating a new event type.",
|
|
971
|
+
"Prefer existing event types when one clearly fits the situation."
|
|
972
|
+
],
|
|
240
973
|
fieldGuide: [
|
|
241
|
-
{
|
|
242
|
-
|
|
974
|
+
{
|
|
975
|
+
name: "label",
|
|
976
|
+
type: "string",
|
|
977
|
+
required: true,
|
|
978
|
+
description: "Human-readable event type label."
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
name: "description",
|
|
982
|
+
type: "string",
|
|
983
|
+
required: false,
|
|
984
|
+
description: "What kind of incident this event type represents.",
|
|
985
|
+
defaultValue: ""
|
|
986
|
+
}
|
|
243
987
|
]
|
|
244
988
|
},
|
|
245
989
|
{
|
|
@@ -250,11 +994,31 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
250
994
|
"Trigger report emotions can reference an emotion definition through emotionDefinitionId.",
|
|
251
995
|
"Use emotion definitions to normalize repeated emotional labels across reports."
|
|
252
996
|
],
|
|
253
|
-
searchHints: [
|
|
997
|
+
searchHints: [
|
|
998
|
+
"Search by label before creating a new emotion definition.",
|
|
999
|
+
"Prefer an existing emotion definition when the label already captures the feeling well."
|
|
1000
|
+
],
|
|
254
1001
|
fieldGuide: [
|
|
255
|
-
{
|
|
256
|
-
|
|
257
|
-
|
|
1002
|
+
{
|
|
1003
|
+
name: "label",
|
|
1004
|
+
type: "string",
|
|
1005
|
+
required: true,
|
|
1006
|
+
description: "Emotion label."
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
name: "description",
|
|
1010
|
+
type: "string",
|
|
1011
|
+
required: false,
|
|
1012
|
+
description: "What this emotion label is meant to capture.",
|
|
1013
|
+
defaultValue: ""
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
name: "category",
|
|
1017
|
+
type: "string",
|
|
1018
|
+
required: false,
|
|
1019
|
+
description: "Optional grouping such as threat, grief, anger, or connection.",
|
|
1020
|
+
defaultValue: ""
|
|
1021
|
+
}
|
|
258
1022
|
]
|
|
259
1023
|
},
|
|
260
1024
|
{
|
|
@@ -265,17 +1029,69 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
265
1029
|
"Values can link to goals, projects, and tasks.",
|
|
266
1030
|
"Patterns, behaviors, beliefs, and reports can all point back to values."
|
|
267
1031
|
],
|
|
268
|
-
searchHints: [
|
|
269
|
-
|
|
1032
|
+
searchHints: [
|
|
1033
|
+
"Search by title before creating a new value.",
|
|
1034
|
+
"Use linkedTo if the value should already be attached to a goal or task."
|
|
1035
|
+
],
|
|
1036
|
+
examples: [
|
|
1037
|
+
'{"title":"Steadiness","valuedDirection":"Respond calmly instead of collapsing or reacting fast.","whyItMatters":"I want to stay grounded in relationships and work."}'
|
|
1038
|
+
],
|
|
270
1039
|
fieldGuide: [
|
|
271
|
-
{
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
{
|
|
278
|
-
|
|
1040
|
+
{
|
|
1041
|
+
name: "title",
|
|
1042
|
+
type: "string",
|
|
1043
|
+
required: true,
|
|
1044
|
+
description: "Value name."
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
name: "description",
|
|
1048
|
+
type: "string",
|
|
1049
|
+
required: false,
|
|
1050
|
+
description: "What the value means in practice.",
|
|
1051
|
+
defaultValue: ""
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
name: "valuedDirection",
|
|
1055
|
+
type: "string",
|
|
1056
|
+
required: false,
|
|
1057
|
+
description: "How the user wants to live or act when guided by this value.",
|
|
1058
|
+
defaultValue: ""
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
name: "whyItMatters",
|
|
1062
|
+
type: "string",
|
|
1063
|
+
required: false,
|
|
1064
|
+
description: "Why the value matters to the user.",
|
|
1065
|
+
defaultValue: ""
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
name: "linkedGoalIds",
|
|
1069
|
+
type: "string[]",
|
|
1070
|
+
required: false,
|
|
1071
|
+
description: "Linked goal ids.",
|
|
1072
|
+
defaultValue: []
|
|
1073
|
+
},
|
|
1074
|
+
{
|
|
1075
|
+
name: "linkedProjectIds",
|
|
1076
|
+
type: "string[]",
|
|
1077
|
+
required: false,
|
|
1078
|
+
description: "Linked project ids.",
|
|
1079
|
+
defaultValue: []
|
|
1080
|
+
},
|
|
1081
|
+
{
|
|
1082
|
+
name: "linkedTaskIds",
|
|
1083
|
+
type: "string[]",
|
|
1084
|
+
required: false,
|
|
1085
|
+
description: "Linked task ids.",
|
|
1086
|
+
defaultValue: []
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
name: "committedActions",
|
|
1090
|
+
type: "string[]",
|
|
1091
|
+
required: false,
|
|
1092
|
+
description: "Small concrete actions that enact the value.",
|
|
1093
|
+
defaultValue: []
|
|
1094
|
+
}
|
|
279
1095
|
]
|
|
280
1096
|
},
|
|
281
1097
|
{
|
|
@@ -286,21 +1102,96 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
286
1102
|
"Patterns can link to values, beliefs, and modes.",
|
|
287
1103
|
"Trigger reports can link back to patterns they instantiate."
|
|
288
1104
|
],
|
|
289
|
-
searchHints: [
|
|
290
|
-
|
|
1105
|
+
searchHints: [
|
|
1106
|
+
"Search by title or by trigger language before creating a new pattern."
|
|
1107
|
+
],
|
|
1108
|
+
examples: [
|
|
1109
|
+
'{"title":"Late-night father text freeze","cueContexts":["Father texts late at night"],"targetBehavior":"Freeze, avoid replying, and doomscroll","shortTermPayoff":"Avoids immediate overwhelm","longTermCost":"Sleep loss, guilt, and dread","preferredResponse":"Pause, regulate, and reply on my own terms the next morning"}'
|
|
1110
|
+
],
|
|
291
1111
|
fieldGuide: [
|
|
292
|
-
{
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
{
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
1112
|
+
{
|
|
1113
|
+
name: "title",
|
|
1114
|
+
type: "string",
|
|
1115
|
+
required: true,
|
|
1116
|
+
description: "Short pattern name."
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
name: "description",
|
|
1120
|
+
type: "string",
|
|
1121
|
+
required: false,
|
|
1122
|
+
description: "What usually happens in this loop.",
|
|
1123
|
+
defaultValue: ""
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
name: "targetBehavior",
|
|
1127
|
+
type: "string",
|
|
1128
|
+
required: false,
|
|
1129
|
+
description: "The visible behavior this pattern tends to produce.",
|
|
1130
|
+
defaultValue: ""
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
name: "cueContexts",
|
|
1134
|
+
type: "string[]",
|
|
1135
|
+
required: false,
|
|
1136
|
+
description: "Typical cues, contexts, or triggers.",
|
|
1137
|
+
defaultValue: []
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
name: "shortTermPayoff",
|
|
1141
|
+
type: "string",
|
|
1142
|
+
required: false,
|
|
1143
|
+
description: "What the loop gives immediately.",
|
|
1144
|
+
defaultValue: ""
|
|
1145
|
+
},
|
|
1146
|
+
{
|
|
1147
|
+
name: "longTermCost",
|
|
1148
|
+
type: "string",
|
|
1149
|
+
required: false,
|
|
1150
|
+
description: "What the loop costs later.",
|
|
1151
|
+
defaultValue: ""
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
name: "preferredResponse",
|
|
1155
|
+
type: "string",
|
|
1156
|
+
required: false,
|
|
1157
|
+
description: "Preferred alternative response.",
|
|
1158
|
+
defaultValue: ""
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
name: "linkedValueIds",
|
|
1162
|
+
type: "string[]",
|
|
1163
|
+
required: false,
|
|
1164
|
+
description: "Linked value ids.",
|
|
1165
|
+
defaultValue: []
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
name: "linkedSchemaLabels",
|
|
1169
|
+
type: "string[]",
|
|
1170
|
+
required: false,
|
|
1171
|
+
description: "Schema labels involved in the pattern.",
|
|
1172
|
+
defaultValue: []
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
name: "linkedModeLabels",
|
|
1176
|
+
type: "string[]",
|
|
1177
|
+
required: false,
|
|
1178
|
+
description: "Mode labels involved in the pattern.",
|
|
1179
|
+
defaultValue: []
|
|
1180
|
+
},
|
|
1181
|
+
{
|
|
1182
|
+
name: "linkedModeIds",
|
|
1183
|
+
type: "string[]",
|
|
1184
|
+
required: false,
|
|
1185
|
+
description: "Linked mode ids.",
|
|
1186
|
+
defaultValue: []
|
|
1187
|
+
},
|
|
1188
|
+
{
|
|
1189
|
+
name: "linkedBeliefIds",
|
|
1190
|
+
type: "string[]",
|
|
1191
|
+
required: false,
|
|
1192
|
+
description: "Linked belief ids.",
|
|
1193
|
+
defaultValue: []
|
|
1194
|
+
}
|
|
304
1195
|
]
|
|
305
1196
|
},
|
|
306
1197
|
{
|
|
@@ -312,21 +1203,100 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
312
1203
|
"Trigger reports can link to behaviors they contained."
|
|
313
1204
|
],
|
|
314
1205
|
searchHints: ["Search by title and kind before creating a new behavior."],
|
|
315
|
-
examples: [
|
|
1206
|
+
examples: [
|
|
1207
|
+
'{"kind":"away","title":"Doomscroll after conflict cue","commonCues":["Received a critical text"],"shortTermPayoff":"Numbs the anxiety","longTermCost":"Loses time and deepens shame","replacementMove":"Put phone down and take one slow lap outside"}'
|
|
1208
|
+
],
|
|
316
1209
|
fieldGuide: [
|
|
317
|
-
{
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
{
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
1210
|
+
{
|
|
1211
|
+
name: "kind",
|
|
1212
|
+
type: "away|committed|recovery",
|
|
1213
|
+
required: true,
|
|
1214
|
+
description: "Whether the behavior moves away from values, toward them, or repairs after rupture.",
|
|
1215
|
+
enumValues: ["away", "committed", "recovery"]
|
|
1216
|
+
},
|
|
1217
|
+
{
|
|
1218
|
+
name: "title",
|
|
1219
|
+
type: "string",
|
|
1220
|
+
required: true,
|
|
1221
|
+
description: "Behavior label."
|
|
1222
|
+
},
|
|
1223
|
+
{
|
|
1224
|
+
name: "description",
|
|
1225
|
+
type: "string",
|
|
1226
|
+
required: false,
|
|
1227
|
+
description: "What the behavior looks like.",
|
|
1228
|
+
defaultValue: ""
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
name: "commonCues",
|
|
1232
|
+
type: "string[]",
|
|
1233
|
+
required: false,
|
|
1234
|
+
description: "Typical cues for this behavior.",
|
|
1235
|
+
defaultValue: []
|
|
1236
|
+
},
|
|
1237
|
+
{
|
|
1238
|
+
name: "urgeStory",
|
|
1239
|
+
type: "string",
|
|
1240
|
+
required: false,
|
|
1241
|
+
description: "What the inner urge or story feels like.",
|
|
1242
|
+
defaultValue: ""
|
|
1243
|
+
},
|
|
1244
|
+
{
|
|
1245
|
+
name: "shortTermPayoff",
|
|
1246
|
+
type: "string",
|
|
1247
|
+
required: false,
|
|
1248
|
+
description: "Immediate payoff.",
|
|
1249
|
+
defaultValue: ""
|
|
1250
|
+
},
|
|
1251
|
+
{
|
|
1252
|
+
name: "longTermCost",
|
|
1253
|
+
type: "string",
|
|
1254
|
+
required: false,
|
|
1255
|
+
description: "Longer-term cost.",
|
|
1256
|
+
defaultValue: ""
|
|
1257
|
+
},
|
|
1258
|
+
{
|
|
1259
|
+
name: "replacementMove",
|
|
1260
|
+
type: "string",
|
|
1261
|
+
required: false,
|
|
1262
|
+
description: "Preferred replacement move.",
|
|
1263
|
+
defaultValue: ""
|
|
1264
|
+
},
|
|
1265
|
+
{
|
|
1266
|
+
name: "repairPlan",
|
|
1267
|
+
type: "string",
|
|
1268
|
+
required: false,
|
|
1269
|
+
description: "Repair plan after the behavior occurs.",
|
|
1270
|
+
defaultValue: ""
|
|
1271
|
+
},
|
|
1272
|
+
{
|
|
1273
|
+
name: "linkedPatternIds",
|
|
1274
|
+
type: "string[]",
|
|
1275
|
+
required: false,
|
|
1276
|
+
description: "Linked behavior pattern ids.",
|
|
1277
|
+
defaultValue: []
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
name: "linkedValueIds",
|
|
1281
|
+
type: "string[]",
|
|
1282
|
+
required: false,
|
|
1283
|
+
description: "Linked value ids.",
|
|
1284
|
+
defaultValue: []
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
name: "linkedSchemaIds",
|
|
1288
|
+
type: "string[]",
|
|
1289
|
+
required: false,
|
|
1290
|
+
description: "Linked schema ids.",
|
|
1291
|
+
defaultValue: []
|
|
1292
|
+
},
|
|
1293
|
+
{
|
|
1294
|
+
name: "linkedModeIds",
|
|
1295
|
+
type: "string[]",
|
|
1296
|
+
required: false,
|
|
1297
|
+
description: "Linked mode ids.",
|
|
1298
|
+
defaultValue: []
|
|
1299
|
+
}
|
|
330
1300
|
]
|
|
331
1301
|
},
|
|
332
1302
|
{
|
|
@@ -337,21 +1307,97 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
337
1307
|
"Beliefs can link to values, behaviors, modes, and trigger reports.",
|
|
338
1308
|
"Behavior patterns can point to beliefs that keep the loop alive."
|
|
339
1309
|
],
|
|
340
|
-
searchHints: [
|
|
341
|
-
|
|
1310
|
+
searchHints: [
|
|
1311
|
+
"Search by statement or known schema theme before creating a new belief entry."
|
|
1312
|
+
],
|
|
1313
|
+
examples: [
|
|
1314
|
+
'{"statement":"If I disappoint people, they will leave me.","beliefType":"conditional","confidence":82,"evidenceFor":["People got cold when I failed them before"],"evidenceAgainst":["Some people stayed with me even after conflict"],"flexibleAlternative":"Disappointing someone can strain a relationship, but it does not automatically mean abandonment."}'
|
|
1315
|
+
],
|
|
342
1316
|
fieldGuide: [
|
|
343
|
-
{
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
{
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
1317
|
+
{
|
|
1318
|
+
name: "schemaId",
|
|
1319
|
+
type: "string|null",
|
|
1320
|
+
required: false,
|
|
1321
|
+
description: "Optional linked schema catalog id.",
|
|
1322
|
+
defaultValue: null,
|
|
1323
|
+
nullable: true
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
name: "statement",
|
|
1327
|
+
type: "string",
|
|
1328
|
+
required: true,
|
|
1329
|
+
description: "Belief statement in the user's own words."
|
|
1330
|
+
},
|
|
1331
|
+
{
|
|
1332
|
+
name: "beliefType",
|
|
1333
|
+
type: "absolute|conditional",
|
|
1334
|
+
required: true,
|
|
1335
|
+
description: "Whether the belief is absolute or if-then shaped.",
|
|
1336
|
+
enumValues: ["absolute", "conditional"]
|
|
1337
|
+
},
|
|
1338
|
+
{
|
|
1339
|
+
name: "originNote",
|
|
1340
|
+
type: "string",
|
|
1341
|
+
required: false,
|
|
1342
|
+
description: "Where the belief seems to come from.",
|
|
1343
|
+
defaultValue: ""
|
|
1344
|
+
},
|
|
1345
|
+
{
|
|
1346
|
+
name: "confidence",
|
|
1347
|
+
type: "integer",
|
|
1348
|
+
required: false,
|
|
1349
|
+
description: "How strongly the belief feels true from 0 to 100.",
|
|
1350
|
+
defaultValue: 60
|
|
1351
|
+
},
|
|
1352
|
+
{
|
|
1353
|
+
name: "evidenceFor",
|
|
1354
|
+
type: "string[]",
|
|
1355
|
+
required: false,
|
|
1356
|
+
description: "Evidence that seems to support the belief.",
|
|
1357
|
+
defaultValue: []
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
name: "evidenceAgainst",
|
|
1361
|
+
type: "string[]",
|
|
1362
|
+
required: false,
|
|
1363
|
+
description: "Evidence that weakens the belief.",
|
|
1364
|
+
defaultValue: []
|
|
1365
|
+
},
|
|
1366
|
+
{
|
|
1367
|
+
name: "flexibleAlternative",
|
|
1368
|
+
type: "string",
|
|
1369
|
+
required: false,
|
|
1370
|
+
description: "More flexible alternative belief.",
|
|
1371
|
+
defaultValue: ""
|
|
1372
|
+
},
|
|
1373
|
+
{
|
|
1374
|
+
name: "linkedValueIds",
|
|
1375
|
+
type: "string[]",
|
|
1376
|
+
required: false,
|
|
1377
|
+
description: "Linked value ids.",
|
|
1378
|
+
defaultValue: []
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
name: "linkedBehaviorIds",
|
|
1382
|
+
type: "string[]",
|
|
1383
|
+
required: false,
|
|
1384
|
+
description: "Linked behavior ids.",
|
|
1385
|
+
defaultValue: []
|
|
1386
|
+
},
|
|
1387
|
+
{
|
|
1388
|
+
name: "linkedModeIds",
|
|
1389
|
+
type: "string[]",
|
|
1390
|
+
required: false,
|
|
1391
|
+
description: "Linked mode ids.",
|
|
1392
|
+
defaultValue: []
|
|
1393
|
+
},
|
|
1394
|
+
{
|
|
1395
|
+
name: "linkedReportIds",
|
|
1396
|
+
type: "string[]",
|
|
1397
|
+
required: false,
|
|
1398
|
+
description: "Linked trigger report ids.",
|
|
1399
|
+
defaultValue: []
|
|
1400
|
+
}
|
|
355
1401
|
]
|
|
356
1402
|
},
|
|
357
1403
|
{
|
|
@@ -362,24 +1408,124 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
362
1408
|
"Modes can link to patterns, behaviors, and values.",
|
|
363
1409
|
"Trigger reports can include linkedModeIds and modeOverlays that reference modes."
|
|
364
1410
|
],
|
|
365
|
-
searchHints: [
|
|
366
|
-
|
|
1411
|
+
searchHints: [
|
|
1412
|
+
"Search by title or family before creating a new mode profile."
|
|
1413
|
+
],
|
|
1414
|
+
examples: [
|
|
1415
|
+
'{"family":"coping","title":"Cold controller","fear":"If I soften, I will be humiliated or lose control.","protectiveJob":"Stay hyper-competent and unreachable when threatened."}'
|
|
1416
|
+
],
|
|
367
1417
|
fieldGuide: [
|
|
368
|
-
{
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
{
|
|
382
|
-
|
|
1418
|
+
{
|
|
1419
|
+
name: "family",
|
|
1420
|
+
type: "coping|child|critic_parent|healthy_adult|happy_child",
|
|
1421
|
+
required: true,
|
|
1422
|
+
description: "Mode family.",
|
|
1423
|
+
enumValues: [
|
|
1424
|
+
"coping",
|
|
1425
|
+
"child",
|
|
1426
|
+
"critic_parent",
|
|
1427
|
+
"healthy_adult",
|
|
1428
|
+
"happy_child"
|
|
1429
|
+
]
|
|
1430
|
+
},
|
|
1431
|
+
{
|
|
1432
|
+
name: "title",
|
|
1433
|
+
type: "string",
|
|
1434
|
+
required: true,
|
|
1435
|
+
description: "Mode title."
|
|
1436
|
+
},
|
|
1437
|
+
{
|
|
1438
|
+
name: "archetype",
|
|
1439
|
+
type: "string",
|
|
1440
|
+
required: false,
|
|
1441
|
+
description: "Optional archetype label.",
|
|
1442
|
+
defaultValue: ""
|
|
1443
|
+
},
|
|
1444
|
+
{
|
|
1445
|
+
name: "persona",
|
|
1446
|
+
type: "string",
|
|
1447
|
+
required: false,
|
|
1448
|
+
description: "Narrative or felt sense of the mode.",
|
|
1449
|
+
defaultValue: ""
|
|
1450
|
+
},
|
|
1451
|
+
{
|
|
1452
|
+
name: "imagery",
|
|
1453
|
+
type: "string",
|
|
1454
|
+
required: false,
|
|
1455
|
+
description: "Imagery associated with the mode.",
|
|
1456
|
+
defaultValue: ""
|
|
1457
|
+
},
|
|
1458
|
+
{
|
|
1459
|
+
name: "symbolicForm",
|
|
1460
|
+
type: "string",
|
|
1461
|
+
required: false,
|
|
1462
|
+
description: "Symbolic form or metaphor.",
|
|
1463
|
+
defaultValue: ""
|
|
1464
|
+
},
|
|
1465
|
+
{
|
|
1466
|
+
name: "facialExpression",
|
|
1467
|
+
type: "string",
|
|
1468
|
+
required: false,
|
|
1469
|
+
description: "Typical facial expression or posture.",
|
|
1470
|
+
defaultValue: ""
|
|
1471
|
+
},
|
|
1472
|
+
{
|
|
1473
|
+
name: "fear",
|
|
1474
|
+
type: "string",
|
|
1475
|
+
required: false,
|
|
1476
|
+
description: "Core fear carried by the mode.",
|
|
1477
|
+
defaultValue: ""
|
|
1478
|
+
},
|
|
1479
|
+
{
|
|
1480
|
+
name: "burden",
|
|
1481
|
+
type: "string",
|
|
1482
|
+
required: false,
|
|
1483
|
+
description: "Burden or pain the mode carries.",
|
|
1484
|
+
defaultValue: ""
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
name: "protectiveJob",
|
|
1488
|
+
type: "string",
|
|
1489
|
+
required: false,
|
|
1490
|
+
description: "What job the mode thinks it is doing.",
|
|
1491
|
+
defaultValue: ""
|
|
1492
|
+
},
|
|
1493
|
+
{
|
|
1494
|
+
name: "originContext",
|
|
1495
|
+
type: "string",
|
|
1496
|
+
required: false,
|
|
1497
|
+
description: "Where the mode seems to come from.",
|
|
1498
|
+
defaultValue: ""
|
|
1499
|
+
},
|
|
1500
|
+
{
|
|
1501
|
+
name: "firstAppearanceAt",
|
|
1502
|
+
type: "string|null",
|
|
1503
|
+
required: false,
|
|
1504
|
+
description: "Optional first-seen marker.",
|
|
1505
|
+
defaultValue: null,
|
|
1506
|
+
nullable: true
|
|
1507
|
+
},
|
|
1508
|
+
{
|
|
1509
|
+
name: "linkedPatternIds",
|
|
1510
|
+
type: "string[]",
|
|
1511
|
+
required: false,
|
|
1512
|
+
description: "Linked pattern ids.",
|
|
1513
|
+
defaultValue: []
|
|
1514
|
+
},
|
|
1515
|
+
{
|
|
1516
|
+
name: "linkedBehaviorIds",
|
|
1517
|
+
type: "string[]",
|
|
1518
|
+
required: false,
|
|
1519
|
+
description: "Linked behavior ids.",
|
|
1520
|
+
defaultValue: []
|
|
1521
|
+
},
|
|
1522
|
+
{
|
|
1523
|
+
name: "linkedValueIds",
|
|
1524
|
+
type: "string[]",
|
|
1525
|
+
required: false,
|
|
1526
|
+
description: "Linked value ids.",
|
|
1527
|
+
defaultValue: []
|
|
1528
|
+
}
|
|
383
1529
|
]
|
|
384
1530
|
},
|
|
385
1531
|
{
|
|
@@ -390,12 +1536,31 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
390
1536
|
"Mode guide sessions help the user reason toward likely modes before or alongside mode profiles.",
|
|
391
1537
|
"Use mode guide sessions for guided interpretation, not as a replacement for durable mode profiles."
|
|
392
1538
|
],
|
|
393
|
-
searchHints: [
|
|
394
|
-
|
|
1539
|
+
searchHints: [
|
|
1540
|
+
"Search by summary when revisiting a prior guided mode session."
|
|
1541
|
+
],
|
|
1542
|
+
examples: [
|
|
1543
|
+
'{"summary":"Mapping the part that takes over under criticism","answers":[{"questionKey":"felt_shift","value":"I go cold and rigid"}],"results":[{"family":"coping","archetype":"detached_protector","label":"Cold controller","confidence":0.74,"reasoning":"It distances from shame and tries to stay untouchable."}]}'
|
|
1544
|
+
],
|
|
395
1545
|
fieldGuide: [
|
|
396
|
-
{
|
|
397
|
-
|
|
398
|
-
|
|
1546
|
+
{
|
|
1547
|
+
name: "summary",
|
|
1548
|
+
type: "string",
|
|
1549
|
+
required: true,
|
|
1550
|
+
description: "Short summary of what the guided session explored."
|
|
1551
|
+
},
|
|
1552
|
+
{
|
|
1553
|
+
name: "answers",
|
|
1554
|
+
type: "array",
|
|
1555
|
+
required: true,
|
|
1556
|
+
description: "List of { questionKey, value } items capturing the user's guided answers."
|
|
1557
|
+
},
|
|
1558
|
+
{
|
|
1559
|
+
name: "results",
|
|
1560
|
+
type: "array",
|
|
1561
|
+
required: true,
|
|
1562
|
+
description: "List of { family, archetype, label, confidence 0-1, reasoning } candidate mode interpretations."
|
|
1563
|
+
}
|
|
399
1564
|
]
|
|
400
1565
|
},
|
|
401
1566
|
{
|
|
@@ -407,31 +1572,168 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
|
|
|
407
1572
|
"A report is the best container for one specific emotionally meaningful episode.",
|
|
408
1573
|
"Use reports when you need one event chain, not just a generic pattern."
|
|
409
1574
|
],
|
|
410
|
-
searchHints: [
|
|
411
|
-
|
|
1575
|
+
searchHints: [
|
|
1576
|
+
"Search by title, event wording, or linked entities before creating a duplicate report."
|
|
1577
|
+
],
|
|
1578
|
+
examples: [
|
|
1579
|
+
'{"title":"Partner said we need to talk and I spiraled","customEventType":"relationship threat","eventSituation":"My partner texted that we needed to talk tonight.","emotions":[{"label":"fear","intensity":85},{"label":"shame","intensity":60}],"thoughts":[{"text":"This means I messed everything up."}],"behaviors":[{"text":"Paced, catastrophized, and checked my phone repeatedly"}],"nextMoves":["Wait until we speak before predicting the outcome","Write down the facts I actually know"]}'
|
|
1580
|
+
],
|
|
412
1581
|
fieldGuide: [
|
|
413
|
-
{
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
{
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
{
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
1582
|
+
{
|
|
1583
|
+
name: "title",
|
|
1584
|
+
type: "string",
|
|
1585
|
+
required: true,
|
|
1586
|
+
description: "Short name for the incident."
|
|
1587
|
+
},
|
|
1588
|
+
{
|
|
1589
|
+
name: "status",
|
|
1590
|
+
type: "draft|reviewed|integrated",
|
|
1591
|
+
required: false,
|
|
1592
|
+
description: "Reflection progress state.",
|
|
1593
|
+
enumValues: ["draft", "reviewed", "integrated"],
|
|
1594
|
+
defaultValue: "draft"
|
|
1595
|
+
},
|
|
1596
|
+
{
|
|
1597
|
+
name: "eventTypeId",
|
|
1598
|
+
type: "string|null",
|
|
1599
|
+
required: false,
|
|
1600
|
+
description: "Known event type id if already cataloged.",
|
|
1601
|
+
defaultValue: null,
|
|
1602
|
+
nullable: true
|
|
1603
|
+
},
|
|
1604
|
+
{
|
|
1605
|
+
name: "customEventType",
|
|
1606
|
+
type: "string",
|
|
1607
|
+
required: false,
|
|
1608
|
+
description: "Free-text event type when no existing type fits.",
|
|
1609
|
+
defaultValue: ""
|
|
1610
|
+
},
|
|
1611
|
+
{
|
|
1612
|
+
name: "eventSituation",
|
|
1613
|
+
type: "string",
|
|
1614
|
+
required: false,
|
|
1615
|
+
description: "What happened in the situation.",
|
|
1616
|
+
defaultValue: ""
|
|
1617
|
+
},
|
|
1618
|
+
{
|
|
1619
|
+
name: "occurredAt",
|
|
1620
|
+
type: "string|null",
|
|
1621
|
+
required: false,
|
|
1622
|
+
description: "When it happened.",
|
|
1623
|
+
defaultValue: null,
|
|
1624
|
+
nullable: true
|
|
1625
|
+
},
|
|
1626
|
+
{
|
|
1627
|
+
name: "emotions",
|
|
1628
|
+
type: "array",
|
|
1629
|
+
required: false,
|
|
1630
|
+
description: "List of { emotionDefinitionId|null, label, intensity 0-100, note } items.",
|
|
1631
|
+
defaultValue: []
|
|
1632
|
+
},
|
|
1633
|
+
{
|
|
1634
|
+
name: "thoughts",
|
|
1635
|
+
type: "array",
|
|
1636
|
+
required: false,
|
|
1637
|
+
description: "List of { text, parentMode, criticMode, beliefId|null } items.",
|
|
1638
|
+
defaultValue: []
|
|
1639
|
+
},
|
|
1640
|
+
{
|
|
1641
|
+
name: "behaviors",
|
|
1642
|
+
type: "array",
|
|
1643
|
+
required: false,
|
|
1644
|
+
description: "List of { text, mode, behaviorId|null } items.",
|
|
1645
|
+
defaultValue: []
|
|
1646
|
+
},
|
|
1647
|
+
{
|
|
1648
|
+
name: "consequences",
|
|
1649
|
+
type: "object",
|
|
1650
|
+
required: false,
|
|
1651
|
+
description: "Object with selfShortTerm, selfLongTerm, othersShortTerm, othersLongTerm string arrays."
|
|
1652
|
+
},
|
|
1653
|
+
{
|
|
1654
|
+
name: "linkedPatternIds",
|
|
1655
|
+
type: "string[]",
|
|
1656
|
+
required: false,
|
|
1657
|
+
description: "Linked pattern ids.",
|
|
1658
|
+
defaultValue: []
|
|
1659
|
+
},
|
|
1660
|
+
{
|
|
1661
|
+
name: "linkedValueIds",
|
|
1662
|
+
type: "string[]",
|
|
1663
|
+
required: false,
|
|
1664
|
+
description: "Linked value ids.",
|
|
1665
|
+
defaultValue: []
|
|
1666
|
+
},
|
|
1667
|
+
{
|
|
1668
|
+
name: "linkedGoalIds",
|
|
1669
|
+
type: "string[]",
|
|
1670
|
+
required: false,
|
|
1671
|
+
description: "Linked goal ids.",
|
|
1672
|
+
defaultValue: []
|
|
1673
|
+
},
|
|
1674
|
+
{
|
|
1675
|
+
name: "linkedProjectIds",
|
|
1676
|
+
type: "string[]",
|
|
1677
|
+
required: false,
|
|
1678
|
+
description: "Linked project ids.",
|
|
1679
|
+
defaultValue: []
|
|
1680
|
+
},
|
|
1681
|
+
{
|
|
1682
|
+
name: "linkedTaskIds",
|
|
1683
|
+
type: "string[]",
|
|
1684
|
+
required: false,
|
|
1685
|
+
description: "Linked task ids.",
|
|
1686
|
+
defaultValue: []
|
|
1687
|
+
},
|
|
1688
|
+
{
|
|
1689
|
+
name: "linkedBehaviorIds",
|
|
1690
|
+
type: "string[]",
|
|
1691
|
+
required: false,
|
|
1692
|
+
description: "Linked behavior ids.",
|
|
1693
|
+
defaultValue: []
|
|
1694
|
+
},
|
|
1695
|
+
{
|
|
1696
|
+
name: "linkedBeliefIds",
|
|
1697
|
+
type: "string[]",
|
|
1698
|
+
required: false,
|
|
1699
|
+
description: "Linked belief ids.",
|
|
1700
|
+
defaultValue: []
|
|
1701
|
+
},
|
|
1702
|
+
{
|
|
1703
|
+
name: "linkedModeIds",
|
|
1704
|
+
type: "string[]",
|
|
1705
|
+
required: false,
|
|
1706
|
+
description: "Linked mode ids.",
|
|
1707
|
+
defaultValue: []
|
|
1708
|
+
},
|
|
1709
|
+
{
|
|
1710
|
+
name: "modeOverlays",
|
|
1711
|
+
type: "string[]",
|
|
1712
|
+
required: false,
|
|
1713
|
+
description: "Extra mode labels noticed during the incident.",
|
|
1714
|
+
defaultValue: []
|
|
1715
|
+
},
|
|
1716
|
+
{
|
|
1717
|
+
name: "schemaLinks",
|
|
1718
|
+
type: "string[]",
|
|
1719
|
+
required: false,
|
|
1720
|
+
description: "Schema names or themes that seem related to the incident.",
|
|
1721
|
+
defaultValue: []
|
|
1722
|
+
},
|
|
1723
|
+
{
|
|
1724
|
+
name: "modeTimeline",
|
|
1725
|
+
type: "array",
|
|
1726
|
+
required: false,
|
|
1727
|
+
description: "List of { stage, modeId|null, label, note } items describing the sequence of modes.",
|
|
1728
|
+
defaultValue: []
|
|
1729
|
+
},
|
|
1730
|
+
{
|
|
1731
|
+
name: "nextMoves",
|
|
1732
|
+
type: "string[]",
|
|
1733
|
+
required: false,
|
|
1734
|
+
description: "Concrete next steps or repair moves.",
|
|
1735
|
+
defaultValue: []
|
|
1736
|
+
}
|
|
435
1737
|
]
|
|
436
1738
|
}
|
|
437
1739
|
];
|
|
@@ -449,7 +1751,17 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
|
|
|
449
1751
|
"Name the preferred alternative response."
|
|
450
1752
|
],
|
|
451
1753
|
requiredForCreate: ["title"],
|
|
452
|
-
highValueOptionalFields: [
|
|
1754
|
+
highValueOptionalFields: [
|
|
1755
|
+
"description",
|
|
1756
|
+
"targetBehavior",
|
|
1757
|
+
"cueContexts",
|
|
1758
|
+
"shortTermPayoff",
|
|
1759
|
+
"longTermCost",
|
|
1760
|
+
"preferredResponse",
|
|
1761
|
+
"linkedBeliefIds",
|
|
1762
|
+
"linkedModeIds",
|
|
1763
|
+
"linkedValueIds"
|
|
1764
|
+
],
|
|
453
1765
|
exampleQuestions: [
|
|
454
1766
|
"What usually sets this loop off?",
|
|
455
1767
|
"What do you tend to do next, outwardly or inwardly?",
|
|
@@ -475,7 +1787,17 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
|
|
|
475
1787
|
"Link a schemaId only when a real schema catalog match is known."
|
|
476
1788
|
],
|
|
477
1789
|
requiredForCreate: ["statement", "beliefType"],
|
|
478
|
-
highValueOptionalFields: [
|
|
1790
|
+
highValueOptionalFields: [
|
|
1791
|
+
"schemaId",
|
|
1792
|
+
"confidence",
|
|
1793
|
+
"originNote",
|
|
1794
|
+
"evidenceFor",
|
|
1795
|
+
"evidenceAgainst",
|
|
1796
|
+
"flexibleAlternative",
|
|
1797
|
+
"linkedReportIds",
|
|
1798
|
+
"linkedBehaviorIds",
|
|
1799
|
+
"linkedModeIds"
|
|
1800
|
+
],
|
|
479
1801
|
exampleQuestions: [
|
|
480
1802
|
"What is the sentence your mind seems to be pushing here?",
|
|
481
1803
|
"Is it more of an always/never belief, or an if-then rule?",
|
|
@@ -500,7 +1822,17 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
|
|
|
500
1822
|
"Optionally note origin context and linked patterns or behaviors."
|
|
501
1823
|
],
|
|
502
1824
|
requiredForCreate: ["family", "title"],
|
|
503
|
-
highValueOptionalFields: [
|
|
1825
|
+
highValueOptionalFields: [
|
|
1826
|
+
"persona",
|
|
1827
|
+
"imagery",
|
|
1828
|
+
"fear",
|
|
1829
|
+
"burden",
|
|
1830
|
+
"protectiveJob",
|
|
1831
|
+
"originContext",
|
|
1832
|
+
"linkedPatternIds",
|
|
1833
|
+
"linkedBehaviorIds",
|
|
1834
|
+
"linkedValueIds"
|
|
1835
|
+
],
|
|
504
1836
|
exampleQuestions: [
|
|
505
1837
|
"What kind of part does this feel like: coping, child, critic-parent, healthy-adult, or happy-child?",
|
|
506
1838
|
"If you gave this mode a name, what would it be?",
|
|
@@ -526,7 +1858,22 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
|
|
|
526
1858
|
"Identify next moves and linked patterns, beliefs, modes, values, or tasks."
|
|
527
1859
|
],
|
|
528
1860
|
requiredForCreate: ["title"],
|
|
529
|
-
highValueOptionalFields: [
|
|
1861
|
+
highValueOptionalFields: [
|
|
1862
|
+
"eventTypeId",
|
|
1863
|
+
"customEventType",
|
|
1864
|
+
"eventSituation",
|
|
1865
|
+
"occurredAt",
|
|
1866
|
+
"emotions",
|
|
1867
|
+
"thoughts",
|
|
1868
|
+
"behaviors",
|
|
1869
|
+
"consequences",
|
|
1870
|
+
"modeTimeline",
|
|
1871
|
+
"nextMoves",
|
|
1872
|
+
"linkedPatternIds",
|
|
1873
|
+
"linkedBeliefIds",
|
|
1874
|
+
"linkedModeIds",
|
|
1875
|
+
"linkedValueIds"
|
|
1876
|
+
],
|
|
530
1877
|
exampleQuestions: [
|
|
531
1878
|
"What happened, as concretely as you can say it?",
|
|
532
1879
|
"What emotions were there, and how intense were they?",
|
|
@@ -559,11 +1906,16 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
559
1906
|
summary: "Create one or more entities in one ordered batch.",
|
|
560
1907
|
whenToUse: "Use after explicit save intent and after duplicate checks when needed.",
|
|
561
1908
|
inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, clientRef?: string, data: object }> }",
|
|
562
|
-
requiredFields: [
|
|
1909
|
+
requiredFields: [
|
|
1910
|
+
"operations",
|
|
1911
|
+
"operations[].entityType",
|
|
1912
|
+
"operations[].data"
|
|
1913
|
+
],
|
|
563
1914
|
notes: [
|
|
564
1915
|
"entityType alone is never enough; full data is required.",
|
|
565
1916
|
"Batch multiple related creates together when they come from one user ask.",
|
|
566
|
-
"Goal, project, and task creates can include notes: [{ contentMarkdown, author?, links? }] and Forge will auto-link those notes to the newly created entity."
|
|
1917
|
+
"Goal, project, and task creates can include notes: [{ contentMarkdown, author?, tags?, destroyAt?, links? }] and Forge will auto-link those notes to the newly created entity.",
|
|
1918
|
+
"The same batch create route also handles calendar_event, work_block_template, and task_timebox. Calendar-event creates still trigger downstream projection sync when a writable provider calendar is selected."
|
|
567
1919
|
],
|
|
568
1920
|
example: '{"operations":[{"entityType":"task","data":{"title":"Write the public release notes","projectId":"project_123","status":"focus","notes":[{"contentMarkdown":"Starting from the changelog draft and the last QA pass."}]},"clientRef":"task-1"}]}'
|
|
569
1921
|
},
|
|
@@ -572,17 +1924,37 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
572
1924
|
summary: "Patch one or more entities in one ordered batch.",
|
|
573
1925
|
whenToUse: "Use when ids are known and the user explicitly wants a change persisted.",
|
|
574
1926
|
inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, id: string, clientRef?: string, patch: object }> }",
|
|
575
|
-
requiredFields: [
|
|
576
|
-
|
|
577
|
-
|
|
1927
|
+
requiredFields: [
|
|
1928
|
+
"operations",
|
|
1929
|
+
"operations[].entityType",
|
|
1930
|
+
"operations[].id",
|
|
1931
|
+
"operations[].patch"
|
|
1932
|
+
],
|
|
1933
|
+
notes: [
|
|
1934
|
+
"patch is partial; only send the fields that should change.",
|
|
1935
|
+
"Project lifecycle is status-driven: patch project.status to active, paused, or completed instead of looking for separate suspend, restart, or finish routes.",
|
|
1936
|
+
"Setting project.status to completed finishes the project and auto-completes linked unfinished tasks through the normal task completion path.",
|
|
1937
|
+
"Task and project scheduling rules stay on these same entity patches. Update task.schedulingRules, task.plannedDurationSeconds, or project.schedulingRules here.",
|
|
1938
|
+
"Use this same route to move or relink calendar_event records and to edit work_block_template or task_timebox records without switching to narrower calendar CRUD tools."
|
|
1939
|
+
],
|
|
1940
|
+
example: '{"operations":[{"entityType":"project","id":"project_123","patch":{"status":"completed"},"clientRef":"project-finish-1"}]}'
|
|
578
1941
|
},
|
|
579
1942
|
{
|
|
580
1943
|
toolName: "forge_delete_entities",
|
|
581
1944
|
summary: "Delete one or more entities through the batch delete flow.",
|
|
582
1945
|
whenToUse: "Use for explicit delete intent only.",
|
|
583
|
-
inputShape:
|
|
584
|
-
requiredFields: [
|
|
585
|
-
|
|
1946
|
+
inputShape: '{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, id: string, clientRef?: string, mode?: "soft"|"hard", reason?: string }> }',
|
|
1947
|
+
requiredFields: [
|
|
1948
|
+
"operations",
|
|
1949
|
+
"operations[].entityType",
|
|
1950
|
+
"operations[].id"
|
|
1951
|
+
],
|
|
1952
|
+
notes: [
|
|
1953
|
+
"Delete defaults to soft.",
|
|
1954
|
+
"Use mode=hard only for explicit permanent removal.",
|
|
1955
|
+
"Restoration is only possible after soft delete.",
|
|
1956
|
+
"calendar_event, work_block_template, and task_timebox are immediate calendar-domain deletions: calendar events delete remote projections too, and these records do not go through the settings bin."
|
|
1957
|
+
],
|
|
586
1958
|
example: '{"operations":[{"entityType":"task","id":"task_123","mode":"soft","reason":"Merged into another task"}]}'
|
|
587
1959
|
},
|
|
588
1960
|
{
|
|
@@ -590,36 +1962,152 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
590
1962
|
summary: "Restore soft-deleted entities from the settings bin.",
|
|
591
1963
|
whenToUse: "Use when the user wants an entity brought back after a soft delete.",
|
|
592
1964
|
inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, id: string, clientRef?: string }> }",
|
|
593
|
-
requiredFields: [
|
|
1965
|
+
requiredFields: [
|
|
1966
|
+
"operations",
|
|
1967
|
+
"operations[].entityType",
|
|
1968
|
+
"operations[].id"
|
|
1969
|
+
],
|
|
594
1970
|
notes: ["Restore only works for soft-deleted entities."],
|
|
595
1971
|
example: '{"operations":[{"entityType":"goal","id":"goal_123","clientRef":"goal-restore-1"}]}'
|
|
596
1972
|
},
|
|
1973
|
+
{
|
|
1974
|
+
toolName: "forge_get_calendar_overview",
|
|
1975
|
+
summary: "Read connected calendars, Forge-native events, mirrored events, recurring work blocks, and task timeboxes together.",
|
|
1976
|
+
whenToUse: "Use before calendar-aware planning, slot selection, or scheduling diagnostics.",
|
|
1977
|
+
inputShape: "{ from?: string, to?: string }",
|
|
1978
|
+
requiredFields: [],
|
|
1979
|
+
notes: [
|
|
1980
|
+
"Use ISO datetimes.",
|
|
1981
|
+
"The response includes provider metadata, live connections, mirrored external events, derived work-block instances, and task timeboxes."
|
|
1982
|
+
],
|
|
1983
|
+
example: '{"from":"2026-04-02T00:00:00.000Z","to":"2026-04-09T00:00:00.000Z"}'
|
|
1984
|
+
},
|
|
1985
|
+
{
|
|
1986
|
+
toolName: "forge_connect_calendar_provider",
|
|
1987
|
+
summary: "Create a Forge calendar connection for Google, Apple, Exchange Online, or custom CalDAV.",
|
|
1988
|
+
whenToUse: "Use only when the operator explicitly wants Forge connected to an external calendar provider.",
|
|
1989
|
+
inputShape: '{ provider: "google"|"apple"|"caldav"|"microsoft", label: string, username?: string, clientId?: string, clientSecret?: string, refreshToken?: string, password?: string, serverUrl?: string, authSessionId?: string, selectedCalendarUrls: string[], forgeCalendarUrl?: string, createForgeCalendar?: boolean }',
|
|
1990
|
+
requiredFields: ["provider", "label", "provider-specific credentials"],
|
|
1991
|
+
notes: [
|
|
1992
|
+
"Google uses OAuth client credentials plus a refresh token.",
|
|
1993
|
+
"Apple starts from https://caldav.icloud.com and autodiscovers the principal plus calendars after authentication.",
|
|
1994
|
+
"Exchange Online uses Microsoft Graph. In the current Forge implementation it is read-only: Forge mirrors the selected calendars but does not publish work blocks or timeboxes back to Microsoft.",
|
|
1995
|
+
"In the current self-hosted local runtime, Exchange Online now uses an interactive Microsoft public-client sign-in flow with PKCE after the operator has saved the Microsoft client ID, tenant, and redirect URI in Settings -> Calendar. Non-interactive callers should treat Microsoft connection setup as a Settings-owned operator action unless a completed authSessionId already exists.",
|
|
1996
|
+
"Custom CalDAV uses an account-level server URL, not a single calendar collection URL.",
|
|
1997
|
+
"Writable providers publish Forge work blocks and timeboxes to the dedicated Forge calendar for that connection."
|
|
1998
|
+
],
|
|
1999
|
+
example: '{"provider":"apple","label":"Primary Apple","username":"operator@example.com","password":"app-password","selectedCalendarUrls":["https://caldav.icloud.com/.../Family/"],"forgeCalendarUrl":"https://caldav.icloud.com/.../Forge/","createForgeCalendar":false}'
|
|
2000
|
+
},
|
|
2001
|
+
{
|
|
2002
|
+
toolName: "forge_create_work_block_template",
|
|
2003
|
+
summary: "Create a recurring half-day, holiday, or custom work-block template.",
|
|
2004
|
+
whenToUse: "Use when the operator wants recurring time windows such as Main Activity, Secondary Activity, Third Activity, Rest, Holiday, or a custom block.",
|
|
2005
|
+
inputShape: '{ title: string, kind: "main_activity"|"secondary_activity"|"third_activity"|"rest"|"holiday"|"custom", color: string, timezone: string, weekDays: integer[], startMinute: integer, endMinute: integer, startsOn?: "YYYY-MM-DD"|null, endsOn?: "YYYY-MM-DD"|null, blockingState: "allowed"|"blocked" }',
|
|
2006
|
+
requiredFields: [
|
|
2007
|
+
"title",
|
|
2008
|
+
"kind",
|
|
2009
|
+
"timezone",
|
|
2010
|
+
"weekDays",
|
|
2011
|
+
"startMinute",
|
|
2012
|
+
"endMinute",
|
|
2013
|
+
"blockingState"
|
|
2014
|
+
],
|
|
2015
|
+
notes: [
|
|
2016
|
+
"Minutes are measured from midnight in the selected timezone.",
|
|
2017
|
+
"startsOn and endsOn are optional date bounds. Leaving endsOn null makes the block repeat indefinitely.",
|
|
2018
|
+
"Use kind=holiday with weekDays [0,1,2,3,4,5,6] and minutes 0-1440 for vacations or other full-day blocked ranges.",
|
|
2019
|
+
"Derived instances appear in calendar overview responses immediately after creation.",
|
|
2020
|
+
"This is a convenience helper; agents can also create work_block_template through forge_create_entities."
|
|
2021
|
+
],
|
|
2022
|
+
example: '{"title":"Summer holiday","kind":"holiday","color":"#14b8a6","timezone":"Europe/Zurich","weekDays":[0,1,2,3,4,5,6],"startMinute":0,"endMinute":1440,"startsOn":"2026-08-01","endsOn":"2026-08-16","blockingState":"blocked"}'
|
|
2023
|
+
},
|
|
2024
|
+
{
|
|
2025
|
+
toolName: "forge_recommend_task_timeboxes",
|
|
2026
|
+
summary: "Suggest future task slots that fit the current calendar rules and schedule.",
|
|
2027
|
+
whenToUse: "Use when preparing focused work in advance.",
|
|
2028
|
+
inputShape: "{ taskId: string, from?: string, to?: string, limit?: integer }",
|
|
2029
|
+
requiredFields: ["taskId"],
|
|
2030
|
+
notes: [
|
|
2031
|
+
"Recommendations consider mirrored calendar events, recurring work blocks, task or project scheduling rules, and the task's planned duration when available.",
|
|
2032
|
+
"Confirm a suggested slot by creating a task timebox."
|
|
2033
|
+
],
|
|
2034
|
+
example: '{"taskId":"task_123","from":"2026-04-02T00:00:00.000Z","to":"2026-04-09T00:00:00.000Z","limit":6}'
|
|
2035
|
+
},
|
|
2036
|
+
{
|
|
2037
|
+
toolName: "forge_create_task_timebox",
|
|
2038
|
+
summary: "Create a planned task timebox in the Forge calendar domain.",
|
|
2039
|
+
whenToUse: "Use after choosing a valid future slot or when creating a manual timebox directly.",
|
|
2040
|
+
inputShape: '{ taskId: string, projectId?: string|null, title: string, startsAt: string, endsAt: string, source?: "manual"|"suggested"|"live_run" }',
|
|
2041
|
+
requiredFields: ["taskId", "title", "startsAt", "endsAt"],
|
|
2042
|
+
notes: [
|
|
2043
|
+
"Forge publishes these into the dedicated Forge calendar during provider sync.",
|
|
2044
|
+
"Live task runs can later attach to matching timeboxes.",
|
|
2045
|
+
"This is a convenience helper; agents can also create task_timebox through forge_create_entities."
|
|
2046
|
+
],
|
|
2047
|
+
example: '{"taskId":"task_123","projectId":"project_456","title":"Draft the methods section","startsAt":"2026-04-03T08:00:00.000Z","endsAt":"2026-04-03T09:30:00.000Z","source":"suggested"}'
|
|
2048
|
+
},
|
|
2049
|
+
{
|
|
2050
|
+
toolName: "forge_grant_reward_bonus",
|
|
2051
|
+
summary: "Grant an explicit manual XP bonus or penalty with clear provenance.",
|
|
2052
|
+
whenToUse: "Use when the user or operator explicitly wants an auditable reward adjustment beyond the automatic task and habit reward paths.",
|
|
2053
|
+
inputShape: "{ entityType: RewardableEntityType, entityId: string, deltaXp: integer, reasonTitle: string, reasonSummary?: string, metadata?: object }",
|
|
2054
|
+
requiredFields: ["entityType", "entityId", "deltaXp", "reasonTitle"],
|
|
2055
|
+
notes: [
|
|
2056
|
+
"Requires rewards.manage and write scopes.",
|
|
2057
|
+
"Use this for explicit operator judgement, not as a substitute for normal task_run or habit check-in rewards."
|
|
2058
|
+
],
|
|
2059
|
+
example: '{"entityType":"habit","entityId":"habit_morning_training","deltaXp":18,"reasonTitle":"Operator bonus","reasonSummary":"Stayed with the habit through unusual travel friction.","metadata":{"manual":true,"source":"agent"}}'
|
|
2060
|
+
},
|
|
597
2061
|
{
|
|
598
2062
|
toolName: "forge_post_insight",
|
|
599
2063
|
summary: "Store an agent-authored insight.",
|
|
600
2064
|
whenToUse: "Use when you have a data-grounded observation or recommendation worth keeping visible in Forge.",
|
|
601
2065
|
inputShape: "{ entityType?: string|null, entityId?: string|null, timeframeLabel?: string|null, title: string, summary: string, recommendation: string, rationale?: string, confidence?: number, visibility?: string, ctaLabel?: string }",
|
|
602
2066
|
requiredFields: ["title", "summary", "recommendation"],
|
|
603
|
-
notes: [
|
|
2067
|
+
notes: [
|
|
2068
|
+
"Insights are for interpretation and advice, not for replacing user-owned goals or tasks."
|
|
2069
|
+
],
|
|
604
2070
|
example: '{"entityType":"goal","entityId":"goal_123","title":"Admin drag is masking momentum","summary":"Creative progress is happening, but admin cleanup keeps interrupting it.","recommendation":"Protect one clean creative block and isolate admin into a separate recurring task.","confidence":0.82}'
|
|
605
2071
|
},
|
|
2072
|
+
{
|
|
2073
|
+
toolName: "forge_adjust_work_minutes",
|
|
2074
|
+
summary: "Add or remove tracked work minutes on a task or project without creating a live task run.",
|
|
2075
|
+
whenToUse: "Use for truthful retrospective minute corrections. Use this instead of forge_log_work when the task or project already exists and only tracked minutes need adjusting.",
|
|
2076
|
+
inputShape: '{ entityType: "task"|"project", entityId: string, deltaMinutes: integer, note?: string }',
|
|
2077
|
+
requiredFields: ["entityType", "entityId", "deltaMinutes"],
|
|
2078
|
+
notes: [
|
|
2079
|
+
"Positive deltaMinutes add tracked minutes and may award XP when a progress bucket is crossed.",
|
|
2080
|
+
"Negative deltaMinutes remove tracked minutes and may reverse XP symmetrically when a progress bucket is crossed downward.",
|
|
2081
|
+
"Requires rewards.manage and write scopes."
|
|
2082
|
+
],
|
|
2083
|
+
example: '{"entityType":"task","entityId":"task_123","deltaMinutes":25,"note":"Captured the off-timer review pass from this morning."}'
|
|
2084
|
+
},
|
|
606
2085
|
{
|
|
607
2086
|
toolName: "forge_log_work",
|
|
608
2087
|
summary: "Log work that already happened.",
|
|
609
|
-
whenToUse: "Use for retroactive work, not for starting a live session.",
|
|
610
|
-
inputShape: "{ taskId?: string, title?: string, description?: string, summary?: string, goalId?: string|null, projectId?: string|null, owner?: string, status?: TaskStatus, priority?: TaskPriority, dueDate?: string|null, effort?: TaskEffort, energy?: TaskEnergy, points?: number, tagIds?: string[], closeoutNote?: { contentMarkdown: string, author?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
|
|
2088
|
+
whenToUse: "Use for completion-style retroactive work, not for starting a live session or adjusting minutes on an existing record.",
|
|
2089
|
+
inputShape: "{ taskId?: string, title?: string, description?: string, summary?: string, goalId?: string|null, projectId?: string|null, owner?: string, status?: TaskStatus, priority?: TaskPriority, dueDate?: string|null, effort?: TaskEffort, energy?: TaskEnergy, points?: number, tagIds?: string[], closeoutNote?: { contentMarkdown: string, author?: string|null, tags?: string[], destroyAt?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
|
|
611
2090
|
requiredFields: ["taskId or title"],
|
|
612
|
-
notes: [
|
|
2091
|
+
notes: [
|
|
2092
|
+
"Use taskId when logging work against an existing task.",
|
|
2093
|
+
"Use title when a new completed work item should be created and logged.",
|
|
2094
|
+
"Use forge_adjust_work_minutes for signed minute corrections on existing tasks or projects.",
|
|
2095
|
+
"closeoutNote persists the work summary as a real linked note."
|
|
2096
|
+
],
|
|
613
2097
|
example: '{"taskId":"task_123","summary":"Finished the review draft and cleaned the notes.","points":40,"closeoutNote":{"contentMarkdown":"Finished the review draft, cleaned the note structure, and left one follow-up for QA."}}'
|
|
614
2098
|
},
|
|
615
2099
|
{
|
|
616
2100
|
toolName: "forge_start_task_run",
|
|
617
2101
|
summary: "Start truthful live work on a task.",
|
|
618
2102
|
whenToUse: "Use when the user wants to begin working now.",
|
|
619
|
-
inputShape:
|
|
2103
|
+
inputShape: '{ taskId: string, actor: string, timerMode?: "planned"|"unlimited", plannedDurationSeconds?: number|null, overrideReason?: string|null, isCurrent?: boolean, leaseTtlSeconds?: number, note?: string }',
|
|
620
2104
|
requiredFields: ["taskId", "actor"],
|
|
621
|
-
notes: [
|
|
622
|
-
|
|
2105
|
+
notes: [
|
|
2106
|
+
"If timerMode is planned, plannedDurationSeconds is required.",
|
|
2107
|
+
"If timerMode is unlimited, plannedDurationSeconds must be null or omitted.",
|
|
2108
|
+
"If calendar rules currently block the task, pass an explicit overrideReason to proceed and keep the exception auditable."
|
|
2109
|
+
],
|
|
2110
|
+
example: '{"taskId":"task_123","actor":"aurel","timerMode":"planned","plannedDurationSeconds":1500,"overrideReason":"Protected creative block after clinic hours.","isCurrent":true,"leaseTtlSeconds":900,"note":"Starting focused writing block"}'
|
|
623
2111
|
},
|
|
624
2112
|
{
|
|
625
2113
|
toolName: "forge_heartbeat_task_run",
|
|
@@ -636,25 +2124,33 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
|
|
|
636
2124
|
whenToUse: "Use when several runs exist and one should be the visible current run.",
|
|
637
2125
|
inputShape: "{ taskRunId: string, actor?: string }",
|
|
638
2126
|
requiredFields: ["taskRunId"],
|
|
639
|
-
notes: [
|
|
2127
|
+
notes: [
|
|
2128
|
+
"This does not complete or release a run; it just changes current focus."
|
|
2129
|
+
],
|
|
640
2130
|
example: '{"taskRunId":"run_123","actor":"aurel"}'
|
|
641
2131
|
},
|
|
642
2132
|
{
|
|
643
2133
|
toolName: "forge_complete_task_run",
|
|
644
2134
|
summary: "Finish an active run as completed work.",
|
|
645
2135
|
whenToUse: "Use when the user has finished the live work block.",
|
|
646
|
-
inputShape: "{ taskRunId: string, actor?: string, note?: string, closeoutNote?: { contentMarkdown: string, author?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
|
|
2136
|
+
inputShape: "{ taskRunId: string, actor?: string, note?: string, closeoutNote?: { contentMarkdown: string, author?: string|null, tags?: string[], destroyAt?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
|
|
647
2137
|
requiredFields: ["taskRunId"],
|
|
648
|
-
notes: [
|
|
2138
|
+
notes: [
|
|
2139
|
+
"This is the truthful way to finish live work and award completion effects.",
|
|
2140
|
+
"closeoutNote persists a real linked note instead of only updating the transient run note."
|
|
2141
|
+
],
|
|
649
2142
|
example: '{"taskRunId":"run_123","actor":"aurel","note":"Finished the review draft","closeoutNote":{"contentMarkdown":"Completed the draft review and listed the follow-up fixes."}}'
|
|
650
2143
|
},
|
|
651
2144
|
{
|
|
652
2145
|
toolName: "forge_release_task_run",
|
|
653
2146
|
summary: "Stop an active run without marking the task complete.",
|
|
654
2147
|
whenToUse: "Use when the user is stopping or pausing work without completion.",
|
|
655
|
-
inputShape: "{ taskRunId: string, actor?: string, note?: string, closeoutNote?: { contentMarkdown: string, author?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
|
|
2148
|
+
inputShape: "{ taskRunId: string, actor?: string, note?: string, closeoutNote?: { contentMarkdown: string, author?: string|null, tags?: string[], destroyAt?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
|
|
656
2149
|
requiredFields: ["taskRunId"],
|
|
657
|
-
notes: [
|
|
2150
|
+
notes: [
|
|
2151
|
+
"Use this instead of faking a stop by only changing task status.",
|
|
2152
|
+
"closeoutNote is useful for documenting blockers or handoff context."
|
|
2153
|
+
],
|
|
658
2154
|
example: '{"taskRunId":"run_123","actor":"aurel","note":"Stopping for now; blocked on feedback","closeoutNote":{"contentMarkdown":"Blocked on feedback from design before I can continue."}}'
|
|
659
2155
|
}
|
|
660
2156
|
];
|
|
@@ -712,11 +2208,14 @@ function buildAgentOnboardingPayload(request) {
|
|
|
712
2208
|
},
|
|
713
2209
|
conceptModel: {
|
|
714
2210
|
goal: "Long-horizon direction or outcome. Goals anchor projects and sometimes tasks directly.",
|
|
715
|
-
project: "A multi-step workstream under one goal. Projects organize related tasks.",
|
|
2211
|
+
project: "A multi-step workstream under one goal. Projects organize related tasks. Project lifecycle is driven by status: active means in play, paused means suspended, and completed means finished. Setting a project to completed auto-completes linked unfinished tasks.",
|
|
716
2212
|
task: "A concrete actionable work item. Task status is board state, not proof of live work.",
|
|
717
2213
|
taskRun: "A live work session attached to a task. Start, heartbeat, focus, complete, and release runs instead of faking work with status alone.",
|
|
718
2214
|
note: "A Markdown work note that can link to one or many entities. Use notes for progress evidence, context, and close-out summaries.",
|
|
719
2215
|
insight: "An agent-authored observation or recommendation grounded in Forge data.",
|
|
2216
|
+
calendar: "A connected calendar source mirrored into Forge. Calendar state combines provider events, recurring work blocks, and task timeboxes.",
|
|
2217
|
+
workBlock: "A recurring half-day or custom time window such as Main Activity, Secondary Activity, Third Activity, Rest, Holiday, or Custom. Work blocks can allow or block work by default, can define active date bounds, and remain editable through the calendar surface.",
|
|
2218
|
+
taskTimebox: "A planned or live calendar slot tied to a task. Timeboxes can be suggested in advance or created automatically from active task runs.",
|
|
720
2219
|
psyche: "Forge Psyche is the reflective domain for values, patterns, behaviors, beliefs, modes, and trigger reports. It is sensitive and should be handled deliberately."
|
|
721
2220
|
},
|
|
722
2221
|
psycheSubmoduleModel: {
|
|
@@ -736,6 +2235,7 @@ function buildAgentOnboardingPayload(request) {
|
|
|
736
2235
|
"Goals are the top-level strategic layer.",
|
|
737
2236
|
"Projects belong to one goal through goalId.",
|
|
738
2237
|
"Tasks can belong to a goal, a project, both, or neither.",
|
|
2238
|
+
"Habits are recurring records that can connect directly to goals, projects, tasks, and durable Psyche entities.",
|
|
739
2239
|
"Task runs represent live work sessions on tasks and are separate from task status.",
|
|
740
2240
|
"Notes can link to one or many entities and are the canonical place for Markdown progress context or close-out evidence.",
|
|
741
2241
|
"Psyche values can link to goals, projects, and tasks.",
|
|
@@ -748,6 +2248,7 @@ function buildAgentOnboardingPayload(request) {
|
|
|
748
2248
|
context: "/api/v1/context",
|
|
749
2249
|
xpMetrics: "/api/v1/metrics/xp",
|
|
750
2250
|
weeklyReview: "/api/v1/reviews/weekly",
|
|
2251
|
+
calendarOverview: "/api/v1/calendar/overview",
|
|
751
2252
|
settingsBin: "/api/v1/settings/bin",
|
|
752
2253
|
batchSearch: "/api/v1/entities/search",
|
|
753
2254
|
psycheSchemaCatalog: "/api/v1/psyche/schema-catalog",
|
|
@@ -771,7 +2272,9 @@ function buildAgentOnboardingPayload(request) {
|
|
|
771
2272
|
"forge_delete_entities",
|
|
772
2273
|
"forge_restore_entities"
|
|
773
2274
|
],
|
|
2275
|
+
rewardWorkflow: ["forge_grant_reward_bonus"],
|
|
774
2276
|
workWorkflow: [
|
|
2277
|
+
"forge_adjust_work_minutes",
|
|
775
2278
|
"forge_log_work",
|
|
776
2279
|
"forge_start_task_run",
|
|
777
2280
|
"forge_heartbeat_task_run",
|
|
@@ -779,6 +2282,14 @@ function buildAgentOnboardingPayload(request) {
|
|
|
779
2282
|
"forge_complete_task_run",
|
|
780
2283
|
"forge_release_task_run"
|
|
781
2284
|
],
|
|
2285
|
+
calendarWorkflow: [
|
|
2286
|
+
"forge_get_calendar_overview",
|
|
2287
|
+
"forge_connect_calendar_provider",
|
|
2288
|
+
"forge_sync_calendar_connection",
|
|
2289
|
+
"forge_create_work_block_template",
|
|
2290
|
+
"forge_recommend_task_timeboxes",
|
|
2291
|
+
"forge_create_task_timebox"
|
|
2292
|
+
],
|
|
782
2293
|
insightWorkflow: ["forge_post_insight"]
|
|
783
2294
|
},
|
|
784
2295
|
interactionGuidance: {
|
|
@@ -801,14 +2312,14 @@ function buildAgentOnboardingPayload(request) {
|
|
|
801
2312
|
},
|
|
802
2313
|
deleteDefault: "soft",
|
|
803
2314
|
hardDeleteRequiresExplicitMode: true,
|
|
804
|
-
restoreSummary: "Restore soft-deleted entities through the restore route or the settings bin.",
|
|
805
|
-
entityDeleteSummary: "Entity DELETE routes default to soft delete. Pass mode=hard only when permanent removal is intended.",
|
|
2315
|
+
restoreSummary: "Restore soft-deleted entities through the restore route or the settings bin. Calendar-domain deletes for calendar_event, work_block_template, and task_timebox are immediate and do not enter the bin.",
|
|
2316
|
+
entityDeleteSummary: "Entity DELETE routes default to soft delete. Pass mode=hard only when permanent removal is intended. Calendar-event deletes still remove remote projections downstream.",
|
|
806
2317
|
batchingRule: "forge_create_entities, forge_update_entities, forge_delete_entities, and forge_restore_entities all accept operations as arrays. Batch multiple related mutations together in one request when possible.",
|
|
807
2318
|
searchRule: "forge_search_entities accepts searches as an array. Search before create or update when duplicate risk exists.",
|
|
808
|
-
createRule: "Each create operation must include entityType and full data. entityType alone is not enough.",
|
|
809
|
-
updateRule: "Each update operation must include entityType, id, and patch.",
|
|
2319
|
+
createRule: "Each create operation must include entityType and full data. entityType alone is not enough. This includes calendar_event, work_block_template, and task_timebox alongside the usual planning and Psyche entities.",
|
|
2320
|
+
updateRule: "Each update operation must include entityType, id, and patch. For projects, lifecycle changes are status patches: active to restart, paused to suspend, completed to finish. Keep task and project scheduling rules on those same patch payloads. Calendar-event updates still run downstream provider projection sync.",
|
|
810
2321
|
createExample: '{"operations":[{"entityType":"goal","data":{"title":"Create meaningfully"},"clientRef":"goal-create-1"},{"entityType":"goal","data":{"title":"Build a beautiful family"},"clientRef":"goal-create-2"}]}',
|
|
811
|
-
updateExample: '{"operations":[{"entityType":"
|
|
2322
|
+
updateExample: '{"operations":[{"entityType":"project","id":"project_123","patch":{"status":"paused","schedulingRules":{"blockWorkBlockKinds":["main_activity"],"allowWorkBlockKinds":["secondary_activity"]}},"clientRef":"project-suspend-1"},{"entityType":"task","id":"task_456","patch":{"plannedDurationSeconds":5400,"schedulingRules":{"allowEventKeywords":["creative"],"blockEventKeywords":["clinic"]}},"clientRef":"task-scheduling-1"}]}'
|
|
812
2323
|
}
|
|
813
2324
|
};
|
|
814
2325
|
}
|
|
@@ -872,7 +2383,9 @@ function parseActivityContext(headers) {
|
|
|
872
2383
|
if (Array.isArray(rawSource)) {
|
|
873
2384
|
throw new Error("X-Forge-Source must be a single header value");
|
|
874
2385
|
}
|
|
875
|
-
const source = rawSource === undefined
|
|
2386
|
+
const source = rawSource === undefined
|
|
2387
|
+
? "ui"
|
|
2388
|
+
: activitySourceSchema.parse(typeof rawSource === "string" ? rawSource.trim() : rawSource);
|
|
876
2389
|
return {
|
|
877
2390
|
source,
|
|
878
2391
|
actor: parseOptionalActorHeader(headers)
|
|
@@ -915,7 +2428,8 @@ function hasTokenScope(token, scope) {
|
|
|
915
2428
|
return Boolean(token?.scopes.includes(scope));
|
|
916
2429
|
}
|
|
917
2430
|
function isPsycheEntityType(entityType) {
|
|
918
|
-
return Boolean(entityType &&
|
|
2431
|
+
return Boolean(entityType &&
|
|
2432
|
+
PSYCHE_ENTITY_TYPES.includes(entityType));
|
|
919
2433
|
}
|
|
920
2434
|
function getWatchdogHealth(taskRunWatchdog) {
|
|
921
2435
|
if (!taskRunWatchdog) {
|
|
@@ -955,7 +2469,17 @@ function buildHealthPayload(taskRunWatchdog, extras = {}) {
|
|
|
955
2469
|
...extras
|
|
956
2470
|
};
|
|
957
2471
|
}
|
|
2472
|
+
function shouldIncludeRuntimeProbe(headers) {
|
|
2473
|
+
const probeHeader = headers["x-forge-runtime-probe"];
|
|
2474
|
+
if (Array.isArray(probeHeader)) {
|
|
2475
|
+
return probeHeader.some((value) => typeof value === "string" && value.trim() === "1");
|
|
2476
|
+
}
|
|
2477
|
+
return typeof probeHeader === "string" && probeHeader.trim() === "1";
|
|
2478
|
+
}
|
|
958
2479
|
function buildV1Context() {
|
|
2480
|
+
const goals = listGoals();
|
|
2481
|
+
const tasks = listTasks();
|
|
2482
|
+
const habits = listHabits();
|
|
959
2483
|
return {
|
|
960
2484
|
meta: {
|
|
961
2485
|
apiVersion: "v1",
|
|
@@ -964,15 +2488,16 @@ function buildV1Context() {
|
|
|
964
2488
|
backend: "forge-node-runtime",
|
|
965
2489
|
mode: "transitional-node"
|
|
966
2490
|
},
|
|
967
|
-
metrics: buildGamificationProfile(
|
|
2491
|
+
metrics: buildGamificationProfile(goals, tasks, habits),
|
|
968
2492
|
dashboard: getDashboard(),
|
|
969
2493
|
overview: getOverviewContext(),
|
|
970
2494
|
today: getTodayContext(),
|
|
971
2495
|
risk: getRiskContext(),
|
|
972
|
-
goals
|
|
2496
|
+
goals,
|
|
973
2497
|
projects: listProjectSummaries(),
|
|
974
2498
|
tags: listTags(),
|
|
975
|
-
tasks
|
|
2499
|
+
tasks,
|
|
2500
|
+
habits,
|
|
976
2501
|
activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
|
|
977
2502
|
activity: listActivityEvents({ limit: 25 })
|
|
978
2503
|
};
|
|
@@ -980,8 +2505,9 @@ function buildV1Context() {
|
|
|
980
2505
|
function buildXpMetricsPayload() {
|
|
981
2506
|
const goals = listGoals();
|
|
982
2507
|
const tasks = listTasks();
|
|
2508
|
+
const habits = listHabits();
|
|
983
2509
|
const rules = listRewardRules();
|
|
984
|
-
const gamificationOverview = buildGamificationOverview(goals, tasks);
|
|
2510
|
+
const gamificationOverview = buildGamificationOverview(goals, tasks, habits);
|
|
985
2511
|
const dailyAmbientCap = rules
|
|
986
2512
|
.filter((rule) => rule.family === "ambient")
|
|
987
2513
|
.reduce((max, rule) => Math.max(max, Number(rule.config.dailyCap ?? 0)), 0) || 12;
|
|
@@ -989,15 +2515,58 @@ function buildXpMetricsPayload() {
|
|
|
989
2515
|
profile: gamificationOverview.profile,
|
|
990
2516
|
achievements: gamificationOverview.achievements,
|
|
991
2517
|
milestoneRewards: gamificationOverview.milestoneRewards,
|
|
992
|
-
momentumPulse: buildXpMomentumPulse(goals, tasks),
|
|
2518
|
+
momentumPulse: buildXpMomentumPulse(goals, tasks, habits),
|
|
993
2519
|
recentLedger: listRewardLedger({ limit: 25 }),
|
|
994
2520
|
rules,
|
|
995
2521
|
dailyAmbientXp: getDailyAmbientXp(new Date().toISOString().slice(0, 10)),
|
|
996
2522
|
dailyAmbientCap
|
|
997
2523
|
};
|
|
998
2524
|
}
|
|
2525
|
+
function resolveWorkAdjustmentTarget(entityType, entityId) {
|
|
2526
|
+
if (entityType === "task") {
|
|
2527
|
+
const task = getTaskById(entityId);
|
|
2528
|
+
return task
|
|
2529
|
+
? {
|
|
2530
|
+
entityType,
|
|
2531
|
+
entityId: task.id,
|
|
2532
|
+
title: task.title,
|
|
2533
|
+
time: task.time
|
|
2534
|
+
}
|
|
2535
|
+
: null;
|
|
2536
|
+
}
|
|
2537
|
+
const project = getProjectSummary(entityId);
|
|
2538
|
+
return project
|
|
2539
|
+
? {
|
|
2540
|
+
entityType,
|
|
2541
|
+
entityId: project.id,
|
|
2542
|
+
title: project.title,
|
|
2543
|
+
time: project.time
|
|
2544
|
+
}
|
|
2545
|
+
: null;
|
|
2546
|
+
}
|
|
2547
|
+
function clampWorkAdjustmentMinutes(deltaMinutes, currentCreditedSeconds) {
|
|
2548
|
+
if (deltaMinutes >= 0) {
|
|
2549
|
+
return deltaMinutes;
|
|
2550
|
+
}
|
|
2551
|
+
const maxRemovableMinutes = Math.max(0, Math.floor(currentCreditedSeconds / 60));
|
|
2552
|
+
return -Math.min(Math.abs(deltaMinutes), maxRemovableMinutes);
|
|
2553
|
+
}
|
|
2554
|
+
function describeWorkAdjustment(input) {
|
|
2555
|
+
const entityLabel = input.entityType === "task" ? "Task" : "Project";
|
|
2556
|
+
const requestedLabel = `${Math.abs(input.requestedDeltaMinutes)} minute${Math.abs(input.requestedDeltaMinutes) === 1 ? "" : "s"}`;
|
|
2557
|
+
const appliedLabel = `${Math.abs(input.appliedDeltaMinutes)} minute${Math.abs(input.appliedDeltaMinutes) === 1 ? "" : "s"}`;
|
|
2558
|
+
const direction = input.appliedDeltaMinutes >= 0 ? "added" : "removed";
|
|
2559
|
+
const clamped = input.requestedDeltaMinutes !== input.appliedDeltaMinutes;
|
|
2560
|
+
return {
|
|
2561
|
+
title: `${entityLabel} work adjusted: ${input.targetTitle}`,
|
|
2562
|
+
description: clamped
|
|
2563
|
+
? `${requestedLabel} requested, ${appliedLabel} ${direction} after clamping to the currently tracked time.`
|
|
2564
|
+
: `${appliedLabel} ${direction} from the tracked work total.`
|
|
2565
|
+
};
|
|
2566
|
+
}
|
|
999
2567
|
function buildOperatorContext() {
|
|
1000
2568
|
const tasks = listTasks();
|
|
2569
|
+
const dueHabits = listHabits({ dueToday: true }).slice(0, 12);
|
|
1001
2570
|
const activeProjects = listProjectSummaries({ status: "active" }).filter((project) => project.activeTaskCount > 0 || project.completedTaskCount > 0);
|
|
1002
2571
|
const focusTasks = tasks.filter((task) => task.status === "focus" || task.status === "in_progress");
|
|
1003
2572
|
const recommendedNextTask = focusTasks[0] ??
|
|
@@ -1008,10 +2577,13 @@ function buildOperatorContext() {
|
|
|
1008
2577
|
generatedAt: new Date().toISOString(),
|
|
1009
2578
|
activeProjects: activeProjects.slice(0, 8),
|
|
1010
2579
|
focusTasks: focusTasks.slice(0, 12),
|
|
2580
|
+
dueHabits,
|
|
1011
2581
|
currentBoard: {
|
|
1012
2582
|
backlog: tasks.filter((task) => task.status === "backlog").slice(0, 20),
|
|
1013
2583
|
focus: tasks.filter((task) => task.status === "focus").slice(0, 20),
|
|
1014
|
-
inProgress: tasks
|
|
2584
|
+
inProgress: tasks
|
|
2585
|
+
.filter((task) => task.status === "in_progress")
|
|
2586
|
+
.slice(0, 20),
|
|
1015
2587
|
blocked: tasks.filter((task) => task.status === "blocked").slice(0, 20),
|
|
1016
2588
|
done: tasks.filter((task) => task.status === "done").slice(0, 20)
|
|
1017
2589
|
},
|
|
@@ -1085,6 +2657,12 @@ function buildOperatorOverviewRouteGuide() {
|
|
|
1085
2657
|
summary: "Preferred multi-entity mutation surface for agents. Delete defaults to soft delete and restore reverses soft deletion.",
|
|
1086
2658
|
requiredScope: "write"
|
|
1087
2659
|
},
|
|
2660
|
+
{
|
|
2661
|
+
id: "work_adjustments",
|
|
2662
|
+
path: "/api/v1/work-adjustments",
|
|
2663
|
+
summary: "Signed retrospective minute adjustments for existing tasks or projects, with symmetric progress-XP updates and clamp protection.",
|
|
2664
|
+
requiredScope: "write"
|
|
2665
|
+
},
|
|
1088
2666
|
{
|
|
1089
2667
|
id: "operator_log_work",
|
|
1090
2668
|
path: "/api/v1/operator/log-work",
|
|
@@ -1102,8 +2680,14 @@ function buildOperatorOverviewRouteGuide() {
|
|
|
1102
2680
|
}
|
|
1103
2681
|
function buildOperatorOverview(request) {
|
|
1104
2682
|
const auth = parseRequestAuth(request.headers);
|
|
1105
|
-
const canReadPsyche = auth.token
|
|
1106
|
-
|
|
2683
|
+
const canReadPsyche = auth.token
|
|
2684
|
+
? hasTokenScope(auth.token, "psyche.read")
|
|
2685
|
+
: true;
|
|
2686
|
+
const warnings = canReadPsyche
|
|
2687
|
+
? []
|
|
2688
|
+
: [
|
|
2689
|
+
"Psyche summary omitted because the active token does not include psyche.read."
|
|
2690
|
+
];
|
|
1107
2691
|
return {
|
|
1108
2692
|
generatedAt: new Date().toISOString(),
|
|
1109
2693
|
snapshot: buildV1Context(),
|
|
@@ -1115,9 +2699,15 @@ function buildOperatorOverview(request) {
|
|
|
1115
2699
|
tokenPresent: Boolean(auth.token),
|
|
1116
2700
|
scopes: auth.token?.scopes ?? [],
|
|
1117
2701
|
canReadPsyche,
|
|
1118
|
-
canWritePsyche: auth.token
|
|
1119
|
-
|
|
1120
|
-
|
|
2702
|
+
canWritePsyche: auth.token
|
|
2703
|
+
? hasTokenScope(auth.token, "psyche.write")
|
|
2704
|
+
: true,
|
|
2705
|
+
canManageModes: auth.token
|
|
2706
|
+
? hasTokenScope(auth.token, "psyche.mode")
|
|
2707
|
+
: true,
|
|
2708
|
+
canManageRewards: auth.token
|
|
2709
|
+
? hasTokenScope(auth.token, "rewards.manage")
|
|
2710
|
+
: true
|
|
1121
2711
|
},
|
|
1122
2712
|
warnings,
|
|
1123
2713
|
routeGuide: buildOperatorOverviewRouteGuide()
|
|
@@ -1125,7 +2715,21 @@ function buildOperatorOverview(request) {
|
|
|
1125
2715
|
}
|
|
1126
2716
|
export async function buildServer(options = {}) {
|
|
1127
2717
|
const managers = createManagerRuntime({ dataRoot: options.dataRoot });
|
|
1128
|
-
|
|
2718
|
+
managers.externalServices.register("google_calendar", {
|
|
2719
|
+
provider: "google",
|
|
2720
|
+
label: "Google Calendar"
|
|
2721
|
+
});
|
|
2722
|
+
managers.externalServices.register("microsoft_graph_calendar", {
|
|
2723
|
+
provider: "microsoft",
|
|
2724
|
+
label: "Exchange Online"
|
|
2725
|
+
});
|
|
2726
|
+
managers.externalServices.register("caldav", {
|
|
2727
|
+
provider: "caldav",
|
|
2728
|
+
label: "CalDAV"
|
|
2729
|
+
});
|
|
2730
|
+
const runtimeConfig = managers.configuration.readRuntimeConfig({
|
|
2731
|
+
dataRoot: options.dataRoot
|
|
2732
|
+
});
|
|
1129
2733
|
configureDatabase({ dataRoot: runtimeConfig.dataRoot ?? undefined });
|
|
1130
2734
|
configureDatabaseSeeding(options.seedDemoData ?? false);
|
|
1131
2735
|
await managers.migration.initialize();
|
|
@@ -1133,7 +2737,9 @@ export async function buildServer(options = {}) {
|
|
|
1133
2737
|
logger: false,
|
|
1134
2738
|
rewriteUrl: (request) => rewriteMountPath(request.url ?? "/")
|
|
1135
2739
|
});
|
|
1136
|
-
const taskRunWatchdog = options.taskRunWatchdog === false
|
|
2740
|
+
const taskRunWatchdog = options.taskRunWatchdog === false
|
|
2741
|
+
? null
|
|
2742
|
+
: createTaskRunWatchdog(options.taskRunWatchdog);
|
|
1137
2743
|
await app.register(cors, {
|
|
1138
2744
|
origin: (origin, callback) => {
|
|
1139
2745
|
if (!origin) {
|
|
@@ -1164,7 +2770,9 @@ export async function buildServer(options = {}) {
|
|
|
1164
2770
|
: statusCode === 400
|
|
1165
2771
|
? "invalid_request"
|
|
1166
2772
|
: "internal_error",
|
|
1167
|
-
error: validationIssues
|
|
2773
|
+
error: validationIssues
|
|
2774
|
+
? "Request validation failed"
|
|
2775
|
+
: getErrorMessage(error),
|
|
1168
2776
|
statusCode,
|
|
1169
2777
|
...(validationIssues ? { details: validationIssues } : {}),
|
|
1170
2778
|
...(isHttpError(error) && error.details ? error.details : {}),
|
|
@@ -1176,6 +2784,112 @@ export async function buildServer(options = {}) {
|
|
|
1176
2784
|
actor: context.actor,
|
|
1177
2785
|
source: context.source
|
|
1178
2786
|
});
|
|
2787
|
+
const applyBatchCalendarEntityEffects = async (results, auth, action) => {
|
|
2788
|
+
for (const result of results) {
|
|
2789
|
+
if (!result.ok ||
|
|
2790
|
+
typeof result.entityType !== "string" ||
|
|
2791
|
+
typeof result.id !== "string") {
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
if (result.entityType === "calendar_event") {
|
|
2795
|
+
if (action === "delete") {
|
|
2796
|
+
await deleteCalendarEventProjection(result.id, managers.secrets);
|
|
2797
|
+
const event = (result.entity ?? {});
|
|
2798
|
+
recordActivityEvent({
|
|
2799
|
+
entityType: "calendar_event",
|
|
2800
|
+
entityId: result.id,
|
|
2801
|
+
eventType: "calendar_event_deleted",
|
|
2802
|
+
title: `Calendar event deleted: ${typeof event.title === "string" ? event.title : result.id}`,
|
|
2803
|
+
description: "The Forge calendar event was removed and any projected remote copies were deleted.",
|
|
2804
|
+
actor: auth.actor ?? null,
|
|
2805
|
+
source: auth.source,
|
|
2806
|
+
metadata: {
|
|
2807
|
+
calendarId: typeof event.calendarId === "string" ? event.calendarId : null,
|
|
2808
|
+
originType: typeof event.originType === "string" ? event.originType : null
|
|
2809
|
+
}
|
|
2810
|
+
});
|
|
2811
|
+
continue;
|
|
2812
|
+
}
|
|
2813
|
+
await pushCalendarEventUpdate(result.id, managers.secrets);
|
|
2814
|
+
const refreshed = getCalendarEventById(result.id);
|
|
2815
|
+
if (!refreshed) {
|
|
2816
|
+
continue;
|
|
2817
|
+
}
|
|
2818
|
+
result.entity = refreshed;
|
|
2819
|
+
recordActivityEvent({
|
|
2820
|
+
entityType: "calendar_event",
|
|
2821
|
+
entityId: refreshed.id,
|
|
2822
|
+
eventType: action === "create"
|
|
2823
|
+
? "calendar_event_created"
|
|
2824
|
+
: "calendar_event_updated",
|
|
2825
|
+
title: `Calendar event ${action === "create" ? "created" : "updated"}: ${refreshed.title}`,
|
|
2826
|
+
description: action === "create"
|
|
2827
|
+
? "A native Forge calendar event was created."
|
|
2828
|
+
: "The Forge calendar event was updated and projected to remote calendars when configured.",
|
|
2829
|
+
actor: auth.actor ?? null,
|
|
2830
|
+
source: auth.source,
|
|
2831
|
+
metadata: {
|
|
2832
|
+
calendarId: refreshed.calendarId,
|
|
2833
|
+
originType: refreshed.originType
|
|
2834
|
+
}
|
|
2835
|
+
});
|
|
2836
|
+
continue;
|
|
2837
|
+
}
|
|
2838
|
+
if (result.entityType === "work_block_template" &&
|
|
2839
|
+
result.entity &&
|
|
2840
|
+
typeof result.entity === "object") {
|
|
2841
|
+
const template = result.entity;
|
|
2842
|
+
recordActivityEvent({
|
|
2843
|
+
entityType: "work_block",
|
|
2844
|
+
entityId: template.id,
|
|
2845
|
+
eventType: action === "create"
|
|
2846
|
+
? "work_block_created"
|
|
2847
|
+
: action === "update"
|
|
2848
|
+
? "work_block_updated"
|
|
2849
|
+
: "work_block_deleted",
|
|
2850
|
+
title: `Work block ${action}: ${template.title}`,
|
|
2851
|
+
description: action === "create"
|
|
2852
|
+
? "A recurring work block was added to Forge."
|
|
2853
|
+
: action === "update"
|
|
2854
|
+
? "The recurring work block was updated."
|
|
2855
|
+
: "The recurring work block was removed.",
|
|
2856
|
+
actor: auth.actor ?? null,
|
|
2857
|
+
source: auth.source,
|
|
2858
|
+
metadata: {
|
|
2859
|
+
kind: template.kind ?? null,
|
|
2860
|
+
blockingState: action === "delete" ? null : (template.blockingState ?? null)
|
|
2861
|
+
}
|
|
2862
|
+
});
|
|
2863
|
+
continue;
|
|
2864
|
+
}
|
|
2865
|
+
if (result.entityType === "task_timebox" &&
|
|
2866
|
+
result.entity &&
|
|
2867
|
+
typeof result.entity === "object") {
|
|
2868
|
+
const timebox = result.entity;
|
|
2869
|
+
recordActivityEvent({
|
|
2870
|
+
entityType: "task_timebox",
|
|
2871
|
+
entityId: timebox.id,
|
|
2872
|
+
eventType: action === "create"
|
|
2873
|
+
? "task_timebox_created"
|
|
2874
|
+
: action === "update"
|
|
2875
|
+
? "task_timebox_updated"
|
|
2876
|
+
: "task_timebox_deleted",
|
|
2877
|
+
title: `Task timebox ${action}: ${timebox.title}`,
|
|
2878
|
+
description: action === "create"
|
|
2879
|
+
? "A future work slot was planned in Forge."
|
|
2880
|
+
: action === "update"
|
|
2881
|
+
? "The planned work slot was updated."
|
|
2882
|
+
: "The planned work slot was removed.",
|
|
2883
|
+
actor: auth.actor ?? null,
|
|
2884
|
+
source: auth.source,
|
|
2885
|
+
metadata: {
|
|
2886
|
+
taskId: timebox.taskId ?? null,
|
|
2887
|
+
status: action === "delete" ? null : (timebox.status ?? null)
|
|
2888
|
+
}
|
|
2889
|
+
});
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
};
|
|
1179
2893
|
const requireOperatorSession = (headers, detail) => {
|
|
1180
2894
|
const context = authenticateRequest(headers);
|
|
1181
2895
|
managers.authorization.requireAuthenticatedOperator(context, detail);
|
|
@@ -1231,9 +2945,18 @@ export async function buildServer(options = {}) {
|
|
|
1231
2945
|
return context;
|
|
1232
2946
|
};
|
|
1233
2947
|
app.get("/api/health", async () => buildHealthPayload(taskRunWatchdog));
|
|
1234
|
-
app.get("/api/v1/health", async () => buildHealthPayload(taskRunWatchdog, {
|
|
2948
|
+
app.get("/api/v1/health", async (request) => buildHealthPayload(taskRunWatchdog, {
|
|
1235
2949
|
apiVersion: "v1",
|
|
1236
|
-
backend: "forge-node-runtime"
|
|
2950
|
+
backend: "forge-node-runtime",
|
|
2951
|
+
...(shouldIncludeRuntimeProbe(request.headers)
|
|
2952
|
+
? {
|
|
2953
|
+
runtime: {
|
|
2954
|
+
pid: process.pid,
|
|
2955
|
+
storageRoot: runtimeConfig.dataRoot ?? process.cwd(),
|
|
2956
|
+
basePath: runtimeConfig.basePath
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
: {})
|
|
1237
2960
|
}));
|
|
1238
2961
|
app.get("/api/v1/auth/operator-session", async (request, reply) => ({
|
|
1239
2962
|
session: managers.session.ensureLocalOperatorSession(request.headers, reply)
|
|
@@ -1244,13 +2967,17 @@ export async function buildServer(options = {}) {
|
|
|
1244
2967
|
app.get("/api/v1/openapi.json", async () => buildOpenApiDocument());
|
|
1245
2968
|
app.get("/api/v1/context", async () => buildV1Context());
|
|
1246
2969
|
app.get("/api/v1/operator/context", async (request) => {
|
|
1247
|
-
requireOperatorSession(request.headers, {
|
|
2970
|
+
requireOperatorSession(request.headers, {
|
|
2971
|
+
route: "/api/v1/operator/context"
|
|
2972
|
+
});
|
|
1248
2973
|
return {
|
|
1249
2974
|
context: buildOperatorContext()
|
|
1250
2975
|
};
|
|
1251
2976
|
});
|
|
1252
2977
|
app.get("/api/v1/operator/overview", async (request) => {
|
|
1253
|
-
requireOperatorSession(request.headers, {
|
|
2978
|
+
requireOperatorSession(request.headers, {
|
|
2979
|
+
route: "/api/v1/operator/overview"
|
|
2980
|
+
});
|
|
1254
2981
|
return {
|
|
1255
2982
|
overview: buildOperatorOverview(request)
|
|
1256
2983
|
};
|
|
@@ -1606,8 +3333,16 @@ export async function buildServer(options = {}) {
|
|
|
1606
3333
|
}
|
|
1607
3334
|
return {
|
|
1608
3335
|
report,
|
|
1609
|
-
notes: listNotes({
|
|
1610
|
-
|
|
3336
|
+
notes: listNotes({
|
|
3337
|
+
linkedEntityType: "trigger_report",
|
|
3338
|
+
linkedEntityId: id,
|
|
3339
|
+
limit: 50
|
|
3340
|
+
}),
|
|
3341
|
+
insights: listInsights({
|
|
3342
|
+
entityType: "trigger_report",
|
|
3343
|
+
entityId: id,
|
|
3344
|
+
limit: 50
|
|
3345
|
+
})
|
|
1611
3346
|
};
|
|
1612
3347
|
});
|
|
1613
3348
|
app.patch("/api/v1/psyche/reports/:id", async (request, reply) => {
|
|
@@ -1686,7 +3421,10 @@ export async function buildServer(options = {}) {
|
|
|
1686
3421
|
app.delete("/api/v1/notes/:id", async (request, reply) => {
|
|
1687
3422
|
const { id } = request.params;
|
|
1688
3423
|
const current = getNoteById(id);
|
|
1689
|
-
const linkedEntityType = current?.links.find((link) => isPsycheEntityType(link.entityType))
|
|
3424
|
+
const linkedEntityType = current?.links.find((link) => isPsycheEntityType(link.entityType))
|
|
3425
|
+
?.entityType ??
|
|
3426
|
+
current?.links[0]?.entityType ??
|
|
3427
|
+
null;
|
|
1690
3428
|
const auth = requireNoteAccess(request.headers, linkedEntityType, {
|
|
1691
3429
|
route: "/api/v1/notes/:id",
|
|
1692
3430
|
entityType: linkedEntityType
|
|
@@ -1721,6 +3459,160 @@ export async function buildServer(options = {}) {
|
|
|
1721
3459
|
const query = taskListQuerySchema.parse(request.query ?? {});
|
|
1722
3460
|
return { tasks: listTasks(query) };
|
|
1723
3461
|
});
|
|
3462
|
+
app.get("/api/v1/calendar/overview", async (request) => {
|
|
3463
|
+
const query = calendarOverviewQuerySchema.parse(request.query ?? {});
|
|
3464
|
+
const now = new Date();
|
|
3465
|
+
const from = query.from ??
|
|
3466
|
+
new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
3467
|
+
const to = query.to ??
|
|
3468
|
+
new Date(now.getTime() + 21 * 24 * 60 * 60 * 1000).toISOString();
|
|
3469
|
+
return { calendar: readCalendarOverview({ from, to }) };
|
|
3470
|
+
});
|
|
3471
|
+
app.get("/api/v1/calendar/agenda", async (request) => {
|
|
3472
|
+
const query = calendarOverviewQuerySchema.parse(request.query ?? {});
|
|
3473
|
+
const now = new Date();
|
|
3474
|
+
const from = query.from ??
|
|
3475
|
+
new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
3476
|
+
const to = query.to ??
|
|
3477
|
+
new Date(now.getTime() + 21 * 24 * 60 * 60 * 1000).toISOString();
|
|
3478
|
+
return {
|
|
3479
|
+
providers: listCalendarProviderMetadata(),
|
|
3480
|
+
calendars: listCalendars(),
|
|
3481
|
+
events: listCalendarEvents({ from, to }),
|
|
3482
|
+
workBlocks: listWorkBlockInstances({ from, to }),
|
|
3483
|
+
timeboxes: listTaskTimeboxes({ from, to })
|
|
3484
|
+
};
|
|
3485
|
+
});
|
|
3486
|
+
app.get("/api/v1/calendar/connections", async () => ({
|
|
3487
|
+
providers: listCalendarProviderMetadata(),
|
|
3488
|
+
connections: listConnectedCalendarConnections()
|
|
3489
|
+
}));
|
|
3490
|
+
app.post("/api/v1/calendar/oauth/microsoft/start", async (request) => {
|
|
3491
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
3492
|
+
route: "/api/v1/calendar/oauth/microsoft/start"
|
|
3493
|
+
});
|
|
3494
|
+
return await startMicrosoftCalendarOauth(startMicrosoftCalendarOauthSchema.parse(request.body ?? {}), getRequestOrigin(request));
|
|
3495
|
+
});
|
|
3496
|
+
app.post("/api/v1/calendar/oauth/microsoft/test-config", async (request) => {
|
|
3497
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
3498
|
+
route: "/api/v1/calendar/oauth/microsoft/test-config"
|
|
3499
|
+
});
|
|
3500
|
+
return {
|
|
3501
|
+
result: await testMicrosoftCalendarOauthConfiguration(testMicrosoftCalendarOauthConfigurationSchema.parse(request.body ?? {}))
|
|
3502
|
+
};
|
|
3503
|
+
});
|
|
3504
|
+
app.get("/api/v1/calendar/oauth/microsoft/session/:id", async (request, reply) => {
|
|
3505
|
+
requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/oauth/microsoft/session/:id" });
|
|
3506
|
+
try {
|
|
3507
|
+
return getMicrosoftCalendarOauthSession(request.params.id);
|
|
3508
|
+
}
|
|
3509
|
+
catch (error) {
|
|
3510
|
+
if (error instanceof Error &&
|
|
3511
|
+
error.message.startsWith("Unknown Microsoft calendar auth session")) {
|
|
3512
|
+
reply.code(404);
|
|
3513
|
+
return { error: "Microsoft calendar auth session not found" };
|
|
3514
|
+
}
|
|
3515
|
+
throw error;
|
|
3516
|
+
}
|
|
3517
|
+
});
|
|
3518
|
+
app.get("/api/v1/calendar/oauth/microsoft/callback", async (request, reply) => {
|
|
3519
|
+
const query = request.query;
|
|
3520
|
+
const result = await completeMicrosoftCalendarOauth({
|
|
3521
|
+
state: query.state ?? null,
|
|
3522
|
+
code: query.code ?? null,
|
|
3523
|
+
error: query.error ?? null,
|
|
3524
|
+
errorDescription: query.error_description ?? null
|
|
3525
|
+
});
|
|
3526
|
+
const session = result.session;
|
|
3527
|
+
const escapedOrigin = JSON.stringify(result.openerOrigin || "*");
|
|
3528
|
+
const escapedMessage = JSON.stringify({
|
|
3529
|
+
type: "forge:microsoft-calendar-auth",
|
|
3530
|
+
sessionId: session.sessionId,
|
|
3531
|
+
status: session.status
|
|
3532
|
+
});
|
|
3533
|
+
const body = `<!doctype html>
|
|
3534
|
+
<html lang="en">
|
|
3535
|
+
<head>
|
|
3536
|
+
<meta charset="utf-8" />
|
|
3537
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
3538
|
+
<title>Forge Microsoft sign-in</title>
|
|
3539
|
+
<style>
|
|
3540
|
+
body{margin:0;font-family:ui-sans-serif,system-ui,sans-serif;background:#0b1320;color:#f8fafc;display:grid;place-items:center;min-height:100vh}
|
|
3541
|
+
main{max-width:28rem;padding:2rem;border:1px solid rgba(255,255,255,.08);border-radius:24px;background:linear-gradient(180deg,rgba(18,28,38,.98),rgba(11,17,28,.98))}
|
|
3542
|
+
h1{margin:0 0 .75rem;font-size:1.15rem}
|
|
3543
|
+
p{margin:0;color:rgba(248,250,252,.72);line-height:1.6}
|
|
3544
|
+
</style>
|
|
3545
|
+
</head>
|
|
3546
|
+
<body>
|
|
3547
|
+
<main>
|
|
3548
|
+
<h1>${session.status === "authorized" ? "Microsoft account connected" : "Microsoft sign-in needs attention"}</h1>
|
|
3549
|
+
<p>${session.status === "authorized" ? "Forge received your Microsoft account and sent the result back to the calendar setup flow. You can close this window." : (session.error ?? "Forge could not complete Microsoft sign-in. You can close this window and try again from Settings.")}</p>
|
|
3550
|
+
</main>
|
|
3551
|
+
<script>
|
|
3552
|
+
const message = ${escapedMessage};
|
|
3553
|
+
const targetOrigin = ${escapedOrigin};
|
|
3554
|
+
try {
|
|
3555
|
+
if (window.opener && !window.opener.closed) {
|
|
3556
|
+
window.opener.postMessage(message, targetOrigin);
|
|
3557
|
+
}
|
|
3558
|
+
} catch {}
|
|
3559
|
+
setTimeout(() => window.close(), 180);
|
|
3560
|
+
</script>
|
|
3561
|
+
</body>
|
|
3562
|
+
</html>`;
|
|
3563
|
+
reply.type("text/html; charset=utf-8");
|
|
3564
|
+
return body;
|
|
3565
|
+
});
|
|
3566
|
+
app.post("/api/v1/calendar/discovery", async (request) => {
|
|
3567
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/discovery" });
|
|
3568
|
+
const discovery = await discoverCalendarConnection(discoverCalendarConnectionSchema.parse(request.body ?? {}));
|
|
3569
|
+
recordActivityEvent({
|
|
3570
|
+
entityType: "calendar_connection",
|
|
3571
|
+
entityId: "calendar_discovery",
|
|
3572
|
+
eventType: "calendar_connection_discovered",
|
|
3573
|
+
title: `Calendar discovery completed for ${discovery.provider}`,
|
|
3574
|
+
description: "Forge discovered provider calendars before connection setup.",
|
|
3575
|
+
actor: auth.actor ?? null,
|
|
3576
|
+
source: auth.source,
|
|
3577
|
+
metadata: {
|
|
3578
|
+
provider: discovery.provider,
|
|
3579
|
+
calendars: discovery.calendars.length
|
|
3580
|
+
}
|
|
3581
|
+
});
|
|
3582
|
+
return { discovery };
|
|
3583
|
+
});
|
|
3584
|
+
app.get("/api/v1/calendar/calendars", async () => ({
|
|
3585
|
+
calendars: listCalendars()
|
|
3586
|
+
}));
|
|
3587
|
+
app.get("/api/v1/calendar/connections/:id/discovery", async (request, reply) => {
|
|
3588
|
+
requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/connections/:id/discovery" });
|
|
3589
|
+
const { id } = request.params;
|
|
3590
|
+
try {
|
|
3591
|
+
const discovery = await discoverExistingCalendarConnection(id, managers.secrets);
|
|
3592
|
+
return { discovery };
|
|
3593
|
+
}
|
|
3594
|
+
catch (error) {
|
|
3595
|
+
if (error instanceof Error &&
|
|
3596
|
+
error.message.startsWith("Unknown calendar connection")) {
|
|
3597
|
+
reply.code(404);
|
|
3598
|
+
return { error: "Calendar connection not found" };
|
|
3599
|
+
}
|
|
3600
|
+
throw error;
|
|
3601
|
+
}
|
|
3602
|
+
});
|
|
3603
|
+
app.get("/api/v1/habits", async (request) => {
|
|
3604
|
+
const query = habitListQuerySchema.parse(request.query ?? {});
|
|
3605
|
+
return { habits: listHabits(query) };
|
|
3606
|
+
});
|
|
3607
|
+
app.get("/api/v1/habits/:id", async (request, reply) => {
|
|
3608
|
+
const { id } = request.params;
|
|
3609
|
+
const habit = getHabitById(id);
|
|
3610
|
+
if (!habit) {
|
|
3611
|
+
reply.code(404);
|
|
3612
|
+
return { error: "Habit not found" };
|
|
3613
|
+
}
|
|
3614
|
+
return { habit };
|
|
3615
|
+
});
|
|
1724
3616
|
app.get("/api/v1/projects/:id", async (request, reply) => {
|
|
1725
3617
|
const { id } = request.params;
|
|
1726
3618
|
const project = listProjectSummaries().find((entry) => entry.id === id);
|
|
@@ -1754,7 +3646,9 @@ export async function buildServer(options = {}) {
|
|
|
1754
3646
|
return { activity: listActivityEvents(query) };
|
|
1755
3647
|
});
|
|
1756
3648
|
app.post("/api/v1/activity/:id/remove", async (request, reply) => {
|
|
1757
|
-
requireScopedAccess(request.headers, ["write"], {
|
|
3649
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
3650
|
+
route: "/api/v1/activity/:id/remove"
|
|
3651
|
+
});
|
|
1758
3652
|
const { id } = request.params;
|
|
1759
3653
|
const event = removeActivityEvent(id, removeActivityEventSchema.parse(request.body ?? {}), parseActivityContext(request.headers));
|
|
1760
3654
|
if (!event) {
|
|
@@ -1764,7 +3658,7 @@ export async function buildServer(options = {}) {
|
|
|
1764
3658
|
return { event };
|
|
1765
3659
|
});
|
|
1766
3660
|
app.get("/api/v1/metrics", async () => ({
|
|
1767
|
-
metrics: buildGamificationOverview(listGoals(), listTasks())
|
|
3661
|
+
metrics: buildGamificationOverview(listGoals(), listTasks(), listHabits())
|
|
1768
3662
|
}));
|
|
1769
3663
|
app.get("/api/v1/metrics/xp", async () => ({
|
|
1770
3664
|
metrics: buildXpMetricsPayload()
|
|
@@ -1778,7 +3672,10 @@ export async function buildServer(options = {}) {
|
|
|
1778
3672
|
route: "/api/v1/insights",
|
|
1779
3673
|
entityType: input.entityType
|
|
1780
3674
|
});
|
|
1781
|
-
const insight = createInsight(input, {
|
|
3675
|
+
const insight = createInsight(input, {
|
|
3676
|
+
actor: auth.actor,
|
|
3677
|
+
source: auth.source
|
|
3678
|
+
});
|
|
1782
3679
|
reply.code(201);
|
|
1783
3680
|
return { insight };
|
|
1784
3681
|
});
|
|
@@ -1815,7 +3712,10 @@ export async function buildServer(options = {}) {
|
|
|
1815
3712
|
const query = entityDeleteQuerySchema.parse(request.query ?? {});
|
|
1816
3713
|
const insight = query.mode === "hard"
|
|
1817
3714
|
? deleteInsight(id, { actor: auth.actor, source: auth.source })
|
|
1818
|
-
: deleteEntity("insight", id, query, {
|
|
3715
|
+
: deleteEntity("insight", id, query, {
|
|
3716
|
+
actor: auth.actor,
|
|
3717
|
+
source: auth.source
|
|
3718
|
+
});
|
|
1819
3719
|
if (!insight) {
|
|
1820
3720
|
reply.code(404);
|
|
1821
3721
|
return { error: "Insight not found" };
|
|
@@ -1837,7 +3737,9 @@ export async function buildServer(options = {}) {
|
|
|
1837
3737
|
return { feedback };
|
|
1838
3738
|
});
|
|
1839
3739
|
app.get("/api/v1/approval-requests", async (request) => {
|
|
1840
|
-
requireOperatorSession(request.headers, {
|
|
3740
|
+
requireOperatorSession(request.headers, {
|
|
3741
|
+
route: "/api/v1/approval-requests"
|
|
3742
|
+
});
|
|
1841
3743
|
const query = request.query;
|
|
1842
3744
|
return { approvalRequests: listApprovalRequests(query?.status) };
|
|
1843
3745
|
});
|
|
@@ -1845,7 +3747,9 @@ export async function buildServer(options = {}) {
|
|
|
1845
3747
|
const context = requireOperatorSession(request.headers, { route: "/api/v1/approval-requests/:id/approve" });
|
|
1846
3748
|
const { id } = request.params;
|
|
1847
3749
|
const body = resolveApprovalRequestSchema.parse(request.body ?? {});
|
|
1848
|
-
const approvalRequest = approveApprovalRequest(id, body.note, body.actor ??
|
|
3750
|
+
const approvalRequest = approveApprovalRequest(id, body.note, body.actor ??
|
|
3751
|
+
context.actor ??
|
|
3752
|
+
parseOptionalActorHeader(request.headers));
|
|
1849
3753
|
if (!approvalRequest) {
|
|
1850
3754
|
reply.code(404);
|
|
1851
3755
|
return { error: "Approval request not found" };
|
|
@@ -1856,7 +3760,9 @@ export async function buildServer(options = {}) {
|
|
|
1856
3760
|
const context = requireOperatorSession(request.headers, { route: "/api/v1/approval-requests/:id/reject" });
|
|
1857
3761
|
const { id } = request.params;
|
|
1858
3762
|
const body = resolveApprovalRequestSchema.parse(request.body ?? {});
|
|
1859
|
-
const approvalRequest = rejectApprovalRequest(id, body.note, body.actor ??
|
|
3763
|
+
const approvalRequest = rejectApprovalRequest(id, body.note, body.actor ??
|
|
3764
|
+
context.actor ??
|
|
3765
|
+
parseOptionalActorHeader(request.headers));
|
|
1860
3766
|
if (!approvalRequest) {
|
|
1861
3767
|
reply.code(404);
|
|
1862
3768
|
return { error: "Approval request not found" };
|
|
@@ -1877,16 +3783,24 @@ export async function buildServer(options = {}) {
|
|
|
1877
3783
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/agent-actions" });
|
|
1878
3784
|
const input = createAgentActionSchema.parse(request.body ?? {});
|
|
1879
3785
|
const idempotencyKey = parseIdempotencyKey(request.headers);
|
|
1880
|
-
const result = createAgentAction(input, {
|
|
3786
|
+
const result = createAgentAction(input, {
|
|
3787
|
+
actor: auth.actor,
|
|
3788
|
+
source: auth.source,
|
|
3789
|
+
token: auth.token ? managers.token.getTokenById(auth.token.id) : null
|
|
3790
|
+
}, idempotencyKey);
|
|
1881
3791
|
reply.code(result.approvalRequest ? 202 : 201);
|
|
1882
3792
|
return result;
|
|
1883
3793
|
});
|
|
1884
3794
|
app.get("/api/v1/rewards/rules", async (request) => {
|
|
1885
|
-
requireOperatorSession(request.headers, {
|
|
3795
|
+
requireOperatorSession(request.headers, {
|
|
3796
|
+
route: "/api/v1/rewards/rules"
|
|
3797
|
+
});
|
|
1886
3798
|
return { rules: listRewardRules() };
|
|
1887
3799
|
});
|
|
1888
3800
|
app.get("/api/v1/rewards/rules/:id", async (request, reply) => {
|
|
1889
|
-
requireOperatorSession(request.headers, {
|
|
3801
|
+
requireOperatorSession(request.headers, {
|
|
3802
|
+
route: "/api/v1/rewards/rules/:id"
|
|
3803
|
+
});
|
|
1890
3804
|
const { id } = request.params;
|
|
1891
3805
|
const rule = getRewardRuleById(id);
|
|
1892
3806
|
if (!rule) {
|
|
@@ -1906,7 +3820,9 @@ export async function buildServer(options = {}) {
|
|
|
1906
3820
|
return { rule };
|
|
1907
3821
|
});
|
|
1908
3822
|
app.get("/api/v1/rewards/ledger", async (request) => {
|
|
1909
|
-
requireOperatorSession(request.headers, {
|
|
3823
|
+
requireOperatorSession(request.headers, {
|
|
3824
|
+
route: "/api/v1/rewards/ledger"
|
|
3825
|
+
});
|
|
1910
3826
|
const query = rewardsLedgerQuerySchema.parse(request.query ?? {});
|
|
1911
3827
|
return { ledger: listRewardLedger(query) };
|
|
1912
3828
|
});
|
|
@@ -1919,7 +3835,10 @@ export async function buildServer(options = {}) {
|
|
|
1919
3835
|
app.post("/api/v1/session-events", async (request, reply) => {
|
|
1920
3836
|
const auth = requireAuthenticatedActor(request.headers, { route: "/api/v1/session-events" });
|
|
1921
3837
|
const payload = createSessionEventSchema.parse(request.body ?? {});
|
|
1922
|
-
const event = recordSessionEvent(payload, {
|
|
3838
|
+
const event = recordSessionEvent(payload, {
|
|
3839
|
+
actor: auth.actor,
|
|
3840
|
+
source: auth.source
|
|
3841
|
+
});
|
|
1923
3842
|
reply.code(201);
|
|
1924
3843
|
return event;
|
|
1925
3844
|
});
|
|
@@ -1930,6 +3849,27 @@ export async function buildServer(options = {}) {
|
|
|
1930
3849
|
app.get("/api/v1/reviews/weekly", async () => ({
|
|
1931
3850
|
review: getWeeklyReviewPayload()
|
|
1932
3851
|
}));
|
|
3852
|
+
app.post("/api/v1/reviews/weekly/finalize", async (request, reply) => {
|
|
3853
|
+
const auth = requireAuthenticatedActor(request.headers, { route: "/api/v1/reviews/weekly/finalize" });
|
|
3854
|
+
const currentReview = getWeeklyReviewPayload();
|
|
3855
|
+
const finalized = finalizeWeeklyReviewClosure({
|
|
3856
|
+
weekKey: currentReview.weekKey,
|
|
3857
|
+
weekStartDate: currentReview.weekStartDate,
|
|
3858
|
+
weekEndDate: currentReview.weekEndDate,
|
|
3859
|
+
windowLabel: currentReview.windowLabel,
|
|
3860
|
+
rewardXp: currentReview.reward.rewardXp,
|
|
3861
|
+
actor: auth.actor,
|
|
3862
|
+
source: auth.source
|
|
3863
|
+
});
|
|
3864
|
+
const result = finalizeWeeklyReviewResultSchema.parse({
|
|
3865
|
+
closure: finalized.closure,
|
|
3866
|
+
reward: finalized.reward,
|
|
3867
|
+
review: getWeeklyReviewPayload(),
|
|
3868
|
+
metrics: buildXpMetricsPayload()
|
|
3869
|
+
});
|
|
3870
|
+
reply.code(finalized.created ? 201 : 200);
|
|
3871
|
+
return result;
|
|
3872
|
+
});
|
|
1933
3873
|
app.get("/api/v1/settings", async (request) => {
|
|
1934
3874
|
requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/settings" });
|
|
1935
3875
|
return { settings: getSettings() };
|
|
@@ -1944,6 +3884,301 @@ export async function buildServer(options = {}) {
|
|
|
1944
3884
|
reply.code(201);
|
|
1945
3885
|
return { project };
|
|
1946
3886
|
});
|
|
3887
|
+
app.post("/api/v1/calendar/connections", async (request, reply) => {
|
|
3888
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/connections" });
|
|
3889
|
+
try {
|
|
3890
|
+
const connection = await createCalendarConnection(createCalendarConnectionSchema.parse(request.body ?? {}), managers.secrets, toActivityContext(auth));
|
|
3891
|
+
reply.code(201);
|
|
3892
|
+
return { connection };
|
|
3893
|
+
}
|
|
3894
|
+
catch (error) {
|
|
3895
|
+
if (error instanceof CalendarConnectionConflictError) {
|
|
3896
|
+
reply.code(409);
|
|
3897
|
+
return {
|
|
3898
|
+
code: "calendar_connection_duplicate",
|
|
3899
|
+
error: error.message,
|
|
3900
|
+
existingConnectionId: error.connectionId
|
|
3901
|
+
};
|
|
3902
|
+
}
|
|
3903
|
+
throw error;
|
|
3904
|
+
}
|
|
3905
|
+
});
|
|
3906
|
+
app.patch("/api/v1/calendar/connections/:id", async (request, reply) => {
|
|
3907
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/connections/:id" });
|
|
3908
|
+
const { id } = request.params;
|
|
3909
|
+
const patch = updateCalendarConnectionSchema.parse(request.body ?? {});
|
|
3910
|
+
try {
|
|
3911
|
+
const connection = patch.label !== undefined || patch.selectedCalendarUrls !== undefined
|
|
3912
|
+
? await updateCalendarConnectionSelection(id, {
|
|
3913
|
+
label: patch.label,
|
|
3914
|
+
selectedCalendarUrls: patch.selectedCalendarUrls
|
|
3915
|
+
}, managers.secrets, toActivityContext(auth))
|
|
3916
|
+
: getCalendarConnectionById(id);
|
|
3917
|
+
if (!connection) {
|
|
3918
|
+
reply.code(404);
|
|
3919
|
+
return { error: "Calendar connection not found" };
|
|
3920
|
+
}
|
|
3921
|
+
return {
|
|
3922
|
+
connection: listConnectedCalendarConnections().find((entry) => entry.id === id)
|
|
3923
|
+
};
|
|
3924
|
+
}
|
|
3925
|
+
catch (error) {
|
|
3926
|
+
if (error instanceof Error &&
|
|
3927
|
+
error.message.startsWith("Unknown calendar connection")) {
|
|
3928
|
+
reply.code(404);
|
|
3929
|
+
return { error: "Calendar connection not found" };
|
|
3930
|
+
}
|
|
3931
|
+
throw error;
|
|
3932
|
+
}
|
|
3933
|
+
});
|
|
3934
|
+
app.post("/api/v1/calendar/connections/:id/sync", async (request, reply) => {
|
|
3935
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/connections/:id/sync" });
|
|
3936
|
+
const { id } = request.params;
|
|
3937
|
+
const connection = await syncCalendarConnection(id, managers.secrets, toActivityContext(auth));
|
|
3938
|
+
if (!connection) {
|
|
3939
|
+
reply.code(404);
|
|
3940
|
+
return { error: "Calendar connection not found" };
|
|
3941
|
+
}
|
|
3942
|
+
return {
|
|
3943
|
+
connection: listConnectedCalendarConnections().find((entry) => entry.id === id)
|
|
3944
|
+
};
|
|
3945
|
+
});
|
|
3946
|
+
app.delete("/api/v1/calendar/connections/:id", async (request, reply) => {
|
|
3947
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/connections/:id" });
|
|
3948
|
+
const { id } = request.params;
|
|
3949
|
+
const connection = await removeCalendarConnection(id, managers.secrets, toActivityContext(auth));
|
|
3950
|
+
if (!connection) {
|
|
3951
|
+
reply.code(404);
|
|
3952
|
+
return { error: "Calendar connection not found" };
|
|
3953
|
+
}
|
|
3954
|
+
return { connection };
|
|
3955
|
+
});
|
|
3956
|
+
app.get("/api/v1/calendar/work-block-templates", async () => ({
|
|
3957
|
+
templates: listWorkBlockTemplates()
|
|
3958
|
+
}));
|
|
3959
|
+
app.post("/api/v1/calendar/work-block-templates", async (request, reply) => {
|
|
3960
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/work-block-templates" });
|
|
3961
|
+
const template = createWorkBlockTemplate(createWorkBlockTemplateSchema.parse(request.body ?? {}));
|
|
3962
|
+
recordActivityEvent({
|
|
3963
|
+
entityType: "work_block",
|
|
3964
|
+
entityId: template.id,
|
|
3965
|
+
eventType: "work_block_created",
|
|
3966
|
+
title: `Work block created: ${template.title}`,
|
|
3967
|
+
description: "A recurring work block was added to Forge.",
|
|
3968
|
+
actor: auth.actor ?? null,
|
|
3969
|
+
source: auth.source,
|
|
3970
|
+
metadata: {
|
|
3971
|
+
kind: template.kind,
|
|
3972
|
+
blockingState: template.blockingState
|
|
3973
|
+
}
|
|
3974
|
+
});
|
|
3975
|
+
reply.code(201);
|
|
3976
|
+
return { template };
|
|
3977
|
+
});
|
|
3978
|
+
app.patch("/api/v1/calendar/work-block-templates/:id", async (request, reply) => {
|
|
3979
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/work-block-templates/:id" });
|
|
3980
|
+
const { id } = request.params;
|
|
3981
|
+
const template = updateWorkBlockTemplate(id, updateWorkBlockTemplateSchema.parse(request.body ?? {}));
|
|
3982
|
+
if (!template) {
|
|
3983
|
+
reply.code(404);
|
|
3984
|
+
return { error: "Work block template not found" };
|
|
3985
|
+
}
|
|
3986
|
+
recordActivityEvent({
|
|
3987
|
+
entityType: "work_block",
|
|
3988
|
+
entityId: template.id,
|
|
3989
|
+
eventType: "work_block_updated",
|
|
3990
|
+
title: `Work block updated: ${template.title}`,
|
|
3991
|
+
description: "The recurring work block was updated.",
|
|
3992
|
+
actor: auth.actor ?? null,
|
|
3993
|
+
source: auth.source,
|
|
3994
|
+
metadata: {
|
|
3995
|
+
kind: template.kind,
|
|
3996
|
+
blockingState: template.blockingState
|
|
3997
|
+
}
|
|
3998
|
+
});
|
|
3999
|
+
return { template };
|
|
4000
|
+
});
|
|
4001
|
+
app.delete("/api/v1/calendar/work-block-templates/:id", async (request, reply) => {
|
|
4002
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/work-block-templates/:id" });
|
|
4003
|
+
const { id } = request.params;
|
|
4004
|
+
const template = deleteWorkBlockTemplate(id);
|
|
4005
|
+
if (!template) {
|
|
4006
|
+
reply.code(404);
|
|
4007
|
+
return { error: "Work block template not found" };
|
|
4008
|
+
}
|
|
4009
|
+
recordActivityEvent({
|
|
4010
|
+
entityType: "work_block",
|
|
4011
|
+
entityId: template.id,
|
|
4012
|
+
eventType: "work_block_deleted",
|
|
4013
|
+
title: `Work block deleted: ${template.title}`,
|
|
4014
|
+
description: "The recurring work block was removed.",
|
|
4015
|
+
actor: auth.actor ?? null,
|
|
4016
|
+
source: auth.source,
|
|
4017
|
+
metadata: {
|
|
4018
|
+
kind: template.kind
|
|
4019
|
+
}
|
|
4020
|
+
});
|
|
4021
|
+
return { template };
|
|
4022
|
+
});
|
|
4023
|
+
app.get("/api/v1/calendar/timeboxes", async (request) => {
|
|
4024
|
+
const query = calendarOverviewQuerySchema.parse(request.query ?? {});
|
|
4025
|
+
const now = new Date();
|
|
4026
|
+
const from = query.from ??
|
|
4027
|
+
new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
4028
|
+
const to = query.to ??
|
|
4029
|
+
new Date(now.getTime() + 21 * 24 * 60 * 60 * 1000).toISOString();
|
|
4030
|
+
return { timeboxes: listTaskTimeboxes({ from, to }) };
|
|
4031
|
+
});
|
|
4032
|
+
app.post("/api/v1/calendar/timeboxes", async (request, reply) => {
|
|
4033
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/timeboxes" });
|
|
4034
|
+
const timebox = createTaskTimebox(createTaskTimeboxSchema.parse(request.body ?? {}));
|
|
4035
|
+
recordActivityEvent({
|
|
4036
|
+
entityType: "task_timebox",
|
|
4037
|
+
entityId: timebox.id,
|
|
4038
|
+
eventType: "task_timebox_created",
|
|
4039
|
+
title: `Task timebox created: ${timebox.title}`,
|
|
4040
|
+
description: "A future work slot was planned in Forge.",
|
|
4041
|
+
actor: auth.actor ?? null,
|
|
4042
|
+
source: auth.source,
|
|
4043
|
+
metadata: {
|
|
4044
|
+
taskId: timebox.taskId,
|
|
4045
|
+
status: timebox.status
|
|
4046
|
+
}
|
|
4047
|
+
});
|
|
4048
|
+
reply.code(201);
|
|
4049
|
+
return { timebox };
|
|
4050
|
+
});
|
|
4051
|
+
app.patch("/api/v1/calendar/timeboxes/:id", async (request, reply) => {
|
|
4052
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/timeboxes/:id" });
|
|
4053
|
+
const { id } = request.params;
|
|
4054
|
+
const timebox = updateTaskTimebox(id, updateTaskTimeboxSchema.parse(request.body ?? {}));
|
|
4055
|
+
if (!timebox) {
|
|
4056
|
+
reply.code(404);
|
|
4057
|
+
return { error: "Task timebox not found" };
|
|
4058
|
+
}
|
|
4059
|
+
recordActivityEvent({
|
|
4060
|
+
entityType: "task_timebox",
|
|
4061
|
+
entityId: timebox.id,
|
|
4062
|
+
eventType: "task_timebox_updated",
|
|
4063
|
+
title: `Task timebox updated: ${timebox.title}`,
|
|
4064
|
+
description: "The planned work slot was updated.",
|
|
4065
|
+
actor: auth.actor ?? null,
|
|
4066
|
+
source: auth.source,
|
|
4067
|
+
metadata: {
|
|
4068
|
+
taskId: timebox.taskId,
|
|
4069
|
+
status: timebox.status
|
|
4070
|
+
}
|
|
4071
|
+
});
|
|
4072
|
+
return { timebox };
|
|
4073
|
+
});
|
|
4074
|
+
app.delete("/api/v1/calendar/timeboxes/:id", async (request, reply) => {
|
|
4075
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/timeboxes/:id" });
|
|
4076
|
+
const { id } = request.params;
|
|
4077
|
+
const timebox = deleteTaskTimebox(id);
|
|
4078
|
+
if (!timebox) {
|
|
4079
|
+
reply.code(404);
|
|
4080
|
+
return { error: "Task timebox not found" };
|
|
4081
|
+
}
|
|
4082
|
+
recordActivityEvent({
|
|
4083
|
+
entityType: "task_timebox",
|
|
4084
|
+
entityId: timebox.id,
|
|
4085
|
+
eventType: "task_timebox_deleted",
|
|
4086
|
+
title: `Task timebox deleted: ${timebox.title}`,
|
|
4087
|
+
description: "The planned work slot was removed.",
|
|
4088
|
+
actor: auth.actor ?? null,
|
|
4089
|
+
source: auth.source,
|
|
4090
|
+
metadata: {
|
|
4091
|
+
taskId: timebox.taskId
|
|
4092
|
+
}
|
|
4093
|
+
});
|
|
4094
|
+
return { timebox };
|
|
4095
|
+
});
|
|
4096
|
+
app.post("/api/v1/calendar/timeboxes/recommend", async (request) => {
|
|
4097
|
+
const input = recommendTaskTimeboxesSchema.parse(request.body ?? {});
|
|
4098
|
+
return {
|
|
4099
|
+
timeboxes: suggestTaskTimeboxes(input.taskId, {
|
|
4100
|
+
from: input.from,
|
|
4101
|
+
to: input.to,
|
|
4102
|
+
limit: input.limit
|
|
4103
|
+
})
|
|
4104
|
+
};
|
|
4105
|
+
});
|
|
4106
|
+
app.post("/api/v1/calendar/events", async (request, reply) => {
|
|
4107
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/events" });
|
|
4108
|
+
const event = createCalendarEvent(createCalendarEventSchema.parse(request.body ?? {}));
|
|
4109
|
+
await pushCalendarEventUpdate(event.id, managers.secrets);
|
|
4110
|
+
const refreshed = getCalendarEventById(event.id);
|
|
4111
|
+
recordActivityEvent({
|
|
4112
|
+
entityType: "calendar_event",
|
|
4113
|
+
entityId: refreshed.id,
|
|
4114
|
+
eventType: "calendar_event_created",
|
|
4115
|
+
title: `Calendar event created: ${refreshed.title}`,
|
|
4116
|
+
description: "A native Forge calendar event was created.",
|
|
4117
|
+
actor: auth.actor ?? null,
|
|
4118
|
+
source: auth.source,
|
|
4119
|
+
metadata: {
|
|
4120
|
+
calendarId: refreshed.calendarId,
|
|
4121
|
+
originType: refreshed.originType
|
|
4122
|
+
}
|
|
4123
|
+
});
|
|
4124
|
+
reply.code(201);
|
|
4125
|
+
return { event: refreshed };
|
|
4126
|
+
});
|
|
4127
|
+
app.patch("/api/v1/calendar/events/:id", async (request, reply) => {
|
|
4128
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/events/:id" });
|
|
4129
|
+
const { id } = request.params;
|
|
4130
|
+
const event = updateCalendarEvent(id, updateCalendarEventSchema.parse(request.body ?? {}));
|
|
4131
|
+
if (!event) {
|
|
4132
|
+
reply.code(404);
|
|
4133
|
+
return { error: "Calendar event not found" };
|
|
4134
|
+
}
|
|
4135
|
+
await pushCalendarEventUpdate(id, managers.secrets);
|
|
4136
|
+
const refreshed = getCalendarEventById(id);
|
|
4137
|
+
recordActivityEvent({
|
|
4138
|
+
entityType: "calendar_event",
|
|
4139
|
+
entityId: refreshed.id,
|
|
4140
|
+
eventType: "calendar_event_updated",
|
|
4141
|
+
title: `Calendar event updated: ${refreshed.title}`,
|
|
4142
|
+
description: "The Forge calendar event was updated and projected to remote calendars when configured.",
|
|
4143
|
+
actor: auth.actor ?? null,
|
|
4144
|
+
source: auth.source,
|
|
4145
|
+
metadata: {
|
|
4146
|
+
calendarId: refreshed.calendarId,
|
|
4147
|
+
originType: refreshed.originType
|
|
4148
|
+
}
|
|
4149
|
+
});
|
|
4150
|
+
return { event: refreshed };
|
|
4151
|
+
});
|
|
4152
|
+
app.delete("/api/v1/calendar/events/:id", async (request, reply) => {
|
|
4153
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/calendar/events/:id" });
|
|
4154
|
+
const { id } = request.params;
|
|
4155
|
+
const event = deleteCalendarEvent(id);
|
|
4156
|
+
if (!event) {
|
|
4157
|
+
reply.code(404);
|
|
4158
|
+
return { error: "Calendar event not found" };
|
|
4159
|
+
}
|
|
4160
|
+
await deleteCalendarEventProjection(id, managers.secrets);
|
|
4161
|
+
recordActivityEvent({
|
|
4162
|
+
entityType: "calendar_event",
|
|
4163
|
+
entityId: event.id,
|
|
4164
|
+
eventType: "calendar_event_deleted",
|
|
4165
|
+
title: `Calendar event deleted: ${event.title}`,
|
|
4166
|
+
description: "The Forge calendar event was removed and any projected remote copies were deleted.",
|
|
4167
|
+
actor: auth.actor ?? null,
|
|
4168
|
+
source: auth.source,
|
|
4169
|
+
metadata: {
|
|
4170
|
+
calendarId: event.calendarId,
|
|
4171
|
+
originType: event.originType
|
|
4172
|
+
}
|
|
4173
|
+
});
|
|
4174
|
+
return { event };
|
|
4175
|
+
});
|
|
4176
|
+
app.post("/api/v1/habits", async (request, reply) => {
|
|
4177
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits" });
|
|
4178
|
+
const habit = createHabit(createHabitSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
4179
|
+
reply.code(201);
|
|
4180
|
+
return { habit };
|
|
4181
|
+
});
|
|
1947
4182
|
app.patch("/api/v1/projects/:id", async (request, reply) => {
|
|
1948
4183
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/projects/:id" });
|
|
1949
4184
|
const { id } = request.params;
|
|
@@ -1964,6 +4199,36 @@ export async function buildServer(options = {}) {
|
|
|
1964
4199
|
}
|
|
1965
4200
|
return { project };
|
|
1966
4201
|
});
|
|
4202
|
+
app.patch("/api/v1/habits/:id", async (request, reply) => {
|
|
4203
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id" });
|
|
4204
|
+
const { id } = request.params;
|
|
4205
|
+
const habit = updateHabit(id, updateHabitSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
4206
|
+
if (!habit) {
|
|
4207
|
+
reply.code(404);
|
|
4208
|
+
return { error: "Habit not found" };
|
|
4209
|
+
}
|
|
4210
|
+
return { habit };
|
|
4211
|
+
});
|
|
4212
|
+
app.delete("/api/v1/habits/:id", async (request, reply) => {
|
|
4213
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id" });
|
|
4214
|
+
const { id } = request.params;
|
|
4215
|
+
const habit = deleteEntity("habit", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
|
|
4216
|
+
if (!habit) {
|
|
4217
|
+
reply.code(404);
|
|
4218
|
+
return { error: "Habit not found" };
|
|
4219
|
+
}
|
|
4220
|
+
return { habit };
|
|
4221
|
+
});
|
|
4222
|
+
app.post("/api/v1/habits/:id/check-ins", async (request, reply) => {
|
|
4223
|
+
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/habits/:id/check-ins" });
|
|
4224
|
+
const { id } = request.params;
|
|
4225
|
+
const habit = createHabitCheckIn(id, createHabitCheckInSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
4226
|
+
if (!habit) {
|
|
4227
|
+
reply.code(404);
|
|
4228
|
+
return { error: "Habit not found" };
|
|
4229
|
+
}
|
|
4230
|
+
return { habit, metrics: buildXpMetricsPayload() };
|
|
4231
|
+
});
|
|
1967
4232
|
app.patch("/api/v1/settings", async (request) => {
|
|
1968
4233
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/settings" });
|
|
1969
4234
|
return {
|
|
@@ -2072,7 +4337,7 @@ export async function buildServer(options = {}) {
|
|
|
2072
4337
|
app.get("/api/metrics", async (_request, reply) => {
|
|
2073
4338
|
markCompatibilityRoute(reply);
|
|
2074
4339
|
return {
|
|
2075
|
-
metrics: buildGamificationProfile(listGoals(), listTasks())
|
|
4340
|
+
metrics: buildGamificationProfile(listGoals(), listTasks(), listHabits())
|
|
2076
4341
|
};
|
|
2077
4342
|
});
|
|
2078
4343
|
app.get("/api/task-runs", async (request, reply) => {
|
|
@@ -2102,7 +4367,7 @@ export async function buildServer(options = {}) {
|
|
|
2102
4367
|
markCompatibilityRoute(reply);
|
|
2103
4368
|
const query = taskListQuerySchema.parse(request.query ?? {});
|
|
2104
4369
|
return {
|
|
2105
|
-
metrics: buildGamificationProfile(listGoals(), listTasks()),
|
|
4370
|
+
metrics: buildGamificationProfile(listGoals(), listTasks(), listHabits()),
|
|
2106
4371
|
dashboard: getDashboard(),
|
|
2107
4372
|
overview: getOverviewContext(),
|
|
2108
4373
|
today: getTodayContext(),
|
|
@@ -2111,6 +4376,7 @@ export async function buildServer(options = {}) {
|
|
|
2111
4376
|
projects: listProjectSummaries(),
|
|
2112
4377
|
tags: listTags(),
|
|
2113
4378
|
tasks: listTasks(query),
|
|
4379
|
+
habits: listHabits(),
|
|
2114
4380
|
activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
|
|
2115
4381
|
activity: listActivityEvents({ limit: 25 })
|
|
2116
4382
|
};
|
|
@@ -2145,8 +4411,10 @@ export async function buildServer(options = {}) {
|
|
|
2145
4411
|
const taskRuns = listTaskRuns({ taskId: id, limit: 10 });
|
|
2146
4412
|
return taskContextPayloadSchema.parse({
|
|
2147
4413
|
task,
|
|
2148
|
-
goal: task.goalId ? getGoalById(task.goalId) ?? null : null,
|
|
2149
|
-
project: task.projectId
|
|
4414
|
+
goal: task.goalId ? (getGoalById(task.goalId) ?? null) : null,
|
|
4415
|
+
project: task.projectId
|
|
4416
|
+
? (listProjectSummaries().find((project) => project.id === task.projectId) ?? null)
|
|
4417
|
+
: null,
|
|
2150
4418
|
activeTaskRun: taskRuns.find((run) => run.status === "active") ?? null,
|
|
2151
4419
|
taskRuns,
|
|
2152
4420
|
activity: listActivityEventsForTask(id, 20),
|
|
@@ -2163,8 +4431,10 @@ export async function buildServer(options = {}) {
|
|
|
2163
4431
|
const taskRuns = listTaskRuns({ taskId: id, limit: 10 });
|
|
2164
4432
|
return taskContextPayloadSchema.parse({
|
|
2165
4433
|
task,
|
|
2166
|
-
goal: task.goalId ? getGoalById(task.goalId) ?? null : null,
|
|
2167
|
-
project: task.projectId
|
|
4434
|
+
goal: task.goalId ? (getGoalById(task.goalId) ?? null) : null,
|
|
4435
|
+
project: task.projectId
|
|
4436
|
+
? (listProjectSummaries().find((project) => project.id === task.projectId) ?? null)
|
|
4437
|
+
: null,
|
|
2168
4438
|
activeTaskRun: taskRuns.find((run) => run.status === "active") ?? null,
|
|
2169
4439
|
taskRuns,
|
|
2170
4440
|
activity: listActivityEventsForTask(id, 20),
|
|
@@ -2340,7 +4610,9 @@ export async function buildServer(options = {}) {
|
|
|
2340
4610
|
const input = operatorLogWorkSchema.parse(request.body ?? {});
|
|
2341
4611
|
if (input.taskId) {
|
|
2342
4612
|
const task = updateTask(input.taskId, {
|
|
2343
|
-
title: input.title && input.title.trim().length > 0
|
|
4613
|
+
title: input.title && input.title.trim().length > 0
|
|
4614
|
+
? input.title
|
|
4615
|
+
: undefined,
|
|
2344
4616
|
description: typeof input.description === "string"
|
|
2345
4617
|
? input.description
|
|
2346
4618
|
: input.summary.trim().length > 0
|
|
@@ -2386,6 +4658,72 @@ export async function buildServer(options = {}) {
|
|
|
2386
4658
|
reply.code(201);
|
|
2387
4659
|
return { task, xp: buildXpMetricsPayload() };
|
|
2388
4660
|
});
|
|
4661
|
+
app.post("/api/v1/work-adjustments", async (request, reply) => {
|
|
4662
|
+
const auth = requireScopedAccess(request.headers, ["write", "rewards.manage"], { route: "/api/v1/work-adjustments" });
|
|
4663
|
+
const input = createWorkAdjustmentSchema.parse(request.body ?? {});
|
|
4664
|
+
const currentTarget = resolveWorkAdjustmentTarget(input.entityType, input.entityId);
|
|
4665
|
+
if (!currentTarget) {
|
|
4666
|
+
reply.code(404);
|
|
4667
|
+
return {
|
|
4668
|
+
error: `${input.entityType === "task" ? "Task" : "Project"} not found`
|
|
4669
|
+
};
|
|
4670
|
+
}
|
|
4671
|
+
const appliedDeltaMinutes = clampWorkAdjustmentMinutes(input.deltaMinutes, currentTarget.time.totalCreditedSeconds);
|
|
4672
|
+
const nextCreditedSeconds = Math.max(0, currentTarget.time.totalCreditedSeconds + appliedDeltaMinutes * 60);
|
|
4673
|
+
const result = runInTransaction(() => {
|
|
4674
|
+
const adjustment = createWorkAdjustment({
|
|
4675
|
+
...input,
|
|
4676
|
+
appliedDeltaMinutes
|
|
4677
|
+
}, toActivityContext(auth));
|
|
4678
|
+
const reward = recordWorkAdjustmentReward({
|
|
4679
|
+
entityType: input.entityType,
|
|
4680
|
+
entityId: input.entityId,
|
|
4681
|
+
targetTitle: currentTarget.title,
|
|
4682
|
+
actor: auth.actor ?? null,
|
|
4683
|
+
source: auth.source,
|
|
4684
|
+
requestedDeltaMinutes: input.deltaMinutes,
|
|
4685
|
+
appliedDeltaMinutes,
|
|
4686
|
+
previousCreditedSeconds: currentTarget.time.totalCreditedSeconds,
|
|
4687
|
+
nextCreditedSeconds,
|
|
4688
|
+
adjustmentId: adjustment.id
|
|
4689
|
+
});
|
|
4690
|
+
const copy = describeWorkAdjustment({
|
|
4691
|
+
entityType: input.entityType,
|
|
4692
|
+
targetTitle: currentTarget.title,
|
|
4693
|
+
requestedDeltaMinutes: input.deltaMinutes,
|
|
4694
|
+
appliedDeltaMinutes
|
|
4695
|
+
});
|
|
4696
|
+
recordActivityEvent({
|
|
4697
|
+
entityType: input.entityType,
|
|
4698
|
+
entityId: input.entityId,
|
|
4699
|
+
eventType: "work_adjusted",
|
|
4700
|
+
title: copy.title,
|
|
4701
|
+
description: copy.description,
|
|
4702
|
+
actor: auth.actor ?? null,
|
|
4703
|
+
source: auth.source,
|
|
4704
|
+
metadata: {
|
|
4705
|
+
adjustmentId: adjustment.id,
|
|
4706
|
+
requestedDeltaMinutes: input.deltaMinutes,
|
|
4707
|
+
appliedDeltaMinutes,
|
|
4708
|
+
rewardDeltaXp: reward?.deltaXp ?? 0,
|
|
4709
|
+
rewardId: reward?.id ?? null,
|
|
4710
|
+
note: input.note || null
|
|
4711
|
+
}
|
|
4712
|
+
});
|
|
4713
|
+
return { adjustment, reward };
|
|
4714
|
+
});
|
|
4715
|
+
const updatedTarget = resolveWorkAdjustmentTarget(input.entityType, input.entityId);
|
|
4716
|
+
if (!updatedTarget) {
|
|
4717
|
+
throw new HttpError(500, "work_adjustment_target_missing", `Could not reload ${input.entityType} ${input.entityId} after adjustment`);
|
|
4718
|
+
}
|
|
4719
|
+
reply.code(201);
|
|
4720
|
+
return workAdjustmentResultSchema.parse({
|
|
4721
|
+
adjustment: result.adjustment,
|
|
4722
|
+
target: updatedTarget,
|
|
4723
|
+
reward: result.reward,
|
|
4724
|
+
metrics: buildXpMetricsPayload()
|
|
4725
|
+
});
|
|
4726
|
+
});
|
|
2389
4727
|
app.post("/api/v1/tasks/:id/uncomplete", async (request, reply) => {
|
|
2390
4728
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/tasks/:id/uncomplete" });
|
|
2391
4729
|
const { id } = request.params;
|
|
@@ -2399,18 +4737,26 @@ export async function buildServer(options = {}) {
|
|
|
2399
4737
|
});
|
|
2400
4738
|
app.post("/api/v1/entities/create", async (request) => {
|
|
2401
4739
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/entities/create" });
|
|
2402
|
-
|
|
4740
|
+
const result = createEntities(batchCreateEntitiesSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
4741
|
+
await applyBatchCalendarEntityEffects(result.results, auth, "create");
|
|
4742
|
+
return result;
|
|
2403
4743
|
});
|
|
2404
4744
|
app.post("/api/v1/entities/update", async (request) => {
|
|
2405
4745
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/entities/update" });
|
|
2406
|
-
|
|
4746
|
+
const result = updateEntities(batchUpdateEntitiesSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
4747
|
+
await applyBatchCalendarEntityEffects(result.results, auth, "update");
|
|
4748
|
+
return result;
|
|
2407
4749
|
});
|
|
2408
4750
|
app.post("/api/v1/entities/delete", async (request) => {
|
|
2409
4751
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/entities/delete" });
|
|
2410
|
-
|
|
4752
|
+
const result = deleteEntities(batchDeleteEntitiesSchema.parse(request.body ?? {}), toActivityContext(auth));
|
|
4753
|
+
await applyBatchCalendarEntityEffects(result.results, auth, "delete");
|
|
4754
|
+
return result;
|
|
2411
4755
|
});
|
|
2412
4756
|
app.post("/api/v1/entities/restore", async (request) => {
|
|
2413
|
-
requireScopedAccess(request.headers, ["write"], {
|
|
4757
|
+
requireScopedAccess(request.headers, ["write"], {
|
|
4758
|
+
route: "/api/v1/entities/restore"
|
|
4759
|
+
});
|
|
2414
4760
|
return restoreEntities(batchRestoreEntitiesSchema.parse(request.body ?? {}));
|
|
2415
4761
|
});
|
|
2416
4762
|
app.post("/api/v1/entities/search", async (request) => {
|
|
@@ -2419,7 +4765,9 @@ export async function buildServer(options = {}) {
|
|
|
2419
4765
|
});
|
|
2420
4766
|
app.post("/api/task-runs/recover", async (request, reply) => {
|
|
2421
4767
|
markCompatibilityRoute(reply);
|
|
2422
|
-
const payload = taskRunListQuerySchema
|
|
4768
|
+
const payload = taskRunListQuerySchema
|
|
4769
|
+
.pick({ limit: true })
|
|
4770
|
+
.parse(request.body ?? {});
|
|
2423
4771
|
return { timedOutRuns: recoverTimedOutTaskRuns({ limit: payload.limit }) };
|
|
2424
4772
|
});
|
|
2425
4773
|
app.post("/api/task-runs/:id/heartbeat", async (request, reply) => {
|
|
@@ -2427,52 +4775,68 @@ export async function buildServer(options = {}) {
|
|
|
2427
4775
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/heartbeat" });
|
|
2428
4776
|
const { id } = request.params;
|
|
2429
4777
|
const input = taskRunHeartbeatSchema.parse(request.body ?? {});
|
|
2430
|
-
return {
|
|
4778
|
+
return {
|
|
4779
|
+
taskRun: heartbeatTaskRun(id, input, new Date(), toActivityContext(auth))
|
|
4780
|
+
};
|
|
2431
4781
|
});
|
|
2432
4782
|
app.post("/api/v1/task-runs/:id/heartbeat", async (request) => {
|
|
2433
4783
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/heartbeat" });
|
|
2434
4784
|
const { id } = request.params;
|
|
2435
4785
|
const input = taskRunHeartbeatSchema.parse(request.body ?? {});
|
|
2436
|
-
return {
|
|
4786
|
+
return {
|
|
4787
|
+
taskRun: heartbeatTaskRun(id, input, new Date(), toActivityContext(auth))
|
|
4788
|
+
};
|
|
2437
4789
|
});
|
|
2438
4790
|
app.post("/api/task-runs/:id/focus", async (request, reply) => {
|
|
2439
4791
|
markCompatibilityRoute(reply);
|
|
2440
4792
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/focus" });
|
|
2441
4793
|
const { id } = request.params;
|
|
2442
4794
|
const input = taskRunFocusSchema.parse(request.body ?? {});
|
|
2443
|
-
return {
|
|
4795
|
+
return {
|
|
4796
|
+
taskRun: focusTaskRun(id, input, new Date(), toActivityContext(auth))
|
|
4797
|
+
};
|
|
2444
4798
|
});
|
|
2445
4799
|
app.post("/api/v1/task-runs/:id/focus", async (request) => {
|
|
2446
4800
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/focus" });
|
|
2447
4801
|
const { id } = request.params;
|
|
2448
4802
|
const input = taskRunFocusSchema.parse(request.body ?? {});
|
|
2449
|
-
return {
|
|
4803
|
+
return {
|
|
4804
|
+
taskRun: focusTaskRun(id, input, new Date(), toActivityContext(auth))
|
|
4805
|
+
};
|
|
2450
4806
|
});
|
|
2451
4807
|
app.post("/api/task-runs/:id/complete", async (request, reply) => {
|
|
2452
4808
|
markCompatibilityRoute(reply);
|
|
2453
4809
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/complete" });
|
|
2454
4810
|
const { id } = request.params;
|
|
2455
4811
|
const input = taskRunFinishSchema.parse(request.body ?? {});
|
|
2456
|
-
return {
|
|
4812
|
+
return {
|
|
4813
|
+
taskRun: completeTaskRun(id, input, new Date(), toActivityContext(auth))
|
|
4814
|
+
};
|
|
2457
4815
|
});
|
|
2458
4816
|
app.post("/api/v1/task-runs/:id/complete", async (request) => {
|
|
2459
4817
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/complete" });
|
|
2460
4818
|
const { id } = request.params;
|
|
2461
4819
|
const input = taskRunFinishSchema.parse(request.body ?? {});
|
|
2462
|
-
return {
|
|
4820
|
+
return {
|
|
4821
|
+
taskRun: completeTaskRun(id, input, new Date(), toActivityContext(auth))
|
|
4822
|
+
};
|
|
2463
4823
|
});
|
|
2464
4824
|
app.post("/api/task-runs/:id/release", async (request, reply) => {
|
|
2465
4825
|
markCompatibilityRoute(reply);
|
|
2466
4826
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/release" });
|
|
2467
4827
|
const { id } = request.params;
|
|
2468
4828
|
const input = taskRunFinishSchema.parse(request.body ?? {});
|
|
2469
|
-
return {
|
|
4829
|
+
return {
|
|
4830
|
+
taskRun: releaseTaskRun(id, input, new Date(), toActivityContext(auth))
|
|
4831
|
+
};
|
|
2470
4832
|
});
|
|
2471
4833
|
app.post("/api/v1/task-runs/:id/release", async (request) => {
|
|
2472
4834
|
const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/release" });
|
|
2473
4835
|
const { id } = request.params;
|
|
2474
4836
|
const input = taskRunFinishSchema.parse(request.body ?? {});
|
|
2475
|
-
return {
|
|
4837
|
+
return {
|
|
4838
|
+
taskRun: releaseTaskRun(id, input, new Date(), toActivityContext(auth))
|
|
4839
|
+
};
|
|
2476
4840
|
});
|
|
2477
4841
|
app.post("/api/tags/suggestions", async (request, reply) => {
|
|
2478
4842
|
markCompatibilityRoute(reply);
|