forge-openclaw-plugin 0.2.4 → 0.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +113 -5
  2. package/dist/assets/board-CzgvdLO8.js +6 -0
  3. package/dist/assets/board-CzgvdLO8.js.map +1 -0
  4. package/dist/assets/favicon-BCHm9dUV.ico +0 -0
  5. package/dist/assets/index-8d_oM8fL.js +27 -0
  6. package/dist/assets/index-8d_oM8fL.js.map +1 -0
  7. package/dist/assets/index-D4A_bq8m.css +1 -0
  8. package/dist/assets/motion-STUd1O46.js +10 -0
  9. package/dist/assets/motion-STUd1O46.js.map +1 -0
  10. package/dist/assets/plus-jakarta-sans-latin-ext-wght-normal-DmpS2jIq.woff2 +0 -0
  11. package/dist/assets/plus-jakarta-sans-latin-wght-normal-eXO_dkmS.woff2 +0 -0
  12. package/dist/assets/plus-jakarta-sans-vietnamese-wght-normal-qRpaaN48.woff2 +0 -0
  13. package/dist/assets/sora-latin-ext-wght-normal-CawQDOvP.woff2 +0 -0
  14. package/dist/assets/sora-latin-wght-normal-DdqRvwsR.woff2 +0 -0
  15. package/dist/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
  16. package/dist/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
  17. package/dist/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
  18. package/dist/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
  19. package/dist/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
  20. package/dist/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
  21. package/dist/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
  22. package/dist/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
  23. package/dist/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
  24. package/dist/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
  25. package/dist/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
  26. package/dist/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
  27. package/dist/assets/table-CtNlETLc.js +23 -0
  28. package/dist/assets/table-CtNlETLc.js.map +1 -0
  29. package/dist/assets/ui-ThzkR_oW.js +46 -0
  30. package/dist/assets/ui-ThzkR_oW.js.map +1 -0
  31. package/dist/assets/vendor-CRS-psbw.css +1 -0
  32. package/dist/assets/vendor-DyHAI6nk.js +423 -0
  33. package/dist/assets/vendor-DyHAI6nk.js.map +1 -0
  34. package/dist/assets/viz-BJuBCz_G.js +34 -0
  35. package/dist/assets/viz-BJuBCz_G.js.map +1 -0
  36. package/dist/favicon.ico +0 -0
  37. package/dist/favicon.png +0 -0
  38. package/dist/index.html +29 -0
  39. package/dist/openclaw/api-client.d.ts +8 -0
  40. package/dist/openclaw/api-client.js +31 -4
  41. package/dist/openclaw/local-runtime.d.ts +3 -0
  42. package/dist/openclaw/local-runtime.js +135 -0
  43. package/dist/openclaw/parity.d.ts +4 -4
  44. package/dist/openclaw/parity.js +23 -33
  45. package/dist/openclaw/plugin-entry-shared.d.ts +4 -2
  46. package/dist/openclaw/plugin-entry-shared.js +51 -9
  47. package/dist/openclaw/routes.d.ts +12 -3
  48. package/dist/openclaw/routes.js +156 -924
  49. package/dist/openclaw/tools.js +242 -1100
  50. package/dist/server/app.js +2450 -0
  51. package/dist/server/db.js +313 -0
  52. package/dist/server/e2e-server.js +20 -0
  53. package/dist/server/errors.js +15 -0
  54. package/dist/server/index.js +16 -0
  55. package/dist/server/managers/base.js +17 -0
  56. package/dist/server/managers/contracts.js +47 -0
  57. package/dist/server/managers/platform/api-gateway-manager.js +11 -0
  58. package/dist/server/managers/platform/audit-manager.js +15 -0
  59. package/dist/server/managers/platform/authentication-manager.js +56 -0
  60. package/dist/server/managers/platform/authorization-manager.js +56 -0
  61. package/dist/server/managers/platform/background-job-manager.js +10 -0
  62. package/dist/server/managers/platform/configuration-manager.js +33 -0
  63. package/dist/server/managers/platform/database-manager.js +14 -0
  64. package/dist/server/managers/platform/event-bus-manager.js +7 -0
  65. package/dist/server/managers/platform/external-service-manager.js +11 -0
  66. package/dist/server/managers/platform/health-manager.js +7 -0
  67. package/dist/server/managers/platform/migration-manager.js +8 -0
  68. package/dist/server/managers/platform/search-index-manager.js +4 -0
  69. package/dist/server/managers/platform/secrets-manager.js +19 -0
  70. package/dist/server/managers/platform/session-manager.js +121 -0
  71. package/dist/server/managers/platform/storage-manager.js +16 -0
  72. package/dist/server/managers/platform/token-manager.js +37 -0
  73. package/dist/server/managers/platform/transaction-manager.js +8 -0
  74. package/dist/server/managers/platform/trusted-network.js +39 -0
  75. package/dist/server/managers/runtime.js +56 -0
  76. package/dist/server/managers/type-guards.js +4 -0
  77. package/dist/server/openapi.js +3512 -0
  78. package/dist/server/psyche-types.js +395 -0
  79. package/dist/server/repositories/activity-events.js +157 -0
  80. package/dist/server/repositories/collaboration.js +497 -0
  81. package/dist/server/repositories/comments.js +176 -0
  82. package/dist/server/repositories/deleted-entities.js +192 -0
  83. package/dist/server/repositories/domains.js +30 -0
  84. package/dist/server/repositories/event-log.js +64 -0
  85. package/dist/server/repositories/goals.js +159 -0
  86. package/dist/server/repositories/projects.js +214 -0
  87. package/dist/server/repositories/psyche.js +1356 -0
  88. package/dist/server/repositories/rewards.js +675 -0
  89. package/dist/server/repositories/settings.js +399 -0
  90. package/dist/server/repositories/tags.js +160 -0
  91. package/dist/server/repositories/task-runs.js +488 -0
  92. package/dist/server/repositories/tasks.js +413 -0
  93. package/dist/server/services/context.js +214 -0
  94. package/dist/server/services/dashboard.js +170 -0
  95. package/dist/server/services/entity-crud.js +576 -0
  96. package/dist/server/services/gamification.js +215 -0
  97. package/dist/server/services/insights.js +91 -0
  98. package/dist/server/services/projects.js +75 -0
  99. package/dist/server/services/psyche.js +63 -0
  100. package/dist/server/services/relations.js +28 -0
  101. package/dist/server/services/reviews.js +88 -0
  102. package/dist/server/services/run-recovery.js +13 -0
  103. package/dist/server/services/tagging.js +49 -0
  104. package/dist/server/services/task-run-watchdog.js +92 -0
  105. package/dist/server/services/work-time.js +176 -0
  106. package/dist/server/types.js +999 -0
  107. package/dist/server/web.js +91 -0
  108. package/openclaw.plugin.json +21 -9
  109. package/package.json +17 -4
  110. package/server/migrations/001_core.sql +333 -0
  111. package/server/migrations/002_psyche.sql +241 -0
  112. package/server/migrations/003_timer_execution.sql +18 -0
  113. package/server/migrations/004_psyche_linked_entities.sql +5 -0
  114. package/server/migrations/005_adaptive_schemas.sql +157 -0
  115. package/server/migrations/006_psyche_auth_setting.sql +4 -0
  116. package/server/migrations/007_deleted_entities.sql +16 -0
  117. package/skills/forge-openclaw/SKILL.md +189 -275
@@ -0,0 +1,2450 @@
1
+ import Fastify from "fastify";
2
+ import cors from "@fastify/cors";
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";
7
+ import { approveApprovalRequest, createAgentAction, createInsight, createInsightFeedback, deleteInsight, getInsightById, listAgentActions, listApprovalRequests, listInsights, rejectApprovalRequest, updateInsight } from "./repositories/collaboration.js";
8
+ import { listEventLog } from "./repositories/event-log.js";
9
+ import { createGoal, getGoalById, listGoals, updateGoal } from "./repositories/goals.js";
10
+ import { createComment, getCommentById, listComments, updateComment } from "./repositories/comments.js";
11
+ import { listDomains } from "./repositories/domains.js";
12
+ 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
+ import { createProject, updateProject } from "./repositories/projects.js";
14
+ import { createManualRewardGrant, getDailyAmbientXp, getRewardRuleById, listRewardLedger, listRewardRules, recordSessionEvent, updateRewardRule } from "./repositories/rewards.js";
15
+ import { listAgentIdentities, getSettings, isPsycheAuthRequired, updateSettings, verifyAgentToken } from "./repositories/settings.js";
16
+ import { createTag, getTagById, listTags, updateTag } from "./repositories/tags.js";
17
+ import { claimTaskRun, completeTaskRun, focusTaskRun, heartbeatTaskRun, listTaskRuns, recoverTimedOutTaskRuns, releaseTaskRun } from "./repositories/task-runs.js";
18
+ import { createTask, createTaskWithIdempotency, getTaskById, listTasks, uncompleteTask, updateTask } from "./repositories/tasks.js";
19
+ import { getDashboard } from "./services/dashboard.js";
20
+ import { getOverviewContext, getRiskContext, getTodayContext } from "./services/context.js";
21
+ import { buildGamificationOverview, buildGamificationProfile, buildXpMomentumPulse } from "./services/gamification.js";
22
+ import { getInsightsPayload } from "./services/insights.js";
23
+ import { createEntities, deleteEntities, deleteEntity, getSettingsBinPayload, restoreEntities, searchEntities, updateEntities } from "./services/entity-crud.js";
24
+ import { getPsycheOverview } from "./services/psyche.js";
25
+ import { getProjectBoard, listProjectSummaries } from "./services/projects.js";
26
+ import { getWeeklyReviewPayload } from "./services/reviews.js";
27
+ import { createTaskRunWatchdog } from "./services/task-run-watchdog.js";
28
+ import { suggestTags } from "./services/tagging.js";
29
+ import { PSYCHE_ENTITY_TYPES, createBehaviorSchema, commentListQuerySchema, createBeliefEntrySchema, createBehaviorPatternSchema, createCommentSchema, createEmotionDefinitionSchema, createEventTypeSchema, createModeGuideSessionSchema, createModeProfileSchema, createPsycheValueSchema, createTriggerReportSchema, updateBehaviorSchema, updateBeliefEntrySchema, updateBehaviorPatternSchema, updateCommentSchema, updateEmotionDefinitionSchema, updateEventTypeSchema, updateModeGuideSessionSchema, updateModeProfileSchema, updatePsycheValueSchema, updateTriggerReportSchema } from "./psyche-types.js";
30
+ import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createProjectSchema, createManualRewardGrantSchema, createSessionEventSchema, createTagSchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateInsightSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskSchema } from "./types.js";
31
+ import { buildOpenApiDocument } from "./openapi.js";
32
+ import { registerWebRoutes } from "./web.js";
33
+ import { createManagerRuntime } from "./managers/runtime.js";
34
+ import { isManagerError } from "./managers/type-guards.js";
35
+ const COMPATIBILITY_SUNSET = "transitional-node";
36
+ function markCompatibilityRoute(reply) {
37
+ reply.header("Deprecation", "true");
38
+ reply.header("Sunset", COMPATIBILITY_SUNSET);
39
+ reply.header("Link", '</api/v1/openapi.json>; rel="successor-version"');
40
+ }
41
+ function markDeprecatedAliasRoute(reply, successorPath) {
42
+ reply.header("Deprecation", "true");
43
+ reply.header("Sunset", COMPATIBILITY_SUNSET);
44
+ reply.header("Link", `<${successorPath}>; rel="successor-version"`);
45
+ }
46
+ function buildEventStreamMeta() {
47
+ return {
48
+ transport: "sse",
49
+ endpoint: "/api/v1/events/stream",
50
+ retryMs: 3000,
51
+ heartbeatIntervalMs: 15000,
52
+ pollIntervalMs: 3000,
53
+ events: [
54
+ {
55
+ name: "snapshot",
56
+ description: "Initial connection payload carrying the latest activity watermark.",
57
+ payload: {
58
+ generatedAt: "date-time",
59
+ latestActivityId: "string|null"
60
+ }
61
+ },
62
+ {
63
+ name: "activity",
64
+ description: "Emitted when a newer activity event becomes visible.",
65
+ payload: "ActivityEvent"
66
+ },
67
+ {
68
+ name: "collaboration",
69
+ description: "Emitted when approvals, insights, or agent actions change.",
70
+ payload: {
71
+ entityType: "string",
72
+ entityId: "string"
73
+ }
74
+ },
75
+ {
76
+ name: "reward",
77
+ description: "Emitted when XP or reward-ledger state changes.",
78
+ payload: {
79
+ deltaXp: "integer",
80
+ entityType: "string",
81
+ entityId: "string"
82
+ }
83
+ },
84
+ {
85
+ name: "heartbeat",
86
+ description: "Keepalive pulse used to keep the connection warm.",
87
+ payload: {
88
+ now: "date-time"
89
+ }
90
+ }
91
+ ],
92
+ reconnect: {
93
+ strategy: "client-reconnect",
94
+ lastEventSupport: false
95
+ }
96
+ };
97
+ }
98
+ function readSingleForwardedHeader(value) {
99
+ if (Array.isArray(value)) {
100
+ return value[0]?.split(",")[0]?.trim() || null;
101
+ }
102
+ if (typeof value === "string") {
103
+ return value.split(",")[0]?.trim() || null;
104
+ }
105
+ return null;
106
+ }
107
+ function getRequestOrigin(request) {
108
+ const protocol = readSingleForwardedHeader(request.headers["x-forwarded-proto"]) ?? request.protocol ?? "http";
109
+ const host = readSingleForwardedHeader(request.headers["x-forwarded-host"]) ??
110
+ readSingleForwardedHeader(request.headers.host) ??
111
+ request.hostname;
112
+ return `${protocol}://${host}`;
113
+ }
114
+ const AGENT_ONBOARDING_ENTITY_CATALOG = [
115
+ {
116
+ entityType: "goal",
117
+ purpose: "A long-horizon outcome or direction. Goals anchor projects and tasks.",
118
+ minimumCreateFields: ["title"],
119
+ relationshipRules: [
120
+ "Goals sit above projects and tasks.",
121
+ "Projects should usually link to one goal through goalId.",
122
+ "Tasks can link directly to a goal when no project exists yet."
123
+ ],
124
+ searchHints: ["Search by title before creating a new goal.", "Use status filters when looking for paused or completed goals."],
125
+ examples: [
126
+ '{"title":"Create meaningfully","horizon":"lifetime","description":"Make work that is honest, beautiful, and published."}',
127
+ '{"title":"Build a beautiful family","horizon":"lifetime","description":"Invest in love, stability, and shared rituals."}'
128
+ ],
129
+ fieldGuide: [
130
+ { name: "title", type: "string", required: true, description: "Human-readable goal name." },
131
+ { name: "description", type: "string", required: false, description: "Why the goal matters or what success looks like.", defaultValue: "" },
132
+ { name: "horizon", type: "quarter|year|lifetime", required: false, description: "How far out the goal is meant to live.", enumValues: ["quarter", "year", "lifetime"], defaultValue: "year" },
133
+ { name: "status", type: "active|paused|completed", required: false, description: "Current lifecycle state for the goal.", enumValues: ["active", "paused", "completed"], defaultValue: "active" },
134
+ { name: "targetPoints", type: "integer", required: false, description: "Approximate XP/point target for the goal.", defaultValue: 400 },
135
+ { name: "themeColor", type: "hex-color", required: false, description: "Visual color used in the UI.", defaultValue: "#c8a46b" },
136
+ { name: "tagIds", type: "string[]", required: false, description: "Existing tag ids linked to the goal.", defaultValue: [] }
137
+ ]
138
+ },
139
+ {
140
+ entityType: "project",
141
+ purpose: "A concrete multi-step workstream under a goal.",
142
+ minimumCreateFields: ["goalId", "title"],
143
+ relationshipRules: [
144
+ "Every project belongs to a goal through goalId.",
145
+ "Tasks can link to a project through projectId.",
146
+ "Projects inherit strategic meaning from their parent goal."
147
+ ],
148
+ searchHints: ["Search by title inside the target goal before creating a new project."],
149
+ examples: ['{"goalId":"goal_create_meaningfully","title":"Launch the public Forge plugin","description":"Ship a real public release that people can install."}'],
150
+ fieldGuide: [
151
+ { name: "goalId", type: "string", required: true, description: "Existing parent goal id." },
152
+ { name: "title", type: "string", required: true, description: "Project name." },
153
+ { name: "description", type: "string", required: false, description: "Desired outcome or scope.", defaultValue: "" },
154
+ { name: "status", type: "active|paused|completed", required: false, description: "Lifecycle state.", enumValues: ["active", "paused", "completed"], defaultValue: "active" },
155
+ { name: "targetPoints", type: "integer", required: false, description: "Approximate XP/point target for the project.", defaultValue: 240 },
156
+ { name: "themeColor", type: "hex-color", required: false, description: "Visual color used in the UI.", defaultValue: "#c0c1ff" }
157
+ ]
158
+ },
159
+ {
160
+ entityType: "task",
161
+ purpose: "A concrete actionable work item. Tasks are what the user actually does.",
162
+ minimumCreateFields: ["title"],
163
+ relationshipRules: [
164
+ "A task can link to a goal, a project, both, or neither.",
165
+ "Live work is tracked by task runs, not by task status alone.",
166
+ "A task status of in_progress does not guarantee a live active run."
167
+ ],
168
+ searchHints: ["Search by title before creating a duplicate task.", "Use linkedTo filters when you know the parent goal or project."],
169
+ examples: ['{"title":"Write the plugin release notes","projectId":"project_forge_plugin_launch","status":"focus","priority":"high"}'],
170
+ fieldGuide: [
171
+ { name: "title", type: "string", required: true, description: "Concrete action label." },
172
+ { name: "description", type: "string", required: false, description: "Helpful context or acceptance notes.", defaultValue: "" },
173
+ { name: "status", type: "backlog|focus|in_progress|blocked|done", required: false, description: "Board lane or completion state.", enumValues: ["backlog", "focus", "in_progress", "blocked", "done"], defaultValue: "backlog" },
174
+ { name: "priority", type: "low|medium|high|critical", required: false, description: "Relative urgency.", enumValues: ["low", "medium", "high", "critical"], defaultValue: "medium" },
175
+ { name: "owner", type: "string", required: false, description: "Human-facing owner label.", defaultValue: "Albert" },
176
+ { name: "goalId", type: "string|null", required: false, description: "Linked goal id.", defaultValue: null, nullable: true },
177
+ { name: "projectId", type: "string|null", required: false, description: "Linked project id.", defaultValue: null, nullable: true },
178
+ { name: "dueDate", type: "YYYY-MM-DD|null", required: false, description: "Optional due date.", defaultValue: null, nullable: true },
179
+ { name: "effort", type: "light|deep|marathon", required: false, description: "How heavy the task feels.", enumValues: ["light", "deep", "marathon"], defaultValue: "deep" },
180
+ { name: "energy", type: "low|steady|high", required: false, description: "Energy demand.", enumValues: ["low", "steady", "high"], defaultValue: "steady" },
181
+ { name: "points", type: "integer", required: false, description: "Reward value for the task.", defaultValue: 40 },
182
+ { name: "sortOrder", type: "integer", required: false, description: "Lane ordering hint when set explicitly." },
183
+ { name: "tagIds", type: "string[]", required: false, description: "Existing tag ids linked to the task.", defaultValue: [] }
184
+ ]
185
+ },
186
+ {
187
+ entityType: "insight",
188
+ purpose: "An agent-authored observation or recommendation grounded in Forge data.",
189
+ minimumCreateFields: ["title", "summary", "recommendation"],
190
+ relationshipRules: [
191
+ "Insights can optionally point at one entity through entityType and entityId.",
192
+ "Use insights for interpretation or advice, not as a replacement for goals, tasks, or trigger reports."
193
+ ],
194
+ searchHints: ["Search recent insights before posting a new one if the same pattern may already be captured."],
195
+ examples: ['{"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."}'],
196
+ fieldGuide: [
197
+ { name: "entityType", type: "string|null", required: false, description: "Optional linked entity type.", defaultValue: null, nullable: true },
198
+ { name: "entityId", type: "string|null", required: false, description: "Optional linked entity id.", defaultValue: null, nullable: true },
199
+ { name: "timeframeLabel", type: "string|null", required: false, description: "Optional time window label.", defaultValue: null, nullable: true },
200
+ { name: "title", type: "string", required: true, description: "Insight title." },
201
+ { name: "summary", type: "string", required: true, description: "Short explanation of the pattern or tension." },
202
+ { name: "recommendation", type: "string", required: true, description: "Actionable next move or reframing." },
203
+ { name: "rationale", type: "string", required: false, description: "Why this insight is grounded in the data.", defaultValue: "" },
204
+ { name: "confidence", type: "number", required: false, description: "Confidence from 0 to 1.", defaultValue: 0.7 },
205
+ { name: "visibility", type: "string", required: false, description: "Visibility mode for the insight.", defaultValue: "visible" },
206
+ { name: "ctaLabel", type: "string", required: false, description: "CTA shown in the UI.", defaultValue: "Review insight" }
207
+ ]
208
+ },
209
+ {
210
+ entityType: "event_type",
211
+ purpose: "A reusable event taxonomy label for trigger reports, such as criticism, conflict, rupture, or overload.",
212
+ minimumCreateFields: ["label"],
213
+ relationshipRules: [
214
+ "Trigger reports can reference one event type through eventTypeId.",
215
+ "Use event types to normalize repeated report categories instead of inventing new wording every time."
216
+ ],
217
+ searchHints: ["Search by label before creating a new event type.", "Prefer existing event types when one clearly fits the situation."],
218
+ fieldGuide: [
219
+ { name: "label", type: "string", required: true, description: "Human-readable event type label." },
220
+ { name: "description", type: "string", required: false, description: "What kind of incident this event type represents.", defaultValue: "" }
221
+ ]
222
+ },
223
+ {
224
+ entityType: "emotion_definition",
225
+ purpose: "A reusable emotion vocabulary item for trigger reports, such as shame, anger, grief, or relief.",
226
+ minimumCreateFields: ["label"],
227
+ relationshipRules: [
228
+ "Trigger report emotions can reference an emotion definition through emotionDefinitionId.",
229
+ "Use emotion definitions to normalize repeated emotional labels across reports."
230
+ ],
231
+ searchHints: ["Search by label before creating a new emotion definition.", "Prefer an existing emotion definition when the label already captures the feeling well."],
232
+ fieldGuide: [
233
+ { name: "label", type: "string", required: true, description: "Emotion label." },
234
+ { name: "description", type: "string", required: false, description: "What this emotion label is meant to capture.", defaultValue: "" },
235
+ { name: "category", type: "string", required: false, description: "Optional grouping such as threat, grief, anger, or connection.", defaultValue: "" }
236
+ ]
237
+ },
238
+ {
239
+ entityType: "psyche_value",
240
+ purpose: "An ACT-style value or direction the user wants to orient toward.",
241
+ minimumCreateFields: ["title"],
242
+ relationshipRules: [
243
+ "Values can link to goals, projects, and tasks.",
244
+ "Patterns, behaviors, beliefs, and reports can all point back to values."
245
+ ],
246
+ searchHints: ["Search by title before creating a new value.", "Use linkedTo if the value should already be attached to a goal or task."],
247
+ examples: ['{"title":"Steadiness","valuedDirection":"Respond calmly instead of collapsing or reacting fast.","whyItMatters":"I want to stay grounded in relationships and work."}'],
248
+ fieldGuide: [
249
+ { name: "title", type: "string", required: true, description: "Value name." },
250
+ { name: "description", type: "string", required: false, description: "What the value means in practice.", defaultValue: "" },
251
+ { name: "valuedDirection", type: "string", required: false, description: "How the user wants to live or act when guided by this value.", defaultValue: "" },
252
+ { name: "whyItMatters", type: "string", required: false, description: "Why the value matters to the user.", defaultValue: "" },
253
+ { name: "linkedGoalIds", type: "string[]", required: false, description: "Linked goal ids.", defaultValue: [] },
254
+ { name: "linkedProjectIds", type: "string[]", required: false, description: "Linked project ids.", defaultValue: [] },
255
+ { name: "linkedTaskIds", type: "string[]", required: false, description: "Linked task ids.", defaultValue: [] },
256
+ { name: "committedActions", type: "string[]", required: false, description: "Small concrete actions that enact the value.", defaultValue: [] }
257
+ ]
258
+ },
259
+ {
260
+ entityType: "behavior_pattern",
261
+ purpose: "A recurring loop or trigger-response pattern.",
262
+ minimumCreateFields: ["title"],
263
+ relationshipRules: [
264
+ "Patterns can link to values, beliefs, and modes.",
265
+ "Trigger reports can link back to patterns they instantiate."
266
+ ],
267
+ searchHints: ["Search by title or by trigger language before creating a new pattern."],
268
+ examples: ['{"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"}'],
269
+ fieldGuide: [
270
+ { name: "title", type: "string", required: true, description: "Short pattern name." },
271
+ { name: "description", type: "string", required: false, description: "What usually happens in this loop.", defaultValue: "" },
272
+ { name: "targetBehavior", type: "string", required: false, description: "The visible behavior this pattern tends to produce.", defaultValue: "" },
273
+ { name: "cueContexts", type: "string[]", required: false, description: "Typical cues, contexts, or triggers.", defaultValue: [] },
274
+ { name: "shortTermPayoff", type: "string", required: false, description: "What the loop gives immediately.", defaultValue: "" },
275
+ { name: "longTermCost", type: "string", required: false, description: "What the loop costs later.", defaultValue: "" },
276
+ { name: "preferredResponse", type: "string", required: false, description: "Preferred alternative response.", defaultValue: "" },
277
+ { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
278
+ { name: "linkedSchemaLabels", type: "string[]", required: false, description: "Schema labels involved in the pattern.", defaultValue: [] },
279
+ { name: "linkedModeLabels", type: "string[]", required: false, description: "Mode labels involved in the pattern.", defaultValue: [] },
280
+ { name: "linkedModeIds", type: "string[]", required: false, description: "Linked mode ids.", defaultValue: [] },
281
+ { name: "linkedBeliefIds", type: "string[]", required: false, description: "Linked belief ids.", defaultValue: [] }
282
+ ]
283
+ },
284
+ {
285
+ entityType: "behavior",
286
+ purpose: "A concrete behavior pattern element or habit worth tracking directly.",
287
+ minimumCreateFields: ["kind", "title"],
288
+ relationshipRules: [
289
+ "Behaviors can connect to behavior patterns, values, schemas, and modes.",
290
+ "Trigger reports can link to behaviors they contained."
291
+ ],
292
+ searchHints: ["Search by title and kind before creating a new behavior."],
293
+ examples: ['{"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"}'],
294
+ fieldGuide: [
295
+ { name: "kind", type: "away|committed|recovery", required: true, description: "Whether the behavior moves away from values, toward them, or repairs after rupture.", enumValues: ["away", "committed", "recovery"] },
296
+ { name: "title", type: "string", required: true, description: "Behavior label." },
297
+ { name: "description", type: "string", required: false, description: "What the behavior looks like.", defaultValue: "" },
298
+ { name: "commonCues", type: "string[]", required: false, description: "Typical cues for this behavior.", defaultValue: [] },
299
+ { name: "urgeStory", type: "string", required: false, description: "What the inner urge or story feels like.", defaultValue: "" },
300
+ { name: "shortTermPayoff", type: "string", required: false, description: "Immediate payoff.", defaultValue: "" },
301
+ { name: "longTermCost", type: "string", required: false, description: "Longer-term cost.", defaultValue: "" },
302
+ { name: "replacementMove", type: "string", required: false, description: "Preferred replacement move.", defaultValue: "" },
303
+ { name: "repairPlan", type: "string", required: false, description: "Repair plan after the behavior occurs.", defaultValue: "" },
304
+ { name: "linkedPatternIds", type: "string[]", required: false, description: "Linked behavior pattern ids.", defaultValue: [] },
305
+ { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
306
+ { name: "linkedSchemaIds", type: "string[]", required: false, description: "Linked schema ids.", defaultValue: [] },
307
+ { name: "linkedModeIds", type: "string[]", required: false, description: "Linked mode ids.", defaultValue: [] }
308
+ ]
309
+ },
310
+ {
311
+ entityType: "belief_entry",
312
+ purpose: "A belief or schema-linked statement worth tracking and testing.",
313
+ minimumCreateFields: ["statement", "beliefType"],
314
+ relationshipRules: [
315
+ "Beliefs can link to values, behaviors, modes, and trigger reports.",
316
+ "Behavior patterns can point to beliefs that keep the loop alive."
317
+ ],
318
+ searchHints: ["Search by statement or known schema theme before creating a new belief entry."],
319
+ examples: ['{"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."}'],
320
+ fieldGuide: [
321
+ { name: "schemaId", type: "string|null", required: false, description: "Optional linked schema catalog id.", defaultValue: null, nullable: true },
322
+ { name: "statement", type: "string", required: true, description: "Belief statement in the user's own words." },
323
+ { name: "beliefType", type: "absolute|conditional", required: true, description: "Whether the belief is absolute or if-then shaped.", enumValues: ["absolute", "conditional"] },
324
+ { name: "originNote", type: "string", required: false, description: "Where the belief seems to come from.", defaultValue: "" },
325
+ { name: "confidence", type: "integer", required: false, description: "How strongly the belief feels true from 0 to 100.", defaultValue: 60 },
326
+ { name: "evidenceFor", type: "string[]", required: false, description: "Evidence that seems to support the belief.", defaultValue: [] },
327
+ { name: "evidenceAgainst", type: "string[]", required: false, description: "Evidence that weakens the belief.", defaultValue: [] },
328
+ { name: "flexibleAlternative", type: "string", required: false, description: "More flexible alternative belief.", defaultValue: "" },
329
+ { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
330
+ { name: "linkedBehaviorIds", type: "string[]", required: false, description: "Linked behavior ids.", defaultValue: [] },
331
+ { name: "linkedModeIds", type: "string[]", required: false, description: "Linked mode ids.", defaultValue: [] },
332
+ { name: "linkedReportIds", type: "string[]", required: false, description: "Linked trigger report ids.", defaultValue: [] }
333
+ ]
334
+ },
335
+ {
336
+ entityType: "mode_profile",
337
+ purpose: "A schema-mode profile such as critic, child, coping, or healthy adult parts.",
338
+ minimumCreateFields: ["family", "title"],
339
+ relationshipRules: [
340
+ "Modes can link to patterns, behaviors, and values.",
341
+ "Trigger reports can include linkedModeIds and modeOverlays that reference modes."
342
+ ],
343
+ searchHints: ["Search by title or family before creating a new mode profile."],
344
+ examples: ['{"family":"coping","title":"Cold controller","fear":"If I soften, I will be humiliated or lose control.","protectiveJob":"Stay hyper-competent and unreachable when threatened."}'],
345
+ fieldGuide: [
346
+ { name: "family", type: "coping|child|critic_parent|healthy_adult|happy_child", required: true, description: "Mode family.", enumValues: ["coping", "child", "critic_parent", "healthy_adult", "happy_child"] },
347
+ { name: "title", type: "string", required: true, description: "Mode title." },
348
+ { name: "archetype", type: "string", required: false, description: "Optional archetype label.", defaultValue: "" },
349
+ { name: "persona", type: "string", required: false, description: "Narrative or felt sense of the mode.", defaultValue: "" },
350
+ { name: "imagery", type: "string", required: false, description: "Imagery associated with the mode.", defaultValue: "" },
351
+ { name: "symbolicForm", type: "string", required: false, description: "Symbolic form or metaphor.", defaultValue: "" },
352
+ { name: "facialExpression", type: "string", required: false, description: "Typical facial expression or posture.", defaultValue: "" },
353
+ { name: "fear", type: "string", required: false, description: "Core fear carried by the mode.", defaultValue: "" },
354
+ { name: "burden", type: "string", required: false, description: "Burden or pain the mode carries.", defaultValue: "" },
355
+ { name: "protectiveJob", type: "string", required: false, description: "What job the mode thinks it is doing.", defaultValue: "" },
356
+ { name: "originContext", type: "string", required: false, description: "Where the mode seems to come from.", defaultValue: "" },
357
+ { name: "firstAppearanceAt", type: "string|null", required: false, description: "Optional first-seen marker.", defaultValue: null, nullable: true },
358
+ { name: "linkedPatternIds", type: "string[]", required: false, description: "Linked pattern ids.", defaultValue: [] },
359
+ { name: "linkedBehaviorIds", type: "string[]", required: false, description: "Linked behavior ids.", defaultValue: [] },
360
+ { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] }
361
+ ]
362
+ },
363
+ {
364
+ entityType: "mode_guide_session",
365
+ purpose: "A guided mode-mapping session that stores structured answers and candidate mode interpretations.",
366
+ minimumCreateFields: ["summary", "answers", "results"],
367
+ relationshipRules: [
368
+ "Mode guide sessions help the user reason toward likely modes before or alongside mode profiles.",
369
+ "Use mode guide sessions for guided interpretation, not as a replacement for durable mode profiles."
370
+ ],
371
+ searchHints: ["Search by summary when revisiting a prior guided mode session."],
372
+ examples: ['{"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."}]}'],
373
+ fieldGuide: [
374
+ { name: "summary", type: "string", required: true, description: "Short summary of what the guided session explored." },
375
+ { name: "answers", type: "array", required: true, description: "List of { questionKey, value } items capturing the user's guided answers." },
376
+ { name: "results", type: "array", required: true, description: "List of { family, archetype, label, confidence 0-1, reasoning } candidate mode interpretations." }
377
+ ]
378
+ },
379
+ {
380
+ entityType: "trigger_report",
381
+ purpose: "A structured reflective incident report that ties situation, emotions, thoughts, behaviors, consequences, and next moves together.",
382
+ minimumCreateFields: ["title"],
383
+ relationshipRules: [
384
+ "Trigger reports can link to values, goals, projects, tasks, patterns, behaviors, beliefs, and modes.",
385
+ "A report is the best container for one specific emotionally meaningful episode.",
386
+ "Use reports when you need one event chain, not just a generic pattern."
387
+ ],
388
+ searchHints: ["Search by title, event wording, or linked entities before creating a duplicate report."],
389
+ examples: ['{"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"]}'],
390
+ fieldGuide: [
391
+ { name: "title", type: "string", required: true, description: "Short name for the incident." },
392
+ { name: "status", type: "draft|reviewed|integrated", required: false, description: "Reflection progress state.", enumValues: ["draft", "reviewed", "integrated"], defaultValue: "draft" },
393
+ { name: "eventTypeId", type: "string|null", required: false, description: "Known event type id if already cataloged.", defaultValue: null, nullable: true },
394
+ { name: "customEventType", type: "string", required: false, description: "Free-text event type when no existing type fits.", defaultValue: "" },
395
+ { name: "eventSituation", type: "string", required: false, description: "What happened in the situation.", defaultValue: "" },
396
+ { name: "occurredAt", type: "string|null", required: false, description: "When it happened.", defaultValue: null, nullable: true },
397
+ { name: "emotions", type: "array", required: false, description: "List of { emotionDefinitionId|null, label, intensity 0-100, note } items.", defaultValue: [] },
398
+ { name: "thoughts", type: "array", required: false, description: "List of { text, parentMode, criticMode, beliefId|null } items.", defaultValue: [] },
399
+ { name: "behaviors", type: "array", required: false, description: "List of { text, mode, behaviorId|null } items.", defaultValue: [] },
400
+ { name: "consequences", type: "object", required: false, description: "Object with selfShortTerm, selfLongTerm, othersShortTerm, othersLongTerm string arrays." },
401
+ { name: "linkedPatternIds", type: "string[]", required: false, description: "Linked pattern ids.", defaultValue: [] },
402
+ { name: "linkedValueIds", type: "string[]", required: false, description: "Linked value ids.", defaultValue: [] },
403
+ { name: "linkedGoalIds", type: "string[]", required: false, description: "Linked goal ids.", defaultValue: [] },
404
+ { name: "linkedProjectIds", type: "string[]", required: false, description: "Linked project ids.", defaultValue: [] },
405
+ { name: "linkedTaskIds", type: "string[]", required: false, description: "Linked task ids.", defaultValue: [] },
406
+ { name: "linkedBehaviorIds", type: "string[]", required: false, description: "Linked behavior ids.", defaultValue: [] },
407
+ { name: "linkedBeliefIds", type: "string[]", required: false, description: "Linked belief ids.", defaultValue: [] },
408
+ { name: "linkedModeIds", type: "string[]", required: false, description: "Linked mode ids.", defaultValue: [] },
409
+ { name: "modeOverlays", type: "string[]", required: false, description: "Extra mode labels noticed during the incident.", defaultValue: [] },
410
+ { name: "schemaLinks", type: "string[]", required: false, description: "Schema names or themes that seem related to the incident.", defaultValue: [] },
411
+ { name: "modeTimeline", type: "array", required: false, description: "List of { stage, modeId|null, label, note } items describing the sequence of modes.", defaultValue: [] },
412
+ { name: "nextMoves", type: "string[]", required: false, description: "Concrete next steps or repair moves.", defaultValue: [] }
413
+ ]
414
+ }
415
+ ];
416
+ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
417
+ {
418
+ focus: "behavior_pattern",
419
+ useWhen: "Use for a recurring loop that shows up across multiple situations and can be described as cue -> response -> payoff -> cost -> preferred response.",
420
+ coachingGoal: "Help the user build a CBT-style functional analysis instead of just naming the problem vaguely.",
421
+ askSequence: [
422
+ "Name the loop in plain language.",
423
+ "Identify the typical cue or context.",
424
+ "Describe the visible behavior or sequence once it starts.",
425
+ "Clarify the short-term payoff or protection.",
426
+ "Clarify the long-term cost.",
427
+ "Name the preferred alternative response."
428
+ ],
429
+ requiredForCreate: ["title"],
430
+ highValueOptionalFields: ["description", "targetBehavior", "cueContexts", "shortTermPayoff", "longTermCost", "preferredResponse", "linkedBeliefIds", "linkedModeIds", "linkedValueIds"],
431
+ exampleQuestions: [
432
+ "What usually sets this loop off?",
433
+ "What do you tend to do next, outwardly or inwardly?",
434
+ "What does that move do for you immediately?",
435
+ "What does it cost you later?",
436
+ "If this loop loosened a little, what response would you want to make instead?"
437
+ ],
438
+ notes: [
439
+ "A pattern is usually the best Psyche container for functional analysis.",
440
+ "If the user is describing one specific episode rather than a repeated loop, prefer a trigger report."
441
+ ]
442
+ },
443
+ {
444
+ focus: "belief_entry",
445
+ useWhen: "Use for a belief, rule, or self-statement that keeps showing up in reactions, especially when the user can phrase it as a sentence.",
446
+ coachingGoal: "Turn implicit self-talk or a likely schema theme into one explicit belief statement that can be tested and linked to patterns, reports, and modes.",
447
+ askSequence: [
448
+ "Capture the belief in the user's own words.",
449
+ "Decide whether it is absolute or conditional.",
450
+ "Estimate how true it feels from 0 to 100.",
451
+ "Collect evidence for and evidence against.",
452
+ "Offer a more flexible alternative belief.",
453
+ "Link a schemaId only when a real schema catalog match is known."
454
+ ],
455
+ requiredForCreate: ["statement", "beliefType"],
456
+ highValueOptionalFields: ["schemaId", "confidence", "originNote", "evidenceFor", "evidenceAgainst", "flexibleAlternative", "linkedReportIds", "linkedBehaviorIds", "linkedModeIds"],
457
+ exampleQuestions: [
458
+ "What is the sentence your mind seems to be pushing here?",
459
+ "Is it more of an always/never belief, or an if-then rule?",
460
+ "How true does it feel right now from 0 to 100?",
461
+ "What seems to support it, and what weakens it?",
462
+ "What would a more flexible alternative sound like?"
463
+ ],
464
+ notes: [
465
+ "Schema catalog entries are reference concepts; belief_entry is the user-owned record.",
466
+ "If no schema catalog match is known, omit schemaId rather than inventing one."
467
+ ]
468
+ },
469
+ {
470
+ focus: "mode_profile",
471
+ useWhen: "Use when the user is describing a recurring part-state, protector, critic, vulnerable child state, or healthy adult stance.",
472
+ coachingGoal: "Help the user describe what the mode is trying to do, what it fears, and how it presents, rather than reducing it to a label only.",
473
+ askSequence: [
474
+ "Choose the mode family first.",
475
+ "Name the mode.",
476
+ "Describe the felt persona or imagery.",
477
+ "Clarify its fear, burden, and protective job.",
478
+ "Optionally note origin context and linked patterns or behaviors."
479
+ ],
480
+ requiredForCreate: ["family", "title"],
481
+ highValueOptionalFields: ["persona", "imagery", "fear", "burden", "protectiveJob", "originContext", "linkedPatternIds", "linkedBehaviorIds", "linkedValueIds"],
482
+ exampleQuestions: [
483
+ "What kind of part does this feel like: coping, child, critic-parent, healthy-adult, or happy-child?",
484
+ "If you gave this mode a name, what would it be?",
485
+ "What is it afraid would happen if it stopped doing its job?",
486
+ "What burden or pain does it seem to carry?"
487
+ ],
488
+ notes: [
489
+ "Mode profiles are durable parts descriptions.",
490
+ "Mode guide sessions are the guided reasoning process that can lead toward a mode profile."
491
+ ]
492
+ },
493
+ {
494
+ focus: "trigger_report",
495
+ useWhen: "Use for one specific emotionally meaningful incident that should be mapped from situation through emotions, thoughts, behaviors, consequences, and next moves.",
496
+ coachingGoal: "Help the user build a clear incident chain with enough structure to learn from one episode.",
497
+ askSequence: [
498
+ "Name the incident briefly.",
499
+ "Describe what happened in the situation.",
500
+ "Capture emotions and intensity.",
501
+ "Capture thoughts or belief-linked interpretations.",
502
+ "Capture behaviors and immediate coping moves.",
503
+ "Capture short-term and long-term consequences.",
504
+ "Identify next moves and linked patterns, beliefs, modes, values, or tasks."
505
+ ],
506
+ requiredForCreate: ["title"],
507
+ highValueOptionalFields: ["eventTypeId", "customEventType", "eventSituation", "occurredAt", "emotions", "thoughts", "behaviors", "consequences", "modeTimeline", "nextMoves", "linkedPatternIds", "linkedBeliefIds", "linkedModeIds", "linkedValueIds"],
508
+ exampleQuestions: [
509
+ "What happened, as concretely as you can say it?",
510
+ "What emotions were there, and how intense were they?",
511
+ "What thoughts or meanings showed up?",
512
+ "What did you do next?",
513
+ "What did that do for you short term, and what did it cost later?",
514
+ "What would be the next good move now?"
515
+ ],
516
+ notes: [
517
+ "Use eventTypeId only when a known event taxonomy item fits; otherwise use customEventType.",
518
+ "Use emotionDefinitionId only when a known emotion definition fits; otherwise keep the raw label."
519
+ ]
520
+ }
521
+ ];
522
+ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
523
+ {
524
+ toolName: "forge_search_entities",
525
+ summary: "Search Forge entities before create or update.",
526
+ whenToUse: "Use when duplicate risk exists or when you need ids before mutating.",
527
+ inputShape: "{ searches: Array<{ entityTypes?: CrudEntityType[], query?: string, ids?: string[], status?: string[], linkedTo?: { entityType, id }, includeDeleted?: boolean, limit?: number, clientRef?: string }> }",
528
+ requiredFields: ["searches"],
529
+ notes: [
530
+ "searches is always an array, even for a single search.",
531
+ "linkedTo is useful when looking for items under one parent entity."
532
+ ],
533
+ example: '{"searches":[{"entityTypes":["goal"],"query":"Create meaningfully","limit":10,"clientRef":"goal-search-1"}]}'
534
+ },
535
+ {
536
+ toolName: "forge_create_entities",
537
+ summary: "Create one or more entities in one ordered batch.",
538
+ whenToUse: "Use after explicit save intent and after duplicate checks when needed.",
539
+ inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, clientRef?: string, data: object }> }",
540
+ requiredFields: ["operations", "operations[].entityType", "operations[].data"],
541
+ notes: [
542
+ "entityType alone is never enough; full data is required.",
543
+ "Batch multiple related creates together when they come from one user ask."
544
+ ],
545
+ example: '{"operations":[{"entityType":"goal","data":{"title":"Create meaningfully","horizon":"lifetime"},"clientRef":"goal-1"},{"entityType":"project","data":{"goalId":"goal_123","title":"Launch the first public Forge plugin build"},"clientRef":"project-1"}]}'
546
+ },
547
+ {
548
+ toolName: "forge_update_entities",
549
+ summary: "Patch one or more entities in one ordered batch.",
550
+ whenToUse: "Use when ids are known and the user explicitly wants a change persisted.",
551
+ inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, id: string, clientRef?: string, patch: object }> }",
552
+ requiredFields: ["operations", "operations[].entityType", "operations[].id", "operations[].patch"],
553
+ notes: ["patch is partial; only send the fields that should change."],
554
+ example: '{"operations":[{"entityType":"task","id":"task_123","patch":{"status":"focus","priority":"high"},"clientRef":"task-patch-1"}]}'
555
+ },
556
+ {
557
+ toolName: "forge_delete_entities",
558
+ summary: "Delete one or more entities through the batch delete flow.",
559
+ whenToUse: "Use for explicit delete intent only.",
560
+ inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, id: string, clientRef?: string, mode?: \"soft\"|\"hard\", reason?: string }> }",
561
+ requiredFields: ["operations", "operations[].entityType", "operations[].id"],
562
+ notes: ["Delete defaults to soft.", "Use mode=hard only for explicit permanent removal."],
563
+ example: '{"operations":[{"entityType":"task","id":"task_123","mode":"soft","reason":"Merged into another task"}]}'
564
+ },
565
+ {
566
+ toolName: "forge_restore_entities",
567
+ summary: "Restore soft-deleted entities from the settings bin.",
568
+ whenToUse: "Use when the user wants an entity brought back after a soft delete.",
569
+ inputShape: "{ atomic?: boolean, operations: Array<{ entityType: CrudEntityType, id: string, clientRef?: string }> }",
570
+ requiredFields: ["operations", "operations[].entityType", "operations[].id"],
571
+ notes: ["Restore only works for soft-deleted entities."],
572
+ example: '{"operations":[{"entityType":"goal","id":"goal_123","clientRef":"goal-restore-1"}]}'
573
+ },
574
+ {
575
+ toolName: "forge_post_insight",
576
+ summary: "Store an agent-authored insight.",
577
+ whenToUse: "Use when you have a data-grounded observation or recommendation worth keeping visible in Forge.",
578
+ inputShape: "{ entityType?: string|null, entityId?: string|null, timeframeLabel?: string|null, title: string, summary: string, recommendation: string, rationale?: string, confidence?: number, visibility?: string, ctaLabel?: string }",
579
+ requiredFields: ["title", "summary", "recommendation"],
580
+ notes: ["Insights are for interpretation and advice, not for replacing user-owned goals or tasks."],
581
+ 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}'
582
+ },
583
+ {
584
+ toolName: "forge_log_work",
585
+ summary: "Log work that already happened.",
586
+ whenToUse: "Use for retroactive work, not for starting a live session.",
587
+ 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[] }",
588
+ requiredFields: ["taskId or title"],
589
+ notes: ["Use taskId when logging work against an existing task.", "Use title when a new completed work item should be created and logged."],
590
+ example: '{"taskId":"task_123","summary":"Finished the review draft and cleaned the notes.","points":40}'
591
+ },
592
+ {
593
+ toolName: "forge_start_task_run",
594
+ summary: "Start truthful live work on a task.",
595
+ whenToUse: "Use when the user wants to begin working now.",
596
+ inputShape: "{ taskId: string, actor: string, timerMode?: \"planned\"|\"unlimited\", plannedDurationSeconds?: number|null, isCurrent?: boolean, leaseTtlSeconds?: number, note?: string }",
597
+ requiredFields: ["taskId", "actor"],
598
+ notes: ["If timerMode is planned, plannedDurationSeconds is required.", "If timerMode is unlimited, plannedDurationSeconds must be null or omitted."],
599
+ example: '{"taskId":"task_123","actor":"aurel","timerMode":"planned","plannedDurationSeconds":1500,"isCurrent":true,"leaseTtlSeconds":900,"note":"Starting focused writing block"}'
600
+ },
601
+ {
602
+ toolName: "forge_heartbeat_task_run",
603
+ summary: "Refresh an active run lease while work continues.",
604
+ whenToUse: "Use periodically during ongoing live work.",
605
+ inputShape: "{ taskRunId: string, actor?: string, leaseTtlSeconds?: number, note?: string }",
606
+ requiredFields: ["taskRunId"],
607
+ notes: ["Heartbeat extends the lease and can update the note."],
608
+ example: '{"taskRunId":"run_123","actor":"aurel","leaseTtlSeconds":900,"note":"Still in the block"}'
609
+ },
610
+ {
611
+ toolName: "forge_focus_task_run",
612
+ summary: "Mark one active run as the current focus.",
613
+ whenToUse: "Use when several runs exist and one should be the visible current run.",
614
+ inputShape: "{ taskRunId: string, actor?: string }",
615
+ requiredFields: ["taskRunId"],
616
+ notes: ["This does not complete or release a run; it just changes current focus."],
617
+ example: '{"taskRunId":"run_123","actor":"aurel"}'
618
+ },
619
+ {
620
+ toolName: "forge_complete_task_run",
621
+ summary: "Finish an active run as completed work.",
622
+ whenToUse: "Use when the user has finished the live work block.",
623
+ inputShape: "{ taskRunId: string, actor?: string, note?: string }",
624
+ requiredFields: ["taskRunId"],
625
+ notes: ["This is the truthful way to finish live work and award completion effects."],
626
+ example: '{"taskRunId":"run_123","actor":"aurel","note":"Finished the review draft"}'
627
+ },
628
+ {
629
+ toolName: "forge_release_task_run",
630
+ summary: "Stop an active run without marking the task complete.",
631
+ whenToUse: "Use when the user is stopping or pausing work without completion.",
632
+ inputShape: "{ taskRunId: string, actor?: string, note?: string }",
633
+ requiredFields: ["taskRunId"],
634
+ notes: ["Use this instead of faking a stop by only changing task status."],
635
+ example: '{"taskRunId":"run_123","actor":"aurel","note":"Stopping for now; blocked on feedback"}'
636
+ }
637
+ ];
638
+ function buildAgentOnboardingPayload(request) {
639
+ const origin = getRequestOrigin(request);
640
+ return {
641
+ forgeBaseUrl: origin,
642
+ webAppUrl: `${origin}/forge/`,
643
+ apiBaseUrl: `${origin}/api/v1`,
644
+ openApiUrl: `${origin}/api/v1/openapi.json`,
645
+ healthUrl: `${origin}/api/v1/health`,
646
+ settingsUrl: `${origin}/api/v1/settings`,
647
+ tokenCreateUrl: `${origin}/api/v1/settings/tokens`,
648
+ pluginBasePath: "/forge/v1",
649
+ defaultConnectionMode: "operator_session",
650
+ defaultActorLabel: "aurel",
651
+ defaultTimeoutMs: 15_000,
652
+ recommendedScopes: [
653
+ "read",
654
+ "write",
655
+ "insights",
656
+ "rewards.manage",
657
+ "psyche.read",
658
+ "psyche.write",
659
+ "psyche.comment",
660
+ "psyche.insight",
661
+ "psyche.mode"
662
+ ],
663
+ recommendedTrustLevel: "trusted",
664
+ recommendedAutonomyMode: "approval_required",
665
+ recommendedApprovalMode: "approval_by_default",
666
+ authModes: {
667
+ operatorSession: {
668
+ label: "Quick connect",
669
+ summary: "Recommended for localhost and Tailscale. No token is required up front; Forge can bootstrap an operator session automatically.",
670
+ tokenRequired: false,
671
+ trustedTargets: ["localhost", "127.0.0.1", "*.ts.net", "100.64.0.0/10"]
672
+ },
673
+ managedToken: {
674
+ label: "Managed token",
675
+ summary: "Use a long-lived token when you want explicit scoped auth, remote non-Tailscale access, or durable agent credentials.",
676
+ tokenRequired: true
677
+ }
678
+ },
679
+ tokenRecovery: {
680
+ rawTokenStoredByForge: false,
681
+ recoveryAction: "rotate_or_issue_new_token",
682
+ rotationSummary: "Forge reveals raw tokens once. If you lose one, rotate it or issue a new token from Settings and update the plugin config.",
683
+ settingsSummary: "Token creation, rotation, and revocation all live under Forge Settings so recovery is explicit and operator-controlled."
684
+ },
685
+ requiredHeaders: {
686
+ authorization: "Authorization: Bearer <forge-api-token>",
687
+ source: "X-Forge-Source: agent",
688
+ actor: "X-Forge-Actor: <agent-label>"
689
+ },
690
+ conceptModel: {
691
+ goal: "Long-horizon direction or outcome. Goals anchor projects and sometimes tasks directly.",
692
+ project: "A multi-step workstream under one goal. Projects organize related tasks.",
693
+ task: "A concrete actionable work item. Task status is board state, not proof of live work.",
694
+ taskRun: "A live work session attached to a task. Start, heartbeat, focus, complete, and release runs instead of faking work with status alone.",
695
+ insight: "An agent-authored observation or recommendation grounded in Forge data.",
696
+ psyche: "Forge Psyche is the reflective domain for values, patterns, behaviors, beliefs, modes, and trigger reports. It is sensitive and should be handled deliberately."
697
+ },
698
+ psycheSubmoduleModel: {
699
+ value: "A value is the direction the user wants to move toward. Values orient action and can link back to goals, projects, tasks, and Psyche records.",
700
+ behaviorPattern: "A behavior pattern is the recurring CBT-style loop: cue/context, visible response, short-term payoff, long-term cost, and preferred response.",
701
+ behavior: "A behavior record is one trackable move or tendency, classified as away, committed, or recovery.",
702
+ beliefEntry: "A belief entry is the user's own trackable belief statement, including beliefType, confidence, evidence, and a more flexible alternative.",
703
+ schemaCatalog: "The schema catalog is the reference taxonomy of maladaptive and adaptive schemas. Belief entries can optionally point to one schema by schemaId, but the schema catalog is not itself the user's belief record.",
704
+ modeProfile: "A mode profile is a durable description of a recurring part-state or strategy, including family, fear, burden, protective job, and origin context.",
705
+ modeGuideSession: "A mode guide session is the guided reasoning worksheet that stores answers and candidate mode interpretations before or alongside a durable mode profile.",
706
+ eventType: "An event type is reusable incident taxonomy for trigger reports, such as criticism, conflict, rupture, or overload.",
707
+ emotionDefinition: "An emotion definition is reusable emotion vocabulary for trigger reports. Reports can either reference one or fall back to raw labels.",
708
+ triggerReport: "A trigger report is the one-episode incident chain: situation, emotions, thoughts, behaviors, consequences, extra mode labels, schema themes, and next moves."
709
+ },
710
+ psycheCoachingPlaybooks: AGENT_ONBOARDING_PSYCHE_PLAYBOOKS,
711
+ relationshipModel: [
712
+ "Goals are the top-level strategic layer.",
713
+ "Projects belong to one goal through goalId.",
714
+ "Tasks can belong to a goal, a project, both, or neither.",
715
+ "Task runs represent live work sessions on tasks and are separate from task status.",
716
+ "Psyche values can link to goals, projects, and tasks.",
717
+ "Behavior patterns, behaviors, beliefs, modes, and trigger reports cross-link to describe one reflective model rather than isolated records.",
718
+ "Insights can point at one entity, but they exist to capture interpretation or advice rather than raw work items."
719
+ ],
720
+ entityCatalog: AGENT_ONBOARDING_ENTITY_CATALOG,
721
+ toolInputCatalog: AGENT_ONBOARDING_TOOL_INPUT_CATALOG,
722
+ verificationPaths: {
723
+ context: "/api/v1/context",
724
+ xpMetrics: "/api/v1/metrics/xp",
725
+ weeklyReview: "/api/v1/reviews/weekly",
726
+ settingsBin: "/api/v1/settings/bin",
727
+ batchSearch: "/api/v1/entities/search",
728
+ psycheSchemaCatalog: "/api/v1/psyche/schema-catalog",
729
+ psycheEventTypes: "/api/v1/psyche/event-types",
730
+ psycheEmotions: "/api/v1/psyche/emotions"
731
+ },
732
+ recommendedPluginTools: {
733
+ bootstrap: ["forge_get_operator_overview"],
734
+ readModels: [
735
+ "forge_get_operator_context",
736
+ "forge_get_current_work",
737
+ "forge_get_psyche_overview",
738
+ "forge_get_xp_metrics",
739
+ "forge_get_weekly_review"
740
+ ],
741
+ uiWorkflow: ["forge_get_ui_entrypoint"],
742
+ entityWorkflow: [
743
+ "forge_search_entities",
744
+ "forge_create_entities",
745
+ "forge_update_entities",
746
+ "forge_delete_entities",
747
+ "forge_restore_entities"
748
+ ],
749
+ workWorkflow: [
750
+ "forge_log_work",
751
+ "forge_start_task_run",
752
+ "forge_heartbeat_task_run",
753
+ "forge_focus_task_run",
754
+ "forge_complete_task_run",
755
+ "forge_release_task_run"
756
+ ],
757
+ insightWorkflow: ["forge_post_insight"]
758
+ },
759
+ interactionGuidance: {
760
+ conversationMode: "continue_main_discussion_first",
761
+ saveSuggestionPlacement: "end_of_message",
762
+ saveSuggestionTone: "gentle_optional",
763
+ maxQuestionsPerTurn: 3,
764
+ duplicateCheckRoute: "/api/v1/entities/search",
765
+ uiSuggestionRule: "offer_visual_ui_when_review_or_editing_would_be_easier",
766
+ browserFallbackRule: "Do not open the Forge UI or a browser just to create or update normal entities when the batch entity tools can do the job.",
767
+ writeConsentRule: "If an entity is only implied, keep helping in the main conversation and offer Forge lightly at the end. Only write after explicit save intent or after the user accepts the Forge save offer."
768
+ },
769
+ mutationGuidance: {
770
+ preferredBatchRoutes: {
771
+ create: "/api/v1/entities/create",
772
+ update: "/api/v1/entities/update",
773
+ delete: "/api/v1/entities/delete",
774
+ restore: "/api/v1/entities/restore",
775
+ search: "/api/v1/entities/search"
776
+ },
777
+ deleteDefault: "soft",
778
+ hardDeleteRequiresExplicitMode: true,
779
+ restoreSummary: "Restore soft-deleted entities through the restore route or the settings bin.",
780
+ entityDeleteSummary: "Entity DELETE routes default to soft delete. Pass mode=hard only when permanent removal is intended.",
781
+ 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.",
782
+ searchRule: "forge_search_entities accepts searches as an array. Search before create or update when duplicate risk exists.",
783
+ createRule: "Each create operation must include entityType and full data. entityType alone is not enough.",
784
+ updateRule: "Each update operation must include entityType, id, and patch.",
785
+ createExample: '{"operations":[{"entityType":"goal","data":{"title":"Create meaningfully"},"clientRef":"goal-create-1"},{"entityType":"goal","data":{"title":"Build a beautiful family"},"clientRef":"goal-create-2"}]}',
786
+ updateExample: '{"operations":[{"entityType":"task","id":"task_123","patch":{"status":"focus","priority":"high"},"clientRef":"task-update-1"}]}'
787
+ }
788
+ };
789
+ }
790
+ function rewriteMountPath(url) {
791
+ const queryIndex = url.indexOf("?");
792
+ const pathname = queryIndex >= 0 ? url.slice(0, queryIndex) : url;
793
+ const search = queryIndex >= 0 ? url.slice(queryIndex) : "";
794
+ if (pathname === "/forge") {
795
+ return `/${search}`;
796
+ }
797
+ if (pathname.startsWith("/forge/")) {
798
+ return `${pathname.slice("/forge".length) || "/"}${search}`;
799
+ }
800
+ return url;
801
+ }
802
+ function getErrorMessage(error) {
803
+ if (error instanceof Error) {
804
+ return error.message;
805
+ }
806
+ return String(error);
807
+ }
808
+ function formatValidationIssues(error) {
809
+ return error.issues.map((issue) => ({
810
+ path: issue.path.length > 0 ? issue.path.map(String).join(".") : "body",
811
+ message: issue.message
812
+ }));
813
+ }
814
+ function parseIdempotencyKey(headers) {
815
+ const raw = headers["idempotency-key"];
816
+ if (raw === undefined) {
817
+ return null;
818
+ }
819
+ if (Array.isArray(raw)) {
820
+ throw new Error("Idempotency-Key must be a single header value");
821
+ }
822
+ if (typeof raw !== "string") {
823
+ throw new Error("Idempotency-Key must be a string");
824
+ }
825
+ const key = raw.trim();
826
+ if (!key || key.length > 128) {
827
+ throw new Error("Idempotency-Key must be between 1 and 128 characters");
828
+ }
829
+ return key;
830
+ }
831
+ function parseOptionalActorHeader(headers) {
832
+ const raw = headers["x-forge-actor"];
833
+ if (raw === undefined) {
834
+ return null;
835
+ }
836
+ if (Array.isArray(raw)) {
837
+ throw new Error("X-Forge-Actor must be a single header value");
838
+ }
839
+ if (typeof raw !== "string") {
840
+ throw new Error("X-Forge-Actor must be a string");
841
+ }
842
+ const actor = raw.trim();
843
+ return actor.length > 0 ? actor : null;
844
+ }
845
+ function parseActivityContext(headers) {
846
+ const rawSource = headers["x-forge-source"];
847
+ if (Array.isArray(rawSource)) {
848
+ throw new Error("X-Forge-Source must be a single header value");
849
+ }
850
+ const source = rawSource === undefined ? "ui" : activitySourceSchema.parse(typeof rawSource === "string" ? rawSource.trim() : rawSource);
851
+ return {
852
+ source,
853
+ actor: parseOptionalActorHeader(headers)
854
+ };
855
+ }
856
+ function parseBearerToken(headers) {
857
+ const raw = headers.authorization;
858
+ if (raw === undefined) {
859
+ return null;
860
+ }
861
+ if (Array.isArray(raw)) {
862
+ throw new Error("Authorization must be a single header value");
863
+ }
864
+ if (typeof raw !== "string") {
865
+ throw new Error("Authorization must be a string");
866
+ }
867
+ const [scheme, token] = raw.trim().split(/\s+/, 2);
868
+ if (scheme?.toLowerCase() !== "bearer" || !token) {
869
+ throw new Error("Authorization must use Bearer token format");
870
+ }
871
+ return token;
872
+ }
873
+ function parseRequestAuth(headers) {
874
+ const bearer = parseBearerToken(headers);
875
+ const token = bearer ? verifyAgentToken(bearer) : null;
876
+ const activity = parseActivityContext(headers);
877
+ const actor = token?.agentLabel ?? activity.actor ?? null;
878
+ const source = token ? "agent" : activity.source;
879
+ return {
880
+ token,
881
+ actor,
882
+ source,
883
+ activity: {
884
+ actor,
885
+ source
886
+ }
887
+ };
888
+ }
889
+ function hasTokenScope(token, scope) {
890
+ return Boolean(token?.scopes.includes(scope));
891
+ }
892
+ function isPsycheEntityType(entityType) {
893
+ return Boolean(entityType && PSYCHE_ENTITY_TYPES.includes(entityType));
894
+ }
895
+ function getWatchdogHealth(taskRunWatchdog) {
896
+ if (!taskRunWatchdog) {
897
+ return {
898
+ enabled: false,
899
+ healthy: true,
900
+ state: "disabled",
901
+ reason: null,
902
+ status: null
903
+ };
904
+ }
905
+ const status = taskRunWatchdog.getStatus();
906
+ if (status.consecutiveFailures > 0) {
907
+ return {
908
+ enabled: true,
909
+ healthy: false,
910
+ state: "degraded",
911
+ reason: status.lastError ?? "Task-run watchdog recovery failed",
912
+ status
913
+ };
914
+ }
915
+ return {
916
+ enabled: true,
917
+ healthy: true,
918
+ state: status.running ? "healthy" : "idle",
919
+ reason: null,
920
+ status
921
+ };
922
+ }
923
+ function buildHealthPayload(taskRunWatchdog, extras = {}) {
924
+ const watchdog = getWatchdogHealth(taskRunWatchdog);
925
+ return {
926
+ ok: watchdog.healthy,
927
+ app: "forge",
928
+ now: new Date().toISOString(),
929
+ watchdog,
930
+ ...extras
931
+ };
932
+ }
933
+ function buildV1Context() {
934
+ return {
935
+ meta: {
936
+ apiVersion: "v1",
937
+ transport: "rest+sse",
938
+ generatedAt: new Date().toISOString(),
939
+ backend: "forge-node-runtime",
940
+ mode: "transitional-node"
941
+ },
942
+ metrics: buildGamificationProfile(listGoals(), listTasks()),
943
+ dashboard: getDashboard(),
944
+ overview: getOverviewContext(),
945
+ today: getTodayContext(),
946
+ risk: getRiskContext(),
947
+ goals: listGoals(),
948
+ projects: listProjectSummaries(),
949
+ tags: listTags(),
950
+ tasks: listTasks(),
951
+ activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
952
+ activity: listActivityEvents({ limit: 25 })
953
+ };
954
+ }
955
+ function buildXpMetricsPayload() {
956
+ const goals = listGoals();
957
+ const tasks = listTasks();
958
+ const rules = listRewardRules();
959
+ const gamificationOverview = buildGamificationOverview(goals, tasks);
960
+ const dailyAmbientCap = rules
961
+ .filter((rule) => rule.family === "ambient")
962
+ .reduce((max, rule) => Math.max(max, Number(rule.config.dailyCap ?? 0)), 0) || 12;
963
+ return {
964
+ profile: gamificationOverview.profile,
965
+ achievements: gamificationOverview.achievements,
966
+ milestoneRewards: gamificationOverview.milestoneRewards,
967
+ momentumPulse: buildXpMomentumPulse(goals, tasks),
968
+ recentLedger: listRewardLedger({ limit: 25 }),
969
+ rules,
970
+ dailyAmbientXp: getDailyAmbientXp(new Date().toISOString().slice(0, 10)),
971
+ dailyAmbientCap
972
+ };
973
+ }
974
+ function buildOperatorContext() {
975
+ const tasks = listTasks();
976
+ const activeProjects = listProjectSummaries({ status: "active" }).filter((project) => project.activeTaskCount > 0 || project.completedTaskCount > 0);
977
+ const focusTasks = tasks.filter((task) => task.status === "focus" || task.status === "in_progress");
978
+ const recommendedNextTask = focusTasks[0] ??
979
+ tasks.find((task) => task.status === "backlog") ??
980
+ tasks.find((task) => task.status === "blocked") ??
981
+ null;
982
+ return {
983
+ generatedAt: new Date().toISOString(),
984
+ activeProjects: activeProjects.slice(0, 8),
985
+ focusTasks: focusTasks.slice(0, 12),
986
+ currentBoard: {
987
+ backlog: tasks.filter((task) => task.status === "backlog").slice(0, 20),
988
+ focus: tasks.filter((task) => task.status === "focus").slice(0, 20),
989
+ inProgress: tasks.filter((task) => task.status === "in_progress").slice(0, 20),
990
+ blocked: tasks.filter((task) => task.status === "blocked").slice(0, 20),
991
+ done: tasks.filter((task) => task.status === "done").slice(0, 20)
992
+ },
993
+ recentActivity: listActivityEvents({ limit: 20 }),
994
+ recentTaskRuns: listTaskRuns({ limit: 12 }),
995
+ recommendedNextTask,
996
+ xp: buildXpMetricsPayload()
997
+ };
998
+ }
999
+ function buildOperatorOverviewRouteGuide() {
1000
+ return {
1001
+ preferredStart: "/api/v1/operator/overview",
1002
+ mainRoutes: [
1003
+ {
1004
+ id: "context",
1005
+ path: "/api/v1/context",
1006
+ summary: "Full Forge shell snapshot with goals, projects, tasks, activity, and derived overview blocks.",
1007
+ requiredScope: null
1008
+ },
1009
+ {
1010
+ id: "operator_context",
1011
+ path: "/api/v1/operator/context",
1012
+ summary: "Operational task board, focus queue, recent activity, and XP state for assistant workflows.",
1013
+ requiredScope: null
1014
+ },
1015
+ {
1016
+ id: "psyche_overview",
1017
+ path: "/api/v1/psyche/overview",
1018
+ summary: "Aggregate Psyche state across values, patterns, behaviors, beliefs, modes, and trigger reports.",
1019
+ requiredScope: "psyche.read"
1020
+ },
1021
+ {
1022
+ id: "xp_metrics",
1023
+ path: "/api/v1/metrics/xp",
1024
+ summary: "Reward profile, rule set, and recent reward-ledger events.",
1025
+ requiredScope: null
1026
+ },
1027
+ {
1028
+ id: "weekly_review",
1029
+ path: "/api/v1/reviews/weekly",
1030
+ summary: "Weekly reflection read model with wins, chart, and reward framing.",
1031
+ requiredScope: null
1032
+ },
1033
+ {
1034
+ id: "events",
1035
+ path: "/api/v1/events",
1036
+ summary: "Canonical event-log inspection for audit and provenance tracing.",
1037
+ requiredScope: null
1038
+ },
1039
+ {
1040
+ id: "agent_onboarding",
1041
+ path: "/api/v1/agents/onboarding",
1042
+ summary: "Live onboarding contract describing headers, scopes, verification probes, and plugin defaults.",
1043
+ requiredScope: null
1044
+ },
1045
+ {
1046
+ id: "settings_bin",
1047
+ path: "/api/v1/settings/bin",
1048
+ summary: "Deleted-items bin grouped by entity type with restore and permanent-delete actions.",
1049
+ requiredScope: "write"
1050
+ },
1051
+ {
1052
+ id: "entity_batch_search",
1053
+ path: "/api/v1/entities/search",
1054
+ summary: "Batch search route for mixed-entity lookup, linked-entity matching, and optional deleted-item visibility.",
1055
+ requiredScope: "write"
1056
+ },
1057
+ {
1058
+ id: "entity_batch_mutation",
1059
+ path: "/api/v1/entities/{create|update|delete|restore}",
1060
+ summary: "Preferred multi-entity mutation surface for agents. Delete defaults to soft delete and restore reverses soft deletion.",
1061
+ requiredScope: "write"
1062
+ },
1063
+ {
1064
+ id: "operator_log_work",
1065
+ path: "/api/v1/operator/log-work",
1066
+ summary: "Retroactively log real work and receive updated XP without pretending a live task run happened.",
1067
+ requiredScope: "write"
1068
+ },
1069
+ {
1070
+ id: "task_runs",
1071
+ path: "/api/v1/tasks/:id/runs + /api/v1/task-runs/*",
1072
+ summary: "Canonical live-work surface for starting, refreshing, focusing, completing, and releasing active task runs.",
1073
+ requiredScope: "write"
1074
+ }
1075
+ ]
1076
+ };
1077
+ }
1078
+ function buildOperatorOverview(request) {
1079
+ const auth = parseRequestAuth(request.headers);
1080
+ const canReadPsyche = auth.token ? hasTokenScope(auth.token, "psyche.read") : true;
1081
+ const warnings = canReadPsyche ? [] : ["Psyche summary omitted because the active token does not include psyche.read."];
1082
+ return {
1083
+ generatedAt: new Date().toISOString(),
1084
+ snapshot: buildV1Context(),
1085
+ operator: buildOperatorContext(),
1086
+ domains: listDomains(),
1087
+ psyche: canReadPsyche ? getPsycheOverview() : null,
1088
+ onboarding: buildAgentOnboardingPayload(request),
1089
+ capabilities: {
1090
+ tokenPresent: Boolean(auth.token),
1091
+ scopes: auth.token?.scopes ?? [],
1092
+ canReadPsyche,
1093
+ canWritePsyche: auth.token ? hasTokenScope(auth.token, "psyche.write") : true,
1094
+ canManageModes: auth.token ? hasTokenScope(auth.token, "psyche.mode") : true,
1095
+ canManageRewards: auth.token ? hasTokenScope(auth.token, "rewards.manage") : true
1096
+ },
1097
+ warnings,
1098
+ routeGuide: buildOperatorOverviewRouteGuide()
1099
+ };
1100
+ }
1101
+ export async function buildServer(options = {}) {
1102
+ const managers = createManagerRuntime({ dataRoot: options.dataRoot });
1103
+ const runtimeConfig = managers.configuration.readRuntimeConfig({ dataRoot: options.dataRoot });
1104
+ configureDatabase({ dataRoot: runtimeConfig.dataRoot ?? undefined });
1105
+ configureDatabaseSeeding(options.seedDemoData ?? false);
1106
+ await managers.migration.initialize();
1107
+ const app = Fastify({
1108
+ logger: false,
1109
+ rewriteUrl: (request) => rewriteMountPath(request.url ?? "/")
1110
+ });
1111
+ const taskRunWatchdog = options.taskRunWatchdog === false ? null : createTaskRunWatchdog(options.taskRunWatchdog);
1112
+ await app.register(cors, {
1113
+ origin: (origin, callback) => {
1114
+ if (!origin) {
1115
+ callback(null, true);
1116
+ return;
1117
+ }
1118
+ callback(null, runtimeConfig.allowedOrigins.some((pattern) => pattern.test(origin)));
1119
+ },
1120
+ credentials: true
1121
+ });
1122
+ app.addHook("onClose", async () => {
1123
+ taskRunWatchdog?.stop();
1124
+ });
1125
+ app.setErrorHandler((error, _request, reply) => {
1126
+ const validationIssues = error instanceof ZodError ? formatValidationIssues(error) : undefined;
1127
+ const statusCode = isHttpError(error)
1128
+ ? error.statusCode
1129
+ : isManagerError(error)
1130
+ ? error.statusCode
1131
+ : error instanceof ZodError
1132
+ ? 400
1133
+ : 500;
1134
+ reply.code(statusCode).send({
1135
+ code: isHttpError(error)
1136
+ ? error.code
1137
+ : isManagerError(error)
1138
+ ? error.code
1139
+ : statusCode === 400
1140
+ ? "invalid_request"
1141
+ : "internal_error",
1142
+ error: validationIssues ? "Request validation failed" : getErrorMessage(error),
1143
+ statusCode,
1144
+ ...(validationIssues ? { details: validationIssues } : {}),
1145
+ ...(isHttpError(error) && error.details ? error.details : {}),
1146
+ ...(isManagerError(error) && error.details ? error.details : {})
1147
+ });
1148
+ });
1149
+ const authenticateRequest = (headers) => managers.authentication.authenticate(headers);
1150
+ const toActivityContext = (context) => ({
1151
+ actor: context.actor,
1152
+ source: context.source
1153
+ });
1154
+ const requireOperatorSession = (headers, detail) => {
1155
+ const context = authenticateRequest(headers);
1156
+ managers.authorization.requireAuthenticatedOperator(context, detail);
1157
+ return context;
1158
+ };
1159
+ const requireAuthenticatedActor = (headers, detail) => {
1160
+ const context = authenticateRequest(headers);
1161
+ managers.authorization.requireAuthenticatedActor(context, detail);
1162
+ return context;
1163
+ };
1164
+ const requireScopedAccess = (headers, scopes, detail) => {
1165
+ const context = authenticateRequest(headers);
1166
+ managers.authorization.requireAnyTokenScope(context, scopes, detail);
1167
+ return context;
1168
+ };
1169
+ const requirePsycheScopedAccess = (headers, scopes, detail) => {
1170
+ const context = authenticateRequest(headers);
1171
+ if (isPsycheAuthRequired()) {
1172
+ managers.authorization.requireAnyTokenScope(context, scopes, detail);
1173
+ }
1174
+ return context;
1175
+ };
1176
+ const requireCommentAccess = (headers, entityType, detail) => {
1177
+ const context = authenticateRequest(headers);
1178
+ if (isPsycheEntityType(entityType)) {
1179
+ if (isPsycheAuthRequired()) {
1180
+ managers.authorization.requireAuthenticatedActor(context, detail);
1181
+ managers.authorization.requireTokenScope(context, "psyche.comment", {
1182
+ entityType,
1183
+ ...(detail ?? {})
1184
+ });
1185
+ }
1186
+ return context;
1187
+ }
1188
+ managers.authorization.requireAuthenticatedActor(context, detail);
1189
+ managers.authorization.requireAnyTokenScope(context, ["write", "insights"], detail);
1190
+ return context;
1191
+ };
1192
+ const requireInsightAccess = (headers, entityType, detail) => {
1193
+ const context = authenticateRequest(headers);
1194
+ if (isPsycheEntityType(entityType)) {
1195
+ if (isPsycheAuthRequired()) {
1196
+ managers.authorization.requireAuthenticatedActor(context, detail);
1197
+ managers.authorization.requireTokenScope(context, "psyche.insight", {
1198
+ entityType,
1199
+ ...(detail ?? {})
1200
+ });
1201
+ }
1202
+ return context;
1203
+ }
1204
+ managers.authorization.requireAuthenticatedActor(context, detail);
1205
+ managers.authorization.requireAnyTokenScope(context, ["write", "insights"], detail);
1206
+ return context;
1207
+ };
1208
+ app.get("/api/health", async () => buildHealthPayload(taskRunWatchdog));
1209
+ app.get("/api/v1/health", async () => buildHealthPayload(taskRunWatchdog, {
1210
+ apiVersion: "v1",
1211
+ backend: "forge-node-runtime"
1212
+ }));
1213
+ app.get("/api/v1/auth/operator-session", async (request, reply) => ({
1214
+ session: managers.session.ensureLocalOperatorSession(request.headers, reply)
1215
+ }));
1216
+ app.delete("/api/v1/auth/operator-session", async (request, reply) => ({
1217
+ revoked: managers.session.revokeCurrentSession(request.headers, reply)
1218
+ }));
1219
+ app.get("/api/v1/openapi.json", async () => buildOpenApiDocument());
1220
+ app.get("/api/v1/context", async () => buildV1Context());
1221
+ app.get("/api/v1/operator/context", async (request) => {
1222
+ requireOperatorSession(request.headers, { route: "/api/v1/operator/context" });
1223
+ return {
1224
+ context: buildOperatorContext()
1225
+ };
1226
+ });
1227
+ app.get("/api/v1/operator/overview", async (request) => {
1228
+ requireOperatorSession(request.headers, { route: "/api/v1/operator/overview" });
1229
+ return {
1230
+ overview: buildOperatorOverview(request)
1231
+ };
1232
+ });
1233
+ app.get("/api/v1/domains", async () => ({
1234
+ domains: listDomains()
1235
+ }));
1236
+ app.get("/api/v1/psyche/overview", async (request) => {
1237
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/overview" });
1238
+ return { overview: getPsycheOverview() };
1239
+ });
1240
+ app.get("/api/v1/psyche/values", async (request) => {
1241
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/values" });
1242
+ return { values: listPsycheValues() };
1243
+ });
1244
+ app.post("/api/v1/psyche/values", async (request, reply) => {
1245
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/values" });
1246
+ const value = createPsycheValue(createPsycheValueSchema.parse(request.body ?? {}), toActivityContext(auth));
1247
+ reply.code(201);
1248
+ return { value };
1249
+ });
1250
+ app.get("/api/v1/psyche/values/:id", async (request, reply) => {
1251
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/values/:id" });
1252
+ const { id } = request.params;
1253
+ const value = getPsycheValueById(id);
1254
+ if (!value) {
1255
+ reply.code(404);
1256
+ return { error: "Psyche value not found" };
1257
+ }
1258
+ return { value };
1259
+ });
1260
+ app.patch("/api/v1/psyche/values/:id", async (request, reply) => {
1261
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/values/:id" });
1262
+ const { id } = request.params;
1263
+ const value = updatePsycheValue(id, updatePsycheValueSchema.parse(request.body ?? {}), toActivityContext(auth));
1264
+ if (!value) {
1265
+ reply.code(404);
1266
+ return { error: "Psyche value not found" };
1267
+ }
1268
+ return { value };
1269
+ });
1270
+ app.delete("/api/v1/psyche/values/:id", async (request, reply) => {
1271
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/values/:id" });
1272
+ const { id } = request.params;
1273
+ const value = deleteEntity("psyche_value", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
1274
+ if (!value) {
1275
+ reply.code(404);
1276
+ return { error: "Psyche value not found" };
1277
+ }
1278
+ return { value };
1279
+ });
1280
+ app.get("/api/v1/psyche/patterns", async (request) => {
1281
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/patterns" });
1282
+ return { patterns: listBehaviorPatterns() };
1283
+ });
1284
+ app.post("/api/v1/psyche/patterns", async (request, reply) => {
1285
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/patterns" });
1286
+ const pattern = createBehaviorPattern(createBehaviorPatternSchema.parse(request.body ?? {}), toActivityContext(auth));
1287
+ reply.code(201);
1288
+ return { pattern };
1289
+ });
1290
+ app.get("/api/v1/psyche/patterns/:id", async (request, reply) => {
1291
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/patterns/:id" });
1292
+ const { id } = request.params;
1293
+ const pattern = getBehaviorPatternById(id);
1294
+ if (!pattern) {
1295
+ reply.code(404);
1296
+ return { error: "Behavior pattern not found" };
1297
+ }
1298
+ return { pattern };
1299
+ });
1300
+ app.patch("/api/v1/psyche/patterns/:id", async (request, reply) => {
1301
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/patterns/:id" });
1302
+ const { id } = request.params;
1303
+ const pattern = updateBehaviorPattern(id, updateBehaviorPatternSchema.parse(request.body ?? {}), toActivityContext(auth));
1304
+ if (!pattern) {
1305
+ reply.code(404);
1306
+ return { error: "Behavior pattern not found" };
1307
+ }
1308
+ return { pattern };
1309
+ });
1310
+ app.delete("/api/v1/psyche/patterns/:id", async (request, reply) => {
1311
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/patterns/:id" });
1312
+ const { id } = request.params;
1313
+ const pattern = deleteEntity("behavior_pattern", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
1314
+ if (!pattern) {
1315
+ reply.code(404);
1316
+ return { error: "Behavior pattern not found" };
1317
+ }
1318
+ return { pattern };
1319
+ });
1320
+ app.get("/api/v1/psyche/behaviors", async (request) => {
1321
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/behaviors" });
1322
+ return { behaviors: listBehaviors() };
1323
+ });
1324
+ app.post("/api/v1/psyche/behaviors", async (request, reply) => {
1325
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/behaviors" });
1326
+ const behavior = createBehavior(createBehaviorSchema.parse(request.body ?? {}), toActivityContext(auth));
1327
+ reply.code(201);
1328
+ return { behavior };
1329
+ });
1330
+ app.get("/api/v1/psyche/behaviors/:id", async (request, reply) => {
1331
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/behaviors/:id" });
1332
+ const { id } = request.params;
1333
+ const behavior = getBehaviorById(id);
1334
+ if (!behavior) {
1335
+ reply.code(404);
1336
+ return { error: "Behavior not found" };
1337
+ }
1338
+ return { behavior };
1339
+ });
1340
+ app.patch("/api/v1/psyche/behaviors/:id", async (request, reply) => {
1341
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/behaviors/:id" });
1342
+ const { id } = request.params;
1343
+ const behavior = updateBehavior(id, updateBehaviorSchema.parse(request.body ?? {}), toActivityContext(auth));
1344
+ if (!behavior) {
1345
+ reply.code(404);
1346
+ return { error: "Behavior not found" };
1347
+ }
1348
+ return { behavior };
1349
+ });
1350
+ app.delete("/api/v1/psyche/behaviors/:id", async (request, reply) => {
1351
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/behaviors/:id" });
1352
+ const { id } = request.params;
1353
+ const behavior = deleteEntity("behavior", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
1354
+ if (!behavior) {
1355
+ reply.code(404);
1356
+ return { error: "Behavior not found" };
1357
+ }
1358
+ return { behavior };
1359
+ });
1360
+ app.get("/api/v1/psyche/schema-catalog", async (request) => {
1361
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/schema-catalog" });
1362
+ return { schemas: listSchemaCatalog() };
1363
+ });
1364
+ app.get("/api/v1/psyche/beliefs", async (request) => {
1365
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/beliefs" });
1366
+ return { beliefs: listBeliefEntries() };
1367
+ });
1368
+ app.post("/api/v1/psyche/beliefs", async (request, reply) => {
1369
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/beliefs" });
1370
+ const belief = createBeliefEntry(createBeliefEntrySchema.parse(request.body ?? {}), toActivityContext(auth));
1371
+ reply.code(201);
1372
+ return { belief };
1373
+ });
1374
+ app.get("/api/v1/psyche/beliefs/:id", async (request, reply) => {
1375
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/beliefs/:id" });
1376
+ const { id } = request.params;
1377
+ const belief = getBeliefEntryById(id);
1378
+ if (!belief) {
1379
+ reply.code(404);
1380
+ return { error: "Belief not found" };
1381
+ }
1382
+ return { belief };
1383
+ });
1384
+ app.patch("/api/v1/psyche/beliefs/:id", async (request, reply) => {
1385
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/beliefs/:id" });
1386
+ const { id } = request.params;
1387
+ const belief = updateBeliefEntry(id, updateBeliefEntrySchema.parse(request.body ?? {}), toActivityContext(auth));
1388
+ if (!belief) {
1389
+ reply.code(404);
1390
+ return { error: "Belief not found" };
1391
+ }
1392
+ return { belief };
1393
+ });
1394
+ app.delete("/api/v1/psyche/beliefs/:id", async (request, reply) => {
1395
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/beliefs/:id" });
1396
+ const { id } = request.params;
1397
+ const belief = deleteEntity("belief_entry", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
1398
+ if (!belief) {
1399
+ reply.code(404);
1400
+ return { error: "Belief not found" };
1401
+ }
1402
+ return { belief };
1403
+ });
1404
+ app.get("/api/v1/psyche/modes", async (request) => {
1405
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/modes" });
1406
+ return { modes: listModeProfiles() };
1407
+ });
1408
+ app.post("/api/v1/psyche/modes", async (request, reply) => {
1409
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.mode"], { route: "/api/v1/psyche/modes" });
1410
+ const mode = createModeProfile(createModeProfileSchema.parse(request.body ?? {}), toActivityContext(auth));
1411
+ reply.code(201);
1412
+ return { mode };
1413
+ });
1414
+ app.get("/api/v1/psyche/modes/:id", async (request, reply) => {
1415
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/modes/:id" });
1416
+ const { id } = request.params;
1417
+ const mode = getModeProfileById(id);
1418
+ if (!mode) {
1419
+ reply.code(404);
1420
+ return { error: "Mode not found" };
1421
+ }
1422
+ return { mode };
1423
+ });
1424
+ app.patch("/api/v1/psyche/modes/:id", async (request, reply) => {
1425
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.mode"], { route: "/api/v1/psyche/modes/:id" });
1426
+ const { id } = request.params;
1427
+ const mode = updateModeProfile(id, updateModeProfileSchema.parse(request.body ?? {}), toActivityContext(auth));
1428
+ if (!mode) {
1429
+ reply.code(404);
1430
+ return { error: "Mode not found" };
1431
+ }
1432
+ return { mode };
1433
+ });
1434
+ app.delete("/api/v1/psyche/modes/:id", async (request, reply) => {
1435
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.mode"], { route: "/api/v1/psyche/modes/:id" });
1436
+ const { id } = request.params;
1437
+ const mode = deleteEntity("mode_profile", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
1438
+ if (!mode) {
1439
+ reply.code(404);
1440
+ return { error: "Mode not found" };
1441
+ }
1442
+ return { mode };
1443
+ });
1444
+ app.get("/api/v1/psyche/mode-guides", async (request) => {
1445
+ requirePsycheScopedAccess(request.headers, ["psyche.mode"], { route: "/api/v1/psyche/mode-guides" });
1446
+ return { sessions: listModeGuideSessions() };
1447
+ });
1448
+ app.post("/api/v1/psyche/mode-guides", async (request, reply) => {
1449
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.mode"], { route: "/api/v1/psyche/mode-guides" });
1450
+ const session = createModeGuideSession(createModeGuideSessionSchema.parse(request.body ?? {}), toActivityContext(auth));
1451
+ reply.code(201);
1452
+ return { session };
1453
+ });
1454
+ app.get("/api/v1/psyche/mode-guides/:id", async (request, reply) => {
1455
+ requirePsycheScopedAccess(request.headers, ["psyche.mode"], { route: "/api/v1/psyche/mode-guides/:id" });
1456
+ const { id } = request.params;
1457
+ const session = getModeGuideSessionById(id);
1458
+ if (!session) {
1459
+ reply.code(404);
1460
+ return { error: "Mode guide session not found" };
1461
+ }
1462
+ return { session };
1463
+ });
1464
+ app.patch("/api/v1/psyche/mode-guides/:id", async (request, reply) => {
1465
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.mode"], { route: "/api/v1/psyche/mode-guides/:id" });
1466
+ const { id } = request.params;
1467
+ const session = updateModeGuideSession(id, updateModeGuideSessionSchema.parse(request.body ?? {}), toActivityContext(auth));
1468
+ if (!session) {
1469
+ reply.code(404);
1470
+ return { error: "Mode guide session not found" };
1471
+ }
1472
+ return { session };
1473
+ });
1474
+ app.delete("/api/v1/psyche/mode-guides/:id", async (request, reply) => {
1475
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.mode"], { route: "/api/v1/psyche/mode-guides/:id" });
1476
+ const { id } = request.params;
1477
+ const session = deleteEntity("mode_guide_session", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
1478
+ if (!session) {
1479
+ reply.code(404);
1480
+ return { error: "Mode guide session not found" };
1481
+ }
1482
+ return { session };
1483
+ });
1484
+ app.get("/api/v1/psyche/event-types", async (request) => {
1485
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/event-types" });
1486
+ return { eventTypes: listEventTypes() };
1487
+ });
1488
+ app.post("/api/v1/psyche/event-types", async (request, reply) => {
1489
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/event-types" });
1490
+ const eventType = createEventType(createEventTypeSchema.parse(request.body ?? {}), toActivityContext(auth));
1491
+ reply.code(201);
1492
+ return { eventType };
1493
+ });
1494
+ app.get("/api/v1/psyche/event-types/:id", async (request, reply) => {
1495
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/event-types/:id" });
1496
+ const { id } = request.params;
1497
+ const eventType = getEventTypeById(id);
1498
+ if (!eventType) {
1499
+ reply.code(404);
1500
+ return { error: "Event type not found" };
1501
+ }
1502
+ return { eventType };
1503
+ });
1504
+ app.patch("/api/v1/psyche/event-types/:id", async (request, reply) => {
1505
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/event-types/:id" });
1506
+ const { id } = request.params;
1507
+ const eventType = updateEventType(id, updateEventTypeSchema.parse(request.body ?? {}), toActivityContext(auth));
1508
+ if (!eventType) {
1509
+ reply.code(404);
1510
+ return { error: "Event type not found" };
1511
+ }
1512
+ return { eventType };
1513
+ });
1514
+ app.delete("/api/v1/psyche/event-types/:id", async (request, reply) => {
1515
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/event-types/:id" });
1516
+ const { id } = request.params;
1517
+ const eventType = deleteEntity("event_type", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
1518
+ if (!eventType) {
1519
+ reply.code(404);
1520
+ return { error: "Event type not found" };
1521
+ }
1522
+ return { eventType };
1523
+ });
1524
+ app.get("/api/v1/psyche/emotions", async (request) => {
1525
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/emotions" });
1526
+ return { emotions: listEmotionDefinitions() };
1527
+ });
1528
+ app.post("/api/v1/psyche/emotions", async (request, reply) => {
1529
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/emotions" });
1530
+ const emotion = createEmotionDefinition(createEmotionDefinitionSchema.parse(request.body ?? {}), toActivityContext(auth));
1531
+ reply.code(201);
1532
+ return { emotion };
1533
+ });
1534
+ app.get("/api/v1/psyche/emotions/:id", async (request, reply) => {
1535
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/emotions/:id" });
1536
+ const { id } = request.params;
1537
+ const emotion = getEmotionDefinitionById(id);
1538
+ if (!emotion) {
1539
+ reply.code(404);
1540
+ return { error: "Emotion definition not found" };
1541
+ }
1542
+ return { emotion };
1543
+ });
1544
+ app.patch("/api/v1/psyche/emotions/:id", async (request, reply) => {
1545
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/emotions/:id" });
1546
+ const { id } = request.params;
1547
+ const emotion = updateEmotionDefinition(id, updateEmotionDefinitionSchema.parse(request.body ?? {}), toActivityContext(auth));
1548
+ if (!emotion) {
1549
+ reply.code(404);
1550
+ return { error: "Emotion definition not found" };
1551
+ }
1552
+ return { emotion };
1553
+ });
1554
+ app.delete("/api/v1/psyche/emotions/:id", async (request, reply) => {
1555
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/emotions/:id" });
1556
+ const { id } = request.params;
1557
+ const emotion = deleteEntity("emotion_definition", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
1558
+ if (!emotion) {
1559
+ reply.code(404);
1560
+ return { error: "Emotion definition not found" };
1561
+ }
1562
+ return { emotion };
1563
+ });
1564
+ app.get("/api/v1/psyche/reports", async (request) => {
1565
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/reports" });
1566
+ return { reports: listTriggerReports() };
1567
+ });
1568
+ app.post("/api/v1/psyche/reports", async (request, reply) => {
1569
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/reports" });
1570
+ const report = createTriggerReport(createTriggerReportSchema.parse(request.body ?? {}), toActivityContext(auth));
1571
+ reply.code(201);
1572
+ return { report };
1573
+ });
1574
+ app.get("/api/v1/psyche/reports/:id", async (request, reply) => {
1575
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/psyche/reports/:id" });
1576
+ const { id } = request.params;
1577
+ const report = getTriggerReportById(id);
1578
+ if (!report) {
1579
+ reply.code(404);
1580
+ return { error: "Trigger report not found" };
1581
+ }
1582
+ return {
1583
+ report,
1584
+ comments: listComments({ entityType: "trigger_report", entityId: id }),
1585
+ insights: listInsights({ entityType: "trigger_report", entityId: id, limit: 50 })
1586
+ };
1587
+ });
1588
+ app.patch("/api/v1/psyche/reports/:id", async (request, reply) => {
1589
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/reports/:id" });
1590
+ const { id } = request.params;
1591
+ const report = updateTriggerReport(id, updateTriggerReportSchema.parse(request.body ?? {}), toActivityContext(auth));
1592
+ if (!report) {
1593
+ reply.code(404);
1594
+ return { error: "Trigger report not found" };
1595
+ }
1596
+ return { report };
1597
+ });
1598
+ app.delete("/api/v1/psyche/reports/:id", async (request, reply) => {
1599
+ const auth = requirePsycheScopedAccess(request.headers, ["psyche.write"], { route: "/api/v1/psyche/reports/:id" });
1600
+ const { id } = request.params;
1601
+ const report = deleteEntity("trigger_report", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
1602
+ if (!report) {
1603
+ reply.code(404);
1604
+ return { error: "Trigger report not found" };
1605
+ }
1606
+ return { report };
1607
+ });
1608
+ app.get("/api/v1/comments", async (request) => {
1609
+ const query = commentListQuerySchema.parse(request.query ?? {});
1610
+ if (isPsycheEntityType(query.entityType)) {
1611
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], { route: "/api/v1/comments", entityType: query.entityType });
1612
+ }
1613
+ return { comments: listComments(query) };
1614
+ });
1615
+ app.post("/api/v1/comments", async (request, reply) => {
1616
+ const input = createCommentSchema.parse(request.body ?? {});
1617
+ const auth = requireCommentAccess(request.headers, input.entityType, {
1618
+ route: "/api/v1/comments",
1619
+ entityType: input.entityType
1620
+ });
1621
+ const comment = createComment(input, toActivityContext(auth));
1622
+ reply.code(201);
1623
+ return { comment };
1624
+ });
1625
+ app.get("/api/v1/comments/:id", async (request, reply) => {
1626
+ const { id } = request.params;
1627
+ const current = getCommentById(id);
1628
+ const auth = requireCommentAccess(request.headers, current?.entityType, {
1629
+ route: "/api/v1/comments/:id",
1630
+ entityType: current?.entityType ?? null
1631
+ });
1632
+ void auth;
1633
+ if (!current) {
1634
+ reply.code(404);
1635
+ return { error: "Comment not found" };
1636
+ }
1637
+ return { comment: current };
1638
+ });
1639
+ app.patch("/api/v1/comments/:id", async (request, reply) => {
1640
+ const { id } = request.params;
1641
+ const patch = updateCommentSchema.parse(request.body ?? {});
1642
+ const current = getCommentById(id);
1643
+ const auth = requireCommentAccess(request.headers, current?.entityType, {
1644
+ route: "/api/v1/comments/:id",
1645
+ entityType: current?.entityType ?? null
1646
+ });
1647
+ const comment = updateComment(id, patch, toActivityContext(auth));
1648
+ if (!comment) {
1649
+ reply.code(404);
1650
+ return { error: "Comment not found" };
1651
+ }
1652
+ return { comment };
1653
+ });
1654
+ app.delete("/api/v1/comments/:id", async (request, reply) => {
1655
+ const { id } = request.params;
1656
+ const current = getCommentById(id);
1657
+ const auth = requireCommentAccess(request.headers, current?.entityType, {
1658
+ route: "/api/v1/comments/:id",
1659
+ entityType: current?.entityType ?? null
1660
+ });
1661
+ const comment = deleteEntity("comment", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
1662
+ if (!comment) {
1663
+ reply.code(404);
1664
+ return { error: "Comment not found" };
1665
+ }
1666
+ return { comment };
1667
+ });
1668
+ app.get("/api/v1/projects", async (request) => {
1669
+ const query = projectListQuerySchema.parse(request.query ?? {});
1670
+ return { projects: listProjectSummaries(query) };
1671
+ });
1672
+ app.get("/api/v1/campaigns", async (request, reply) => {
1673
+ markDeprecatedAliasRoute(reply, "/api/v1/projects");
1674
+ const query = projectListQuerySchema.parse(request.query ?? {});
1675
+ return { projects: listProjectSummaries(query) };
1676
+ });
1677
+ app.get("/api/v1/goals", async () => ({ goals: listGoals() }));
1678
+ app.get("/api/v1/goals/:id", async (request, reply) => {
1679
+ const { id } = request.params;
1680
+ const goal = getGoalById(id);
1681
+ if (!goal) {
1682
+ reply.code(404);
1683
+ return { error: "Goal not found" };
1684
+ }
1685
+ return { goal };
1686
+ });
1687
+ app.get("/api/v1/tasks", async (request) => {
1688
+ const query = taskListQuerySchema.parse(request.query ?? {});
1689
+ return { tasks: listTasks(query) };
1690
+ });
1691
+ app.get("/api/v1/projects/:id", async (request, reply) => {
1692
+ const { id } = request.params;
1693
+ const project = listProjectSummaries().find((entry) => entry.id === id);
1694
+ if (!project) {
1695
+ reply.code(404);
1696
+ return { error: "Project not found" };
1697
+ }
1698
+ return { project };
1699
+ });
1700
+ app.get("/api/v1/projects/:id/board", async (request, reply) => {
1701
+ const { id } = request.params;
1702
+ const payload = getProjectBoard(id);
1703
+ if (!payload) {
1704
+ reply.code(404);
1705
+ return { error: "Project not found" };
1706
+ }
1707
+ return projectBoardPayloadSchema.parse(payload);
1708
+ });
1709
+ app.get("/api/v1/tags", async () => ({ tags: listTags() }));
1710
+ app.get("/api/v1/tags/:id", async (request, reply) => {
1711
+ const { id } = request.params;
1712
+ const tag = getTagById(id);
1713
+ if (!tag) {
1714
+ reply.code(404);
1715
+ return { error: "Tag not found" };
1716
+ }
1717
+ return { tag };
1718
+ });
1719
+ app.get("/api/v1/activity", async (request) => {
1720
+ const query = activityListQuerySchema.parse(request.query ?? {});
1721
+ return { activity: listActivityEvents(query) };
1722
+ });
1723
+ app.post("/api/v1/activity/:id/remove", async (request, reply) => {
1724
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/activity/:id/remove" });
1725
+ const { id } = request.params;
1726
+ const event = removeActivityEvent(id, removeActivityEventSchema.parse(request.body ?? {}), parseActivityContext(request.headers));
1727
+ if (!event) {
1728
+ reply.code(404);
1729
+ return { error: "Activity event not found" };
1730
+ }
1731
+ return { event };
1732
+ });
1733
+ app.get("/api/v1/metrics", async () => ({
1734
+ metrics: buildGamificationOverview(listGoals(), listTasks())
1735
+ }));
1736
+ app.get("/api/v1/metrics/xp", async () => ({
1737
+ metrics: buildXpMetricsPayload()
1738
+ }));
1739
+ app.get("/api/v1/insights", async () => ({
1740
+ insights: getInsightsPayload()
1741
+ }));
1742
+ app.post("/api/v1/insights", async (request, reply) => {
1743
+ const input = createInsightSchema.parse(request.body ?? {});
1744
+ const auth = requireInsightAccess(request.headers, input.entityType, {
1745
+ route: "/api/v1/insights",
1746
+ entityType: input.entityType
1747
+ });
1748
+ const insight = createInsight(input, { actor: auth.actor, source: auth.source });
1749
+ reply.code(201);
1750
+ return { insight };
1751
+ });
1752
+ app.get("/api/v1/insights/:id", async (request, reply) => {
1753
+ const { id } = request.params;
1754
+ const insight = getInsightById(id);
1755
+ if (!insight) {
1756
+ reply.code(404);
1757
+ return { error: "Insight not found" };
1758
+ }
1759
+ return { insight };
1760
+ });
1761
+ app.patch("/api/v1/insights/:id", async (request, reply) => {
1762
+ const { id } = request.params;
1763
+ const current = getInsightById(id);
1764
+ const auth = requireInsightAccess(request.headers, current?.entityType, {
1765
+ route: "/api/v1/insights/:id",
1766
+ entityType: current?.entityType ?? null
1767
+ });
1768
+ const insight = updateInsight(id, updateInsightSchema.parse(request.body ?? {}), { actor: auth.actor, source: auth.source });
1769
+ if (!insight) {
1770
+ reply.code(404);
1771
+ return { error: "Insight not found" };
1772
+ }
1773
+ return { insight };
1774
+ });
1775
+ app.delete("/api/v1/insights/:id", async (request, reply) => {
1776
+ const { id } = request.params;
1777
+ const current = getInsightById(id);
1778
+ const auth = requireInsightAccess(request.headers, current?.entityType, {
1779
+ route: "/api/v1/insights/:id",
1780
+ entityType: current?.entityType ?? null
1781
+ });
1782
+ const query = entityDeleteQuerySchema.parse(request.query ?? {});
1783
+ const insight = query.mode === "hard"
1784
+ ? deleteInsight(id, { actor: auth.actor, source: auth.source })
1785
+ : deleteEntity("insight", id, query, { actor: auth.actor, source: auth.source });
1786
+ if (!insight) {
1787
+ reply.code(404);
1788
+ return { error: "Insight not found" };
1789
+ }
1790
+ return { insight };
1791
+ });
1792
+ app.post("/api/v1/insights/:id/feedback", async (request, reply) => {
1793
+ const { id } = request.params;
1794
+ const current = getInsightById(id);
1795
+ const auth = requireInsightAccess(request.headers, current?.entityType, {
1796
+ route: "/api/v1/insights/:id/feedback",
1797
+ entityType: current?.entityType ?? null
1798
+ });
1799
+ const feedback = createInsightFeedback(id, createInsightFeedbackSchema.parse(request.body ?? {}), { actor: auth.actor, source: auth.source });
1800
+ if (!feedback) {
1801
+ reply.code(404);
1802
+ return { error: "Insight not found" };
1803
+ }
1804
+ return { feedback };
1805
+ });
1806
+ app.get("/api/v1/approval-requests", async (request) => {
1807
+ requireOperatorSession(request.headers, { route: "/api/v1/approval-requests" });
1808
+ const query = request.query;
1809
+ return { approvalRequests: listApprovalRequests(query?.status) };
1810
+ });
1811
+ app.post("/api/v1/approval-requests/:id/approve", async (request, reply) => {
1812
+ const context = requireOperatorSession(request.headers, { route: "/api/v1/approval-requests/:id/approve" });
1813
+ const { id } = request.params;
1814
+ const body = resolveApprovalRequestSchema.parse(request.body ?? {});
1815
+ const approvalRequest = approveApprovalRequest(id, body.note, body.actor ?? context.actor ?? parseOptionalActorHeader(request.headers));
1816
+ if (!approvalRequest) {
1817
+ reply.code(404);
1818
+ return { error: "Approval request not found" };
1819
+ }
1820
+ return { approvalRequest };
1821
+ });
1822
+ app.post("/api/v1/approval-requests/:id/reject", async (request, reply) => {
1823
+ const context = requireOperatorSession(request.headers, { route: "/api/v1/approval-requests/:id/reject" });
1824
+ const { id } = request.params;
1825
+ const body = resolveApprovalRequestSchema.parse(request.body ?? {});
1826
+ const approvalRequest = rejectApprovalRequest(id, body.note, body.actor ?? context.actor ?? parseOptionalActorHeader(request.headers));
1827
+ if (!approvalRequest) {
1828
+ reply.code(404);
1829
+ return { error: "Approval request not found" };
1830
+ }
1831
+ return { approvalRequest };
1832
+ });
1833
+ app.get("/api/v1/agents", async () => ({
1834
+ agents: listAgentIdentities()
1835
+ }));
1836
+ app.get("/api/v1/agents/onboarding", async (request) => ({
1837
+ onboarding: buildAgentOnboardingPayload(request)
1838
+ }));
1839
+ app.get("/api/v1/agents/:id/actions", async (request) => {
1840
+ const { id } = request.params;
1841
+ return { actions: listAgentActions(id) };
1842
+ });
1843
+ app.post("/api/v1/agent-actions", async (request, reply) => {
1844
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/agent-actions" });
1845
+ const input = createAgentActionSchema.parse(request.body ?? {});
1846
+ const idempotencyKey = parseIdempotencyKey(request.headers);
1847
+ const result = createAgentAction(input, { actor: auth.actor, source: auth.source, token: auth.token ? managers.token.getTokenById(auth.token.id) : null }, idempotencyKey);
1848
+ reply.code(result.approvalRequest ? 202 : 201);
1849
+ return result;
1850
+ });
1851
+ app.get("/api/v1/rewards/rules", async (request) => {
1852
+ requireOperatorSession(request.headers, { route: "/api/v1/rewards/rules" });
1853
+ return { rules: listRewardRules() };
1854
+ });
1855
+ app.get("/api/v1/rewards/rules/:id", async (request, reply) => {
1856
+ requireOperatorSession(request.headers, { route: "/api/v1/rewards/rules/:id" });
1857
+ const { id } = request.params;
1858
+ const rule = getRewardRuleById(id);
1859
+ if (!rule) {
1860
+ reply.code(404);
1861
+ return { error: "Reward rule not found" };
1862
+ }
1863
+ return { rule };
1864
+ });
1865
+ app.patch("/api/v1/rewards/rules/:id", async (request, reply) => {
1866
+ const auth = requireScopedAccess(request.headers, ["rewards.manage", "write"], { route: "/api/v1/rewards/rules/:id" });
1867
+ const { id } = request.params;
1868
+ const rule = updateRewardRule(id, updateRewardRuleSchema.parse(request.body ?? {}), toActivityContext(auth));
1869
+ if (!rule) {
1870
+ reply.code(404);
1871
+ return { error: "Reward rule not found" };
1872
+ }
1873
+ return { rule };
1874
+ });
1875
+ app.get("/api/v1/rewards/ledger", async (request) => {
1876
+ requireOperatorSession(request.headers, { route: "/api/v1/rewards/ledger" });
1877
+ const query = rewardsLedgerQuerySchema.parse(request.query ?? {});
1878
+ return { ledger: listRewardLedger(query) };
1879
+ });
1880
+ app.post("/api/v1/rewards/bonus", async (request, reply) => {
1881
+ const auth = requireScopedAccess(request.headers, ["rewards.manage", "write"], { route: "/api/v1/rewards/bonus" });
1882
+ const reward = createManualRewardGrant(createManualRewardGrantSchema.parse(request.body ?? {}), toActivityContext(auth));
1883
+ reply.code(201);
1884
+ return { reward, metrics: buildXpMetricsPayload() };
1885
+ });
1886
+ app.post("/api/v1/session-events", async (request, reply) => {
1887
+ const auth = requireAuthenticatedActor(request.headers, { route: "/api/v1/session-events" });
1888
+ const payload = createSessionEventSchema.parse(request.body ?? {});
1889
+ const event = recordSessionEvent(payload, { actor: auth.actor, source: auth.source });
1890
+ reply.code(201);
1891
+ return event;
1892
+ });
1893
+ app.get("/api/v1/events", async (request) => {
1894
+ const query = eventsListQuerySchema.parse(request.query ?? {});
1895
+ return { events: listEventLog(query) };
1896
+ });
1897
+ app.get("/api/v1/reviews/weekly", async () => ({
1898
+ review: getWeeklyReviewPayload()
1899
+ }));
1900
+ app.get("/api/v1/settings", async (request) => {
1901
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/settings" });
1902
+ return { settings: getSettings() };
1903
+ });
1904
+ app.get("/api/v1/settings/bin", async (request) => {
1905
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/settings/bin" });
1906
+ return { bin: getSettingsBinPayload() };
1907
+ });
1908
+ app.post("/api/v1/projects", async (request, reply) => {
1909
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/projects" });
1910
+ const project = createProject(createProjectSchema.parse(request.body ?? {}), toActivityContext(auth));
1911
+ reply.code(201);
1912
+ return { project };
1913
+ });
1914
+ app.patch("/api/v1/projects/:id", async (request, reply) => {
1915
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/projects/:id" });
1916
+ const { id } = request.params;
1917
+ const project = updateProject(id, updateProjectSchema.parse(request.body ?? {}), toActivityContext(auth));
1918
+ if (!project) {
1919
+ reply.code(404);
1920
+ return { error: "Project not found" };
1921
+ }
1922
+ return { project };
1923
+ });
1924
+ app.delete("/api/v1/projects/:id", async (request, reply) => {
1925
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/projects/:id" });
1926
+ const { id } = request.params;
1927
+ const project = deleteEntity("project", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
1928
+ if (!project) {
1929
+ reply.code(404);
1930
+ return { error: "Project not found" };
1931
+ }
1932
+ return { project };
1933
+ });
1934
+ app.patch("/api/v1/settings", async (request) => {
1935
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/settings" });
1936
+ return {
1937
+ settings: updateSettings(updateSettingsSchema.parse(request.body ?? {}), toActivityContext(auth))
1938
+ };
1939
+ });
1940
+ app.post("/api/v1/settings/tokens", async (request, reply) => {
1941
+ const auth = requireOperatorSession(request.headers, { route: "/api/v1/settings/tokens" });
1942
+ const token = managers.token.issueLocalAgentToken(createAgentTokenSchema.parse(request.body ?? {}), auth);
1943
+ reply.code(201);
1944
+ return { token };
1945
+ });
1946
+ app.post("/api/v1/settings/tokens/:id/rotate", async (request, reply) => {
1947
+ const auth = requireOperatorSession(request.headers, { route: "/api/v1/settings/tokens/:id/rotate" });
1948
+ const { id } = request.params;
1949
+ const token = managers.token.rotateLocalAgentToken(id, auth);
1950
+ if (!token) {
1951
+ reply.code(404);
1952
+ return { error: "Agent token not found" };
1953
+ }
1954
+ return { token };
1955
+ });
1956
+ app.post("/api/v1/settings/tokens/:id/revoke", async (request, reply) => {
1957
+ const auth = requireOperatorSession(request.headers, { route: "/api/v1/settings/tokens/:id/revoke" });
1958
+ const { id } = request.params;
1959
+ const token = managers.token.revokeLocalAgentToken(id, auth);
1960
+ if (!token) {
1961
+ reply.code(404);
1962
+ return { error: "Agent token not found" };
1963
+ }
1964
+ return { token };
1965
+ });
1966
+ app.get("/api/v1/task-runs", async (request) => {
1967
+ const query = taskRunListQuerySchema.parse(request.query ?? {});
1968
+ return { taskRuns: listTaskRuns(query) };
1969
+ });
1970
+ app.get("/api/v1/events/meta", async () => ({
1971
+ events: buildEventStreamMeta()
1972
+ }));
1973
+ app.get("/api/v1/events/stream", async (request, reply) => {
1974
+ reply.hijack();
1975
+ reply.raw.write(`retry: 3000\n`);
1976
+ reply.raw.writeHead(200, {
1977
+ "Content-Type": "text/event-stream",
1978
+ "Cache-Control": "no-cache, no-transform",
1979
+ Connection: "keep-alive",
1980
+ "X-Accel-Buffering": "no"
1981
+ });
1982
+ let lastActivityId = listActivityEvents({ limit: 1 })[0]?.id ?? null;
1983
+ const emit = (event, payload) => {
1984
+ reply.raw.write(`event: ${event}\n`);
1985
+ reply.raw.write(`data: ${JSON.stringify(payload)}\n\n`);
1986
+ };
1987
+ emit("snapshot", {
1988
+ generatedAt: new Date().toISOString(),
1989
+ latestActivityId: lastActivityId
1990
+ });
1991
+ const heartbeat = setInterval(() => {
1992
+ emit("heartbeat", { now: new Date().toISOString() });
1993
+ }, 15_000);
1994
+ const poll = setInterval(() => {
1995
+ const latest = listActivityEvents({ limit: 1 })[0] ?? null;
1996
+ if (!latest || latest.id === lastActivityId) {
1997
+ return;
1998
+ }
1999
+ lastActivityId = latest.id;
2000
+ emit("activity", latest);
2001
+ }, 3_000);
2002
+ request.raw.on("close", () => {
2003
+ clearInterval(heartbeat);
2004
+ clearInterval(poll);
2005
+ reply.raw.end();
2006
+ });
2007
+ });
2008
+ app.get("/api/dashboard", async () => getDashboard());
2009
+ app.get("/api/context/overview", async (_request, reply) => {
2010
+ markCompatibilityRoute(reply);
2011
+ return getOverviewContext();
2012
+ });
2013
+ app.get("/api/context/today", async (_request, reply) => {
2014
+ markCompatibilityRoute(reply);
2015
+ return getTodayContext();
2016
+ });
2017
+ app.get("/api/context/risk", async (_request, reply) => {
2018
+ markCompatibilityRoute(reply);
2019
+ return getRiskContext();
2020
+ });
2021
+ app.get("/api/goals", async (_request, reply) => {
2022
+ markCompatibilityRoute(reply);
2023
+ return { goals: listGoals() };
2024
+ });
2025
+ app.get("/api/tasks", async (request, reply) => {
2026
+ markCompatibilityRoute(reply);
2027
+ const query = taskListQuerySchema.parse(request.query ?? {});
2028
+ return { tasks: listTasks(query) };
2029
+ });
2030
+ app.get("/api/activity", async (request, reply) => {
2031
+ markCompatibilityRoute(reply);
2032
+ const query = activityListQuerySchema.parse(request.query ?? {});
2033
+ return { activity: listActivityEvents(query) };
2034
+ });
2035
+ app.get("/api/tags", async (_request, reply) => {
2036
+ markCompatibilityRoute(reply);
2037
+ return { tags: listTags() };
2038
+ });
2039
+ app.get("/api/metrics", async (_request, reply) => {
2040
+ markCompatibilityRoute(reply);
2041
+ return {
2042
+ metrics: buildGamificationProfile(listGoals(), listTasks())
2043
+ };
2044
+ });
2045
+ app.get("/api/task-runs", async (request, reply) => {
2046
+ markCompatibilityRoute(reply);
2047
+ const query = taskRunListQuerySchema.parse(request.query ?? {});
2048
+ return { taskRuns: listTaskRuns(query) };
2049
+ });
2050
+ app.get("/api/task-runs/watchdog", async () => ({
2051
+ watchdog: taskRunWatchdog?.getStatus() ?? null
2052
+ }));
2053
+ app.post("/api/task-runs/watchdog/reconcile", async (_request, reply) => {
2054
+ if (!taskRunWatchdog) {
2055
+ reply.code(409);
2056
+ return {
2057
+ code: "task_run_watchdog_disabled",
2058
+ error: "Task-run watchdog is disabled for this server instance",
2059
+ statusCode: 409
2060
+ };
2061
+ }
2062
+ const recovery = await taskRunWatchdog.reconcileNow();
2063
+ return {
2064
+ recovery,
2065
+ watchdog: taskRunWatchdog.getStatus()
2066
+ };
2067
+ });
2068
+ app.get("/api/openclaw/context", async (request, reply) => {
2069
+ markCompatibilityRoute(reply);
2070
+ const query = taskListQuerySchema.parse(request.query ?? {});
2071
+ return {
2072
+ metrics: buildGamificationProfile(listGoals(), listTasks()),
2073
+ dashboard: getDashboard(),
2074
+ overview: getOverviewContext(),
2075
+ today: getTodayContext(),
2076
+ risk: getRiskContext(),
2077
+ goals: listGoals(),
2078
+ projects: listProjectSummaries(),
2079
+ tags: listTags(),
2080
+ tasks: listTasks(query),
2081
+ activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
2082
+ activity: listActivityEvents({ limit: 25 })
2083
+ };
2084
+ });
2085
+ app.get("/api/tasks/:id", async (request, reply) => {
2086
+ markCompatibilityRoute(reply);
2087
+ const { id } = request.params;
2088
+ const task = getTaskById(id);
2089
+ if (!task) {
2090
+ reply.code(404);
2091
+ return { error: "Task not found" };
2092
+ }
2093
+ return { task };
2094
+ });
2095
+ app.get("/api/v1/tasks/:id", async (request, reply) => {
2096
+ const { id } = request.params;
2097
+ const task = getTaskById(id);
2098
+ if (!task) {
2099
+ reply.code(404);
2100
+ return { error: "Task not found" };
2101
+ }
2102
+ return { task };
2103
+ });
2104
+ app.get("/api/tasks/:id/context", async (request, reply) => {
2105
+ markCompatibilityRoute(reply);
2106
+ const { id } = request.params;
2107
+ const task = getTaskById(id);
2108
+ if (!task) {
2109
+ reply.code(404);
2110
+ return { error: "Task not found" };
2111
+ }
2112
+ const taskRuns = listTaskRuns({ taskId: id, limit: 10 });
2113
+ return taskContextPayloadSchema.parse({
2114
+ task,
2115
+ goal: task.goalId ? getGoalById(task.goalId) ?? null : null,
2116
+ project: task.projectId ? listProjectSummaries().find((project) => project.id === task.projectId) ?? null : null,
2117
+ activeTaskRun: taskRuns.find((run) => run.status === "active") ?? null,
2118
+ taskRuns,
2119
+ activity: listActivityEventsForTask(id, 20)
2120
+ });
2121
+ });
2122
+ app.get("/api/v1/tasks/:id/context", async (request, reply) => {
2123
+ const { id } = request.params;
2124
+ const task = getTaskById(id);
2125
+ if (!task) {
2126
+ reply.code(404);
2127
+ return { error: "Task not found" };
2128
+ }
2129
+ const taskRuns = listTaskRuns({ taskId: id, limit: 10 });
2130
+ return taskContextPayloadSchema.parse({
2131
+ task,
2132
+ goal: task.goalId ? getGoalById(task.goalId) ?? null : null,
2133
+ project: task.projectId ? listProjectSummaries().find((project) => project.id === task.projectId) ?? null : null,
2134
+ activeTaskRun: taskRuns.find((run) => run.status === "active") ?? null,
2135
+ taskRuns,
2136
+ activity: listActivityEventsForTask(id, 20)
2137
+ });
2138
+ });
2139
+ app.post("/api/goals", async (request, reply) => {
2140
+ markCompatibilityRoute(reply);
2141
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/goals" });
2142
+ const goal = createGoal(createGoalSchema.parse(request.body ?? {}), toActivityContext(auth));
2143
+ reply.code(201);
2144
+ return { goal };
2145
+ });
2146
+ app.post("/api/v1/goals", async (request, reply) => {
2147
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/goals" });
2148
+ const goal = createGoal(createGoalSchema.parse(request.body ?? {}), toActivityContext(auth));
2149
+ reply.code(201);
2150
+ return { goal };
2151
+ });
2152
+ app.patch("/api/goals/:id", async (request, reply) => {
2153
+ markCompatibilityRoute(reply);
2154
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/goals/:id" });
2155
+ const { id } = request.params;
2156
+ const goal = updateGoal(id, updateGoalSchema.parse(request.body ?? {}), toActivityContext(auth));
2157
+ if (!goal) {
2158
+ reply.code(404);
2159
+ return { error: "Goal not found" };
2160
+ }
2161
+ return { goal };
2162
+ });
2163
+ app.patch("/api/v1/goals/:id", async (request, reply) => {
2164
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/goals/:id" });
2165
+ const { id } = request.params;
2166
+ const goal = updateGoal(id, updateGoalSchema.parse(request.body ?? {}), toActivityContext(auth));
2167
+ if (!goal) {
2168
+ reply.code(404);
2169
+ return { error: "Goal not found" };
2170
+ }
2171
+ return { goal };
2172
+ });
2173
+ app.delete("/api/v1/goals/:id", async (request, reply) => {
2174
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/goals/:id" });
2175
+ const { id } = request.params;
2176
+ const goal = deleteEntity("goal", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
2177
+ if (!goal) {
2178
+ reply.code(404);
2179
+ return { error: "Goal not found" };
2180
+ }
2181
+ return { goal };
2182
+ });
2183
+ app.post("/api/tags", async (request, reply) => {
2184
+ markCompatibilityRoute(reply);
2185
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/tags" });
2186
+ const tag = createTag(createTagSchema.parse(request.body ?? {}), toActivityContext(auth));
2187
+ reply.code(201);
2188
+ return { tag };
2189
+ });
2190
+ app.post("/api/v1/tags", async (request, reply) => {
2191
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/tags" });
2192
+ const tag = createTag(createTagSchema.parse(request.body ?? {}), toActivityContext(auth));
2193
+ reply.code(201);
2194
+ return { tag };
2195
+ });
2196
+ app.patch("/api/v1/tags/:id", async (request, reply) => {
2197
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/tags/:id" });
2198
+ const { id } = request.params;
2199
+ const tag = updateTag(id, updateTagSchema.parse(request.body ?? {}), toActivityContext(auth));
2200
+ if (!tag) {
2201
+ reply.code(404);
2202
+ return { error: "Tag not found" };
2203
+ }
2204
+ return { tag };
2205
+ });
2206
+ app.delete("/api/v1/tags/:id", async (request, reply) => {
2207
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/tags/:id" });
2208
+ const { id } = request.params;
2209
+ const tag = deleteEntity("tag", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
2210
+ if (!tag) {
2211
+ reply.code(404);
2212
+ return { error: "Tag not found" };
2213
+ }
2214
+ return { tag };
2215
+ });
2216
+ app.post("/api/tasks", async (request, reply) => {
2217
+ markCompatibilityRoute(reply);
2218
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/tasks" });
2219
+ const input = createTaskSchema.parse(request.body ?? {});
2220
+ const idempotencyKey = parseIdempotencyKey(request.headers);
2221
+ const activity = toActivityContext(auth);
2222
+ const result = idempotencyKey
2223
+ ? createTaskWithIdempotency(input, idempotencyKey, activity)
2224
+ : { task: createTask(input, activity), replayed: false };
2225
+ if (result.replayed) {
2226
+ reply.code(200).header("Idempotency-Replayed", "true");
2227
+ }
2228
+ else {
2229
+ reply.code(201);
2230
+ }
2231
+ return { task: result.task };
2232
+ });
2233
+ app.post("/api/v1/tasks", async (request, reply) => {
2234
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/tasks" });
2235
+ const input = createTaskSchema.parse(request.body ?? {});
2236
+ const idempotencyKey = parseIdempotencyKey(request.headers);
2237
+ const activity = toActivityContext(auth);
2238
+ const result = idempotencyKey
2239
+ ? createTaskWithIdempotency(input, idempotencyKey, activity)
2240
+ : { task: createTask(input, activity), replayed: false };
2241
+ if (result.replayed) {
2242
+ reply.code(200).header("Idempotency-Replayed", "true");
2243
+ }
2244
+ else {
2245
+ reply.code(201);
2246
+ }
2247
+ return { task: result.task };
2248
+ });
2249
+ app.post("/api/tasks/:id/runs", async (request, reply) => {
2250
+ markCompatibilityRoute(reply);
2251
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/tasks/:id/runs" });
2252
+ const { id } = request.params;
2253
+ const input = taskRunClaimSchema.parse(request.body ?? {});
2254
+ const result = claimTaskRun(id, input, new Date(), toActivityContext(auth));
2255
+ reply.code(result.replayed ? 200 : 201);
2256
+ if (result.replayed) {
2257
+ reply.header("Task-Run-Replayed", "true");
2258
+ }
2259
+ return { taskRun: result.run };
2260
+ });
2261
+ app.post("/api/v1/tasks/:id/runs", async (request, reply) => {
2262
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/tasks/:id/runs" });
2263
+ const { id } = request.params;
2264
+ const input = taskRunClaimSchema.parse(request.body ?? {});
2265
+ const result = claimTaskRun(id, input, new Date(), toActivityContext(auth));
2266
+ reply.code(result.replayed ? 200 : 201);
2267
+ if (result.replayed) {
2268
+ reply.header("Task-Run-Replayed", "true");
2269
+ }
2270
+ return { taskRun: result.run };
2271
+ });
2272
+ app.patch("/api/tasks/:id", async (request, reply) => {
2273
+ markCompatibilityRoute(reply);
2274
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/tasks/:id" });
2275
+ const { id } = request.params;
2276
+ const task = updateTask(id, updateTaskSchema.parse(request.body ?? {}), toActivityContext(auth));
2277
+ if (!task) {
2278
+ reply.code(404);
2279
+ return { error: "Task not found" };
2280
+ }
2281
+ return { task };
2282
+ });
2283
+ app.patch("/api/v1/tasks/:id", async (request, reply) => {
2284
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/tasks/:id" });
2285
+ const { id } = request.params;
2286
+ const task = updateTask(id, updateTaskSchema.parse(request.body ?? {}), toActivityContext(auth));
2287
+ if (!task) {
2288
+ reply.code(404);
2289
+ return { error: "Task not found" };
2290
+ }
2291
+ return { task };
2292
+ });
2293
+ app.delete("/api/v1/tasks/:id", async (request, reply) => {
2294
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/tasks/:id" });
2295
+ const { id } = request.params;
2296
+ const task = deleteEntity("task", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
2297
+ if (!task) {
2298
+ reply.code(404);
2299
+ return { error: "Task not found" };
2300
+ }
2301
+ return { task };
2302
+ });
2303
+ app.post("/api/v1/operator/log-work", async (request, reply) => {
2304
+ const auth = requireScopedAccess(request.headers, ["write", "rewards.manage"], { route: "/api/v1/operator/log-work" });
2305
+ const input = operatorLogWorkSchema.parse(request.body ?? {});
2306
+ if (input.taskId) {
2307
+ const task = updateTask(input.taskId, {
2308
+ title: input.title && input.title.trim().length > 0 ? input.title : undefined,
2309
+ description: typeof input.description === "string"
2310
+ ? input.description
2311
+ : input.summary.trim().length > 0
2312
+ ? input.summary
2313
+ : undefined,
2314
+ goalId: input.goalId,
2315
+ projectId: input.projectId,
2316
+ owner: input.owner,
2317
+ status: input.status ?? "done",
2318
+ priority: input.priority,
2319
+ dueDate: input.dueDate,
2320
+ effort: input.effort,
2321
+ energy: input.energy,
2322
+ points: input.points,
2323
+ tagIds: input.tagIds
2324
+ }, toActivityContext(auth));
2325
+ if (!task) {
2326
+ reply.code(404);
2327
+ return { error: "Task not found" };
2328
+ }
2329
+ return { task, xp: buildXpMetricsPayload() };
2330
+ }
2331
+ const task = createTask(createTaskSchema.parse({
2332
+ title: input.title,
2333
+ description: typeof input.description === "string"
2334
+ ? input.description
2335
+ : input.summary.trim().length > 0
2336
+ ? input.summary
2337
+ : "",
2338
+ goalId: input.goalId ?? null,
2339
+ projectId: input.projectId ?? null,
2340
+ owner: input.owner ?? "Albert",
2341
+ status: input.status ?? "done",
2342
+ priority: input.priority ?? "medium",
2343
+ dueDate: input.dueDate ?? null,
2344
+ effort: input.effort ?? "deep",
2345
+ energy: input.energy ?? "steady",
2346
+ points: input.points ?? 40,
2347
+ tagIds: input.tagIds ?? []
2348
+ }), toActivityContext(auth));
2349
+ reply.code(201);
2350
+ return { task, xp: buildXpMetricsPayload() };
2351
+ });
2352
+ app.post("/api/v1/tasks/:id/uncomplete", async (request, reply) => {
2353
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/tasks/:id/uncomplete" });
2354
+ const { id } = request.params;
2355
+ uncompleteTaskSchema.parse(request.body ?? {});
2356
+ const task = uncompleteTask(id, toActivityContext(auth));
2357
+ if (!task) {
2358
+ reply.code(404);
2359
+ return { error: "Task not found" };
2360
+ }
2361
+ return { task };
2362
+ });
2363
+ app.post("/api/v1/entities/create", async (request) => {
2364
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/entities/create" });
2365
+ return createEntities(batchCreateEntitiesSchema.parse(request.body ?? {}), toActivityContext(auth));
2366
+ });
2367
+ app.post("/api/v1/entities/update", async (request) => {
2368
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/entities/update" });
2369
+ return updateEntities(batchUpdateEntitiesSchema.parse(request.body ?? {}), toActivityContext(auth));
2370
+ });
2371
+ app.post("/api/v1/entities/delete", async (request) => {
2372
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/entities/delete" });
2373
+ return deleteEntities(batchDeleteEntitiesSchema.parse(request.body ?? {}), toActivityContext(auth));
2374
+ });
2375
+ app.post("/api/v1/entities/restore", async (request) => {
2376
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/entities/restore" });
2377
+ return restoreEntities(batchRestoreEntitiesSchema.parse(request.body ?? {}));
2378
+ });
2379
+ app.post("/api/v1/entities/search", async (request) => {
2380
+ requireScopedAccess(request.headers, ["read", "write"], { route: "/api/v1/entities/search" });
2381
+ return searchEntities(batchSearchEntitiesSchema.parse(request.body ?? {}));
2382
+ });
2383
+ app.post("/api/task-runs/recover", async (request, reply) => {
2384
+ markCompatibilityRoute(reply);
2385
+ const payload = taskRunListQuerySchema.pick({ limit: true }).parse(request.body ?? {});
2386
+ return { timedOutRuns: recoverTimedOutTaskRuns({ limit: payload.limit }) };
2387
+ });
2388
+ app.post("/api/task-runs/:id/heartbeat", async (request, reply) => {
2389
+ markCompatibilityRoute(reply);
2390
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/heartbeat" });
2391
+ const { id } = request.params;
2392
+ const input = taskRunHeartbeatSchema.parse(request.body ?? {});
2393
+ return { taskRun: heartbeatTaskRun(id, input, new Date(), toActivityContext(auth)) };
2394
+ });
2395
+ app.post("/api/v1/task-runs/:id/heartbeat", async (request) => {
2396
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/heartbeat" });
2397
+ const { id } = request.params;
2398
+ const input = taskRunHeartbeatSchema.parse(request.body ?? {});
2399
+ return { taskRun: heartbeatTaskRun(id, input, new Date(), toActivityContext(auth)) };
2400
+ });
2401
+ app.post("/api/task-runs/:id/focus", async (request, reply) => {
2402
+ markCompatibilityRoute(reply);
2403
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/focus" });
2404
+ const { id } = request.params;
2405
+ const input = taskRunFocusSchema.parse(request.body ?? {});
2406
+ return { taskRun: focusTaskRun(id, input, new Date(), toActivityContext(auth)) };
2407
+ });
2408
+ app.post("/api/v1/task-runs/:id/focus", async (request) => {
2409
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/focus" });
2410
+ const { id } = request.params;
2411
+ const input = taskRunFocusSchema.parse(request.body ?? {});
2412
+ return { taskRun: focusTaskRun(id, input, new Date(), toActivityContext(auth)) };
2413
+ });
2414
+ app.post("/api/task-runs/:id/complete", async (request, reply) => {
2415
+ markCompatibilityRoute(reply);
2416
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/complete" });
2417
+ const { id } = request.params;
2418
+ const input = taskRunFinishSchema.parse(request.body ?? {});
2419
+ return { taskRun: completeTaskRun(id, input, new Date(), toActivityContext(auth)) };
2420
+ });
2421
+ app.post("/api/v1/task-runs/:id/complete", async (request) => {
2422
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/complete" });
2423
+ const { id } = request.params;
2424
+ const input = taskRunFinishSchema.parse(request.body ?? {});
2425
+ return { taskRun: completeTaskRun(id, input, new Date(), toActivityContext(auth)) };
2426
+ });
2427
+ app.post("/api/task-runs/:id/release", async (request, reply) => {
2428
+ markCompatibilityRoute(reply);
2429
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/task-runs/:id/release" });
2430
+ const { id } = request.params;
2431
+ const input = taskRunFinishSchema.parse(request.body ?? {});
2432
+ return { taskRun: releaseTaskRun(id, input, new Date(), toActivityContext(auth)) };
2433
+ });
2434
+ app.post("/api/v1/task-runs/:id/release", async (request) => {
2435
+ const auth = requireScopedAccess(request.headers, ["write"], { route: "/api/v1/task-runs/:id/release" });
2436
+ const { id } = request.params;
2437
+ const input = taskRunFinishSchema.parse(request.body ?? {});
2438
+ return { taskRun: releaseTaskRun(id, input, new Date(), toActivityContext(auth)) };
2439
+ });
2440
+ app.post("/api/tags/suggestions", async (request, reply) => {
2441
+ markCompatibilityRoute(reply);
2442
+ const payload = tagSuggestionRequestSchema.parse(request.body ?? {});
2443
+ return {
2444
+ suggestions: suggestTags(payload)
2445
+ };
2446
+ });
2447
+ await registerWebRoutes(app);
2448
+ await taskRunWatchdog?.start();
2449
+ return app;
2450
+ }