forge-openclaw-plugin 0.2.7 → 0.2.11

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 (55) hide show
  1. package/README.md +81 -1
  2. package/dist/assets/{board-CzgvdLO8.js → board-C_m78kvK.js} +2 -2
  3. package/dist/assets/{board-CzgvdLO8.js.map → board-C_m78kvK.js.map} +1 -1
  4. package/dist/assets/index-BWtLtXwb.js +36 -0
  5. package/dist/assets/index-BWtLtXwb.js.map +1 -0
  6. package/dist/assets/index-Dp5GXY_z.css +1 -0
  7. package/dist/assets/{motion-STUd1O46.js → motion-CpZvZumD.js} +2 -2
  8. package/dist/assets/{motion-STUd1O46.js.map → motion-CpZvZumD.js.map} +1 -1
  9. package/dist/assets/{table-CtNlETLc.js → table-DtyXTw03.js} +2 -2
  10. package/dist/assets/{table-CtNlETLc.js.map → table-DtyXTw03.js.map} +1 -1
  11. package/dist/assets/{ui-ThzkR_oW.js → ui-BXbpiKyS.js} +2 -2
  12. package/dist/assets/{ui-ThzkR_oW.js.map → ui-BXbpiKyS.js.map} +1 -1
  13. package/dist/assets/{vendor-DyHAI6nk.js → vendor-QBH6qVEe.js} +84 -74
  14. package/dist/assets/vendor-QBH6qVEe.js.map +1 -0
  15. package/dist/assets/{viz-BJuBCz_G.js → viz-w-IMeueL.js} +2 -2
  16. package/dist/assets/{viz-BJuBCz_G.js.map → viz-w-IMeueL.js.map} +1 -1
  17. package/dist/index.html +8 -8
  18. package/dist/openclaw/api-client.d.ts +1 -0
  19. package/dist/openclaw/local-runtime.d.ts +8 -0
  20. package/dist/openclaw/local-runtime.js +123 -1
  21. package/dist/openclaw/plugin-entry-shared.js +12 -0
  22. package/dist/openclaw/routes.js +4 -0
  23. package/dist/server/app.js +104 -67
  24. package/dist/server/demo-data.js +49 -0
  25. package/dist/server/openapi.js +84 -43
  26. package/dist/server/psyche-types.js +1 -30
  27. package/dist/server/repositories/deleted-entities.js +60 -26
  28. package/dist/server/repositories/goals.js +2 -5
  29. package/dist/server/repositories/notes.js +359 -0
  30. package/dist/server/repositories/projects.js +2 -5
  31. package/dist/server/repositories/psyche.js +11 -14
  32. package/dist/server/repositories/task-runs.js +2 -0
  33. package/dist/server/repositories/tasks.js +21 -10
  34. package/dist/server/seed-demo.js +11 -0
  35. package/dist/server/services/dashboard.js +4 -1
  36. package/dist/server/services/entity-crud.js +27 -30
  37. package/dist/server/services/insights.js +5 -5
  38. package/dist/server/services/projects.js +3 -1
  39. package/dist/server/services/psyche.js +4 -4
  40. package/dist/server/types.js +70 -11
  41. package/openclaw.plugin.json +12 -1
  42. package/package.json +1 -1
  43. package/server/migrations/001_core.sql +78 -0
  44. package/server/migrations/002_psyche.sql +164 -13
  45. package/skills/forge-openclaw/SKILL.md +19 -5
  46. package/dist/assets/index-8d_oM8fL.js +0 -27
  47. package/dist/assets/index-8d_oM8fL.js.map +0 -1
  48. package/dist/assets/index-D4A_bq8m.css +0 -1
  49. package/dist/assets/vendor-DyHAI6nk.js.map +0 -1
  50. package/dist/server/repositories/comments.js +0 -176
  51. package/server/migrations/003_timer_execution.sql +0 -18
  52. package/server/migrations/004_psyche_linked_entities.sql +0 -5
  53. package/server/migrations/005_adaptive_schemas.sql +0 -157
  54. package/server/migrations/006_psyche_auth_setting.sql +0 -4
  55. package/server/migrations/007_deleted_entities.sql +0 -16
package/dist/index.html CHANGED
@@ -13,15 +13,15 @@
13
13
  />
14
14
  <link rel="icon" type="image/png" href="/forge/assets/favicon-BCHm9dUV.ico" />
15
15
  <link rel="alternate icon" href="/forge/assets/favicon-BCHm9dUV.ico" />
16
- <script type="module" crossorigin src="/forge/assets/index-8d_oM8fL.js"></script>
17
- <link rel="modulepreload" crossorigin href="/forge/assets/vendor-DyHAI6nk.js">
18
- <link rel="modulepreload" crossorigin href="/forge/assets/motion-STUd1O46.js">
19
- <link rel="modulepreload" crossorigin href="/forge/assets/ui-ThzkR_oW.js">
20
- <link rel="modulepreload" crossorigin href="/forge/assets/table-CtNlETLc.js">
21
- <link rel="modulepreload" crossorigin href="/forge/assets/viz-BJuBCz_G.js">
22
- <link rel="modulepreload" crossorigin href="/forge/assets/board-CzgvdLO8.js">
16
+ <script type="module" crossorigin src="/forge/assets/index-BWtLtXwb.js"></script>
17
+ <link rel="modulepreload" crossorigin href="/forge/assets/vendor-QBH6qVEe.js">
18
+ <link rel="modulepreload" crossorigin href="/forge/assets/motion-CpZvZumD.js">
19
+ <link rel="modulepreload" crossorigin href="/forge/assets/ui-BXbpiKyS.js">
20
+ <link rel="modulepreload" crossorigin href="/forge/assets/table-DtyXTw03.js">
21
+ <link rel="modulepreload" crossorigin href="/forge/assets/viz-w-IMeueL.js">
22
+ <link rel="modulepreload" crossorigin href="/forge/assets/board-C_m78kvK.js">
23
23
  <link rel="stylesheet" crossorigin href="/forge/assets/vendor-CRS-psbw.css">
24
- <link rel="stylesheet" crossorigin href="/forge/assets/index-D4A_bq8m.css">
24
+ <link rel="stylesheet" crossorigin href="/forge/assets/index-Dp5GXY_z.css">
25
25
  </head>
26
26
  <body class="bg-canvas text-ink antialiased">
27
27
  <div id="root"></div>
@@ -5,6 +5,7 @@ export type ForgePluginConfig = {
5
5
  port: number;
6
6
  baseUrl: string;
7
7
  webAppUrl: string;
8
+ dataRoot: string;
8
9
  apiToken: string;
9
10
  actorLabel: string;
10
11
  timeoutMs: number;
@@ -1,3 +1,11 @@
1
1
  import type { ForgePluginConfig } from "./api-client.js";
2
+ export type ForgeRuntimeStopResult = {
3
+ ok: boolean;
4
+ stopped: boolean;
5
+ managed: boolean;
6
+ message: string;
7
+ pid: number | null;
8
+ };
2
9
  export declare function ensureForgeRuntimeReady(config: ForgePluginConfig): Promise<void>;
3
10
  export declare function primeForgeRuntime(config: ForgePluginConfig): void;
11
+ export declare function stopForgeRuntime(config: ForgePluginConfig): Promise<ForgeRuntimeStopResult>;
@@ -1,6 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync } from "node:fs";
3
+ import { homedir } from "node:os";
3
4
  import path from "node:path";
5
+ import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
4
6
  import { fileURLToPath } from "node:url";
5
7
  const LOCAL_HOSTNAMES = new Set(["127.0.0.1", "localhost", "::1"]);
6
8
  const STARTUP_TIMEOUT_MS = 15_000;
@@ -12,6 +14,63 @@ let startupPromise = null;
12
14
  function runtimeKey(config) {
13
15
  return `${config.origin}:${config.port}`;
14
16
  }
17
+ function getRuntimeStatePath(config) {
18
+ const origin = new URL(config.origin).hostname.toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
19
+ return path.join(homedir(), ".openclaw", "run", "forge-openclaw-plugin", `${origin}-${config.port}.json`);
20
+ }
21
+ async function writeRuntimeState(config, pid) {
22
+ const statePath = getRuntimeStatePath(config);
23
+ await mkdir(path.dirname(statePath), { recursive: true });
24
+ const payload = {
25
+ pid,
26
+ origin: config.origin,
27
+ port: config.port,
28
+ baseUrl: config.baseUrl,
29
+ startedAt: new Date().toISOString()
30
+ };
31
+ await writeFile(statePath, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
32
+ }
33
+ async function clearRuntimeState(config) {
34
+ await rm(getRuntimeStatePath(config), { force: true });
35
+ }
36
+ async function readRuntimeState(config) {
37
+ try {
38
+ const payload = await readFile(getRuntimeStatePath(config), "utf8");
39
+ const parsed = JSON.parse(payload);
40
+ if (typeof parsed.pid !== "number" || !Number.isFinite(parsed.pid)) {
41
+ return null;
42
+ }
43
+ return {
44
+ pid: Math.trunc(parsed.pid),
45
+ origin: typeof parsed.origin === "string" ? parsed.origin : config.origin,
46
+ port: typeof parsed.port === "number" ? parsed.port : config.port,
47
+ baseUrl: typeof parsed.baseUrl === "string" ? parsed.baseUrl : config.baseUrl,
48
+ startedAt: typeof parsed.startedAt === "string" ? parsed.startedAt : new Date(0).toISOString()
49
+ };
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ }
55
+ function processExists(pid) {
56
+ try {
57
+ process.kill(pid, 0);
58
+ return true;
59
+ }
60
+ catch (error) {
61
+ return !(error instanceof Error) || !("code" in error) || error.code !== "ESRCH";
62
+ }
63
+ }
64
+ async function waitForProcessExit(pid, timeoutMs) {
65
+ const deadline = Date.now() + timeoutMs;
66
+ while (Date.now() < deadline) {
67
+ if (!processExists(pid)) {
68
+ return true;
69
+ }
70
+ await new Promise((resolve) => setTimeout(resolve, HEALTHCHECK_INTERVAL_MS));
71
+ }
72
+ return !processExists(pid);
73
+ }
15
74
  function isLocalOrigin(origin) {
16
75
  try {
17
76
  return LOCAL_HOSTNAMES.has(new URL(origin).hostname.toLowerCase());
@@ -75,7 +134,8 @@ function spawnManagedRuntime(config, plan) {
75
134
  ...process.env,
76
135
  HOST: "127.0.0.1",
77
136
  PORT: String(config.port),
78
- FORGE_BASE_PATH: "/forge/"
137
+ FORGE_BASE_PATH: "/forge/",
138
+ ...(config.dataRoot ? { FORGE_DATA_ROOT: config.dataRoot } : {})
79
139
  },
80
140
  stdio: "ignore",
81
141
  detached: true
@@ -86,9 +146,13 @@ function spawnManagedRuntime(config, plan) {
86
146
  managedRuntimeChild = null;
87
147
  managedRuntimeKey = null;
88
148
  }
149
+ void clearRuntimeState(config);
89
150
  });
90
151
  managedRuntimeChild = child;
91
152
  managedRuntimeKey = runtimeKey(config);
153
+ void writeRuntimeState(config, child.pid).catch(() => {
154
+ // State tracking is best effort. Runtime health checks remain authoritative.
155
+ });
92
156
  }
93
157
  async function waitForRuntime(config, timeoutMs) {
94
158
  const deadline = Date.now() + timeoutMs;
@@ -133,3 +197,61 @@ export function primeForgeRuntime(config) {
133
197
  // Keep plugin registration non-blocking. Failures surface on first real call.
134
198
  });
135
199
  }
200
+ export async function stopForgeRuntime(config) {
201
+ if (!isLocalOrigin(config.origin)) {
202
+ return {
203
+ ok: false,
204
+ stopped: false,
205
+ managed: false,
206
+ message: "Forge stop only supports local plugin-managed runtimes. Remote Forge targets must be stopped where they are hosted.",
207
+ pid: null
208
+ };
209
+ }
210
+ const state = await readRuntimeState(config);
211
+ if (!state) {
212
+ return {
213
+ ok: true,
214
+ stopped: false,
215
+ managed: false,
216
+ message: (await isForgeHealthy(config, HEALTHCHECK_TIMEOUT_MS))
217
+ ? "Forge is running, but it does not look like a plugin-managed runtime. Stop it where it was started."
218
+ : "Forge is not running through the plugin-managed local runtime.",
219
+ pid: null
220
+ };
221
+ }
222
+ if (!processExists(state.pid)) {
223
+ await clearRuntimeState(config);
224
+ return {
225
+ ok: true,
226
+ stopped: false,
227
+ managed: true,
228
+ message: "The saved Forge runtime PID was stale. The plugin-managed runtime is already stopped.",
229
+ pid: state.pid
230
+ };
231
+ }
232
+ process.kill(state.pid, "SIGTERM");
233
+ if (!(await waitForProcessExit(state.pid, 5_000))) {
234
+ process.kill(state.pid, "SIGKILL");
235
+ if (!(await waitForProcessExit(state.pid, 2_000))) {
236
+ return {
237
+ ok: false,
238
+ stopped: false,
239
+ managed: true,
240
+ message: `Forge runtime pid ${state.pid} did not stop cleanly.`,
241
+ pid: state.pid
242
+ };
243
+ }
244
+ }
245
+ if (managedRuntimeChild?.pid === state.pid) {
246
+ managedRuntimeChild = null;
247
+ managedRuntimeKey = null;
248
+ }
249
+ await clearRuntimeState(config);
250
+ return {
251
+ ok: true,
252
+ stopped: true,
253
+ managed: true,
254
+ message: `Stopped the plugin-managed Forge runtime on ${config.baseUrl}.`,
255
+ pid: state.pid
256
+ };
257
+ }
@@ -45,6 +45,7 @@ export function resolveForgePluginConfig(pluginConfig) {
45
45
  port,
46
46
  baseUrl: buildForgeBaseUrl(origin, port),
47
47
  webAppUrl: buildForgeWebAppUrl(origin, port),
48
+ dataRoot: typeof raw.dataRoot === "string" ? raw.dataRoot.trim() : "",
48
49
  apiToken: typeof raw.apiToken === "string" ? raw.apiToken.trim() : "",
49
50
  actorLabel: normalizeString(raw.actorLabel, "aurel"),
50
51
  timeoutMs: normalizeTimeout(raw.timeoutMs, 15_000)
@@ -70,6 +71,11 @@ export const forgePluginConfigSchema = {
70
71
  maximum: 65535,
71
72
  description: "Forge server port. Override this when your local machine uses a different port."
72
73
  },
74
+ dataRoot: {
75
+ type: "string",
76
+ default: "",
77
+ description: "Optional absolute path for the Forge data folder root. Leave blank to use the runtime working directory."
78
+ },
73
79
  apiToken: {
74
80
  type: "string",
75
81
  default: "",
@@ -100,6 +106,12 @@ export const forgePluginConfigSchema = {
100
106
  help: "Forge server port. Change this if your local machine uses another port.",
101
107
  placeholder: "4317"
102
108
  },
109
+ dataRoot: {
110
+ label: "Forge Data Root",
111
+ help: "Optional absolute folder path for Forge data. Use this when you want Forge to read and write a specific data directory instead of the runtime working directory.",
112
+ placeholder: "/Users/you/forge-data",
113
+ advanced: true
114
+ },
103
115
  apiToken: {
104
116
  label: "Forge API Token",
105
117
  help: "Optional bearer token. Leave blank for one-step localhost or Tailscale operator-session bootstrap.",
@@ -1,4 +1,5 @@
1
1
  import { canBootstrapOperatorSession, callConfiguredForgeApi, expectForgeSuccess, readJsonRequestBody, readSingleHeaderValue, requireApiToken, writeForgeProxyResponse, writePluginError, writeRedirectResponse } from "./api-client.js";
2
+ import { stopForgeRuntime } from "./local-runtime.js";
2
3
  import { collectSupportedPluginApiRouteKeys, makeApiRouteKey } from "./parity.js";
3
4
  function passthroughSearch(path, url) {
4
5
  return `${path}${url.search}`;
@@ -374,6 +375,9 @@ export function registerForgePluginCli(api, config) {
374
375
  command.command("ui").description("Print the Forge UI entrypoint").action(async () => {
375
376
  console.log(JSON.stringify({ webAppUrl: await resolveForgeUiUrl(config), pluginUiRoute: "/forge/v1/ui" }, null, 2));
376
377
  });
378
+ command.command("stop").description("Stop the local Forge runtime when it was auto-started by the OpenClaw plugin").action(async () => {
379
+ console.log(JSON.stringify(await stopForgeRuntime(config), null, 2));
380
+ });
377
381
  command.command("doctor").description("Run plugin connectivity and curated route diagnostics").action(async () => {
378
382
  console.log(JSON.stringify(await runDoctor(config), null, 2));
379
383
  });
@@ -7,8 +7,8 @@ import { listActivityEvents, listActivityEventsForTask, removeActivityEvent } fr
7
7
  import { approveApprovalRequest, createAgentAction, createInsight, createInsightFeedback, deleteInsight, getInsightById, listAgentActions, listApprovalRequests, listInsights, rejectApprovalRequest, updateInsight } from "./repositories/collaboration.js";
8
8
  import { listEventLog } from "./repositories/event-log.js";
9
9
  import { createGoal, getGoalById, listGoals, updateGoal } from "./repositories/goals.js";
10
- import { createComment, getCommentById, listComments, updateComment } from "./repositories/comments.js";
11
10
  import { listDomains } from "./repositories/domains.js";
11
+ import { buildNotesSummaryByEntity, createNote, getNoteById, listNotes, updateNote } from "./repositories/notes.js";
12
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
13
  import { createProject, updateProject } from "./repositories/projects.js";
14
14
  import { createManualRewardGrant, getDailyAmbientXp, getRewardRuleById, listRewardLedger, listRewardRules, recordSessionEvent, updateRewardRule } from "./repositories/rewards.js";
@@ -26,8 +26,8 @@ import { getProjectBoard, listProjectSummaries } from "./services/projects.js";
26
26
  import { getWeeklyReviewPayload } from "./services/reviews.js";
27
27
  import { createTaskRunWatchdog } from "./services/task-run-watchdog.js";
28
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";
29
+ import { PSYCHE_ENTITY_TYPES, createBehaviorSchema, createBeliefEntrySchema, createBehaviorPatternSchema, createEmotionDefinitionSchema, createEventTypeSchema, createModeGuideSessionSchema, createModeProfileSchema, createPsycheValueSchema, createTriggerReportSchema, updateBehaviorSchema, updateBeliefEntrySchema, updateBehaviorPatternSchema, updateEmotionDefinitionSchema, updateEventTypeSchema, updateModeGuideSessionSchema, updateModeProfileSchema, updatePsycheValueSchema, updateTriggerReportSchema } from "./psyche-types.js";
30
+ import { activityListQuerySchema, activitySourceSchema, createAgentActionSchema, createAgentTokenSchema, batchCreateEntitiesSchema, batchDeleteEntitiesSchema, batchRestoreEntitiesSchema, batchSearchEntitiesSchema, batchUpdateEntitiesSchema, createGoalSchema, createInsightFeedbackSchema, createInsightSchema, createNoteSchema, createProjectSchema, createManualRewardGrantSchema, createSessionEventSchema, createTagSchema, notesListQuerySchema, updateTagSchema, createTaskSchema, eventsListQuerySchema, operatorLogWorkSchema, projectBoardPayloadSchema, projectListQuerySchema, entityDeleteQuerySchema, removeActivityEventSchema, resolveApprovalRequestSchema, rewardsLedgerQuerySchema, taskContextPayloadSchema, taskRunClaimSchema, taskRunFocusSchema, taskRunFinishSchema, taskRunHeartbeatSchema, taskRunListQuerySchema, taskListQuerySchema, tagSuggestionRequestSchema, uncompleteTaskSchema, updateSettingsSchema, updateGoalSchema, updateInsightSchema, updateNoteSchema, updateProjectSchema, updateRewardRuleSchema, updateTaskSchema } from "./types.js";
31
31
  import { buildOpenApiDocument } from "./openapi.js";
32
32
  import { registerWebRoutes } from "./web.js";
33
33
  import { createManagerRuntime } from "./managers/runtime.js";
@@ -133,7 +133,8 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
133
133
  { name: "status", type: "active|paused|completed", required: false, description: "Current lifecycle state for the goal.", enumValues: ["active", "paused", "completed"], defaultValue: "active" },
134
134
  { name: "targetPoints", type: "integer", required: false, description: "Approximate XP/point target for the goal.", defaultValue: 400 },
135
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: [] }
136
+ { name: "tagIds", type: "string[]", required: false, description: "Existing tag ids linked to the goal.", defaultValue: [] },
137
+ { name: "notes", type: "Array<{ contentMarkdown, author?, links? }>", required: false, description: "Optional nested notes that will auto-link to the new goal.", defaultValue: [] }
137
138
  ]
138
139
  },
139
140
  {
@@ -153,7 +154,8 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
153
154
  { name: "description", type: "string", required: false, description: "Desired outcome or scope.", defaultValue: "" },
154
155
  { name: "status", type: "active|paused|completed", required: false, description: "Lifecycle state.", enumValues: ["active", "paused", "completed"], defaultValue: "active" },
155
156
  { 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
+ { name: "themeColor", type: "hex-color", required: false, description: "Visual color used in the UI.", defaultValue: "#c0c1ff" },
158
+ { name: "notes", type: "Array<{ contentMarkdown, author?, links? }>", required: false, description: "Optional nested notes that will auto-link to the new project.", defaultValue: [] }
157
159
  ]
158
160
  },
159
161
  {
@@ -180,7 +182,27 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
180
182
  { name: "energy", type: "low|steady|high", required: false, description: "Energy demand.", enumValues: ["low", "steady", "high"], defaultValue: "steady" },
181
183
  { name: "points", type: "integer", required: false, description: "Reward value for the task.", defaultValue: 40 },
182
184
  { 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: [] }
185
+ { name: "tagIds", type: "string[]", required: false, description: "Existing tag ids linked to the task.", defaultValue: [] },
186
+ { name: "notes", type: "Array<{ contentMarkdown, author?, links? }>", required: false, description: "Optional nested notes that will auto-link to the new task.", defaultValue: [] }
187
+ ]
188
+ },
189
+ {
190
+ entityType: "note",
191
+ purpose: "A Markdown note that can link to one or many Forge entities.",
192
+ minimumCreateFields: ["contentMarkdown", "links"],
193
+ relationshipRules: [
194
+ "Notes can link to goals, projects, tasks, Psyche records, and other supported Forge entities.",
195
+ "When nested under another create flow, notes auto-link to that new entity and can optionally include extra links."
196
+ ],
197
+ searchHints: ["Search by Markdown content, author, or linked entity before creating a duplicate note."],
198
+ examples: [
199
+ '{"contentMarkdown":"Finished the review pass and captured the remaining edge cases.","links":[{"entityType":"task","entityId":"task_123"}]}',
200
+ '{"contentMarkdown":"Observed a stronger protector response after the meeting.","author":"forge-agent","links":[{"entityType":"trigger_report","entityId":"report_123"},{"entityType":"behavior_pattern","entityId":"pattern_123"}]}'
201
+ ],
202
+ fieldGuide: [
203
+ { name: "contentMarkdown", type: "string", required: true, description: "Markdown body of the note." },
204
+ { name: "author", type: "string|null", required: false, description: "Optional display author for the note.", defaultValue: null, nullable: true },
205
+ { name: "links", type: "Array<{ entityType, entityId, anchorKey? }>", required: true, description: "Entities this note should link to." }
184
206
  ]
185
207
  },
186
208
  {
@@ -540,9 +562,10 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
540
562
  requiredFields: ["operations", "operations[].entityType", "operations[].data"],
541
563
  notes: [
542
564
  "entityType alone is never enough; full data is required.",
543
- "Batch multiple related creates together when they come from one user ask."
565
+ "Batch multiple related creates together when they come from one user ask.",
566
+ "Goal, project, and task creates can include notes: [{ contentMarkdown, author?, links? }] and Forge will auto-link those notes to the newly created entity."
544
567
  ],
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"}]}'
568
+ example: '{"operations":[{"entityType":"task","data":{"title":"Write the public release notes","projectId":"project_123","status":"focus","notes":[{"contentMarkdown":"Starting from the changelog draft and the last QA pass."}]},"clientRef":"task-1"}]}'
546
569
  },
547
570
  {
548
571
  toolName: "forge_update_entities",
@@ -584,10 +607,10 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
584
607
  toolName: "forge_log_work",
585
608
  summary: "Log work that already happened.",
586
609
  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[] }",
610
+ inputShape: "{ taskId?: string, title?: string, description?: string, summary?: string, goalId?: string|null, projectId?: string|null, owner?: string, status?: TaskStatus, priority?: TaskPriority, dueDate?: string|null, effort?: TaskEffort, energy?: TaskEnergy, points?: number, tagIds?: string[], closeoutNote?: { contentMarkdown: string, author?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
588
611
  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}'
612
+ notes: ["Use taskId when logging work against an existing task.", "Use title when a new completed work item should be created and logged.", "closeoutNote persists the work summary as a real linked note."],
613
+ example: '{"taskId":"task_123","summary":"Finished the review draft and cleaned the notes.","points":40,"closeoutNote":{"contentMarkdown":"Finished the review draft, cleaned the note structure, and left one follow-up for QA."}}'
591
614
  },
592
615
  {
593
616
  toolName: "forge_start_task_run",
@@ -620,19 +643,19 @@ const AGENT_ONBOARDING_TOOL_INPUT_CATALOG = [
620
643
  toolName: "forge_complete_task_run",
621
644
  summary: "Finish an active run as completed work.",
622
645
  whenToUse: "Use when the user has finished the live work block.",
623
- inputShape: "{ taskRunId: string, actor?: string, note?: string }",
646
+ inputShape: "{ taskRunId: string, actor?: string, note?: string, closeoutNote?: { contentMarkdown: string, author?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
624
647
  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"}'
648
+ notes: ["This is the truthful way to finish live work and award completion effects.", "closeoutNote persists a real linked note instead of only updating the transient run note."],
649
+ example: '{"taskRunId":"run_123","actor":"aurel","note":"Finished the review draft","closeoutNote":{"contentMarkdown":"Completed the draft review and listed the follow-up fixes."}}'
627
650
  },
628
651
  {
629
652
  toolName: "forge_release_task_run",
630
653
  summary: "Stop an active run without marking the task complete.",
631
654
  whenToUse: "Use when the user is stopping or pausing work without completion.",
632
- inputShape: "{ taskRunId: string, actor?: string, note?: string }",
655
+ inputShape: "{ taskRunId: string, actor?: string, note?: string, closeoutNote?: { contentMarkdown: string, author?: string|null, links?: Array<{ entityType, entityId, anchorKey? }> } }",
633
656
  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"}'
657
+ notes: ["Use this instead of faking a stop by only changing task status.", "closeoutNote is useful for documenting blockers or handoff context."],
658
+ example: '{"taskRunId":"run_123","actor":"aurel","note":"Stopping for now; blocked on feedback","closeoutNote":{"contentMarkdown":"Blocked on feedback from design before I can continue."}}'
636
659
  }
637
660
  ];
638
661
  function buildAgentOnboardingPayload(request) {
@@ -656,7 +679,7 @@ function buildAgentOnboardingPayload(request) {
656
679
  "rewards.manage",
657
680
  "psyche.read",
658
681
  "psyche.write",
659
- "psyche.comment",
682
+ "psyche.note",
660
683
  "psyche.insight",
661
684
  "psyche.mode"
662
685
  ],
@@ -692,6 +715,7 @@ function buildAgentOnboardingPayload(request) {
692
715
  project: "A multi-step workstream under one goal. Projects organize related tasks.",
693
716
  task: "A concrete actionable work item. Task status is board state, not proof of live work.",
694
717
  taskRun: "A live work session attached to a task. Start, heartbeat, focus, complete, and release runs instead of faking work with status alone.",
718
+ note: "A Markdown work note that can link to one or many entities. Use notes for progress evidence, context, and close-out summaries.",
695
719
  insight: "An agent-authored observation or recommendation grounded in Forge data.",
696
720
  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
721
  },
@@ -713,6 +737,7 @@ function buildAgentOnboardingPayload(request) {
713
737
  "Projects belong to one goal through goalId.",
714
738
  "Tasks can belong to a goal, a project, both, or neither.",
715
739
  "Task runs represent live work sessions on tasks and are separate from task status.",
740
+ "Notes can link to one or many entities and are the canonical place for Markdown progress context or close-out evidence.",
716
741
  "Psyche values can link to goals, projects, and tasks.",
717
742
  "Behavior patterns, behaviors, beliefs, modes, and trigger reports cross-link to describe one reflective model rather than isolated records.",
718
743
  "Insights can point at one entity, but they exist to capture interpretation or advice rather than raw work items."
@@ -1173,12 +1198,12 @@ export async function buildServer(options = {}) {
1173
1198
  }
1174
1199
  return context;
1175
1200
  };
1176
- const requireCommentAccess = (headers, entityType, detail) => {
1201
+ const requireNoteAccess = (headers, entityType, detail) => {
1177
1202
  const context = authenticateRequest(headers);
1178
1203
  if (isPsycheEntityType(entityType)) {
1179
1204
  if (isPsycheAuthRequired()) {
1180
1205
  managers.authorization.requireAuthenticatedActor(context, detail);
1181
- managers.authorization.requireTokenScope(context, "psyche.comment", {
1206
+ managers.authorization.requireAnyTokenScope(context, ["psyche.note"], {
1182
1207
  entityType,
1183
1208
  ...(detail ?? {})
1184
1209
  });
@@ -1581,7 +1606,7 @@ export async function buildServer(options = {}) {
1581
1606
  }
1582
1607
  return {
1583
1608
  report,
1584
- comments: listComments({ entityType: "trigger_report", entityId: id }),
1609
+ notes: listNotes({ linkedEntityType: "trigger_report", linkedEntityId: id, limit: 50 }),
1585
1610
  insights: listInsights({ entityType: "trigger_report", entityId: id, limit: 50 })
1586
1611
  };
1587
1612
  });
@@ -1605,65 +1630,73 @@ export async function buildServer(options = {}) {
1605
1630
  }
1606
1631
  return { report };
1607
1632
  });
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
1633
+ app.get("/api/v1/notes", async (request) => {
1634
+ const query = notesListQuerySchema.parse(request.query ?? {});
1635
+ if (isPsycheEntityType(query.linkedEntityType)) {
1636
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], {
1637
+ route: "/api/v1/notes",
1638
+ entityType: query.linkedEntityType
1639
+ });
1640
+ }
1641
+ return { notes: listNotes(query) };
1642
+ });
1643
+ app.post("/api/v1/notes", async (request, reply) => {
1644
+ const input = createNoteSchema.parse(request.body ?? {});
1645
+ const firstLinkedEntityType = input.links[0]?.entityType;
1646
+ const auth = requireNoteAccess(request.headers, firstLinkedEntityType, {
1647
+ route: "/api/v1/notes",
1648
+ entityType: firstLinkedEntityType ?? null
1620
1649
  });
1621
- const comment = createComment(input, toActivityContext(auth));
1650
+ const note = createNote(input, toActivityContext(auth));
1622
1651
  reply.code(201);
1623
- return { comment };
1652
+ return { note };
1624
1653
  });
1625
- app.get("/api/v1/comments/:id", async (request, reply) => {
1654
+ app.get("/api/v1/notes/:id", async (request, reply) => {
1626
1655
  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;
1656
+ const current = getNoteById(id);
1657
+ const psycheEntityType = current?.links.find((link) => isPsycheEntityType(link.entityType))?.entityType;
1658
+ if (psycheEntityType) {
1659
+ requirePsycheScopedAccess(request.headers, ["psyche.read"], {
1660
+ route: "/api/v1/notes/:id",
1661
+ entityType: psycheEntityType
1662
+ });
1663
+ }
1633
1664
  if (!current) {
1634
1665
  reply.code(404);
1635
- return { error: "Comment not found" };
1666
+ return { error: "Note not found" };
1636
1667
  }
1637
- return { comment: current };
1668
+ return { note: current };
1638
1669
  });
1639
- app.patch("/api/v1/comments/:id", async (request, reply) => {
1670
+ app.patch("/api/v1/notes/:id", async (request, reply) => {
1640
1671
  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
1672
+ const patch = updateNoteSchema.parse(request.body ?? {});
1673
+ const current = getNoteById(id);
1674
+ const linkedEntityType = current?.links[0]?.entityType ?? patch.links?.[0]?.entityType ?? null;
1675
+ const auth = requireNoteAccess(request.headers, linkedEntityType, {
1676
+ route: "/api/v1/notes/:id",
1677
+ entityType: linkedEntityType
1646
1678
  });
1647
- const comment = updateComment(id, patch, toActivityContext(auth));
1648
- if (!comment) {
1679
+ const note = updateNote(id, patch, toActivityContext(auth));
1680
+ if (!note) {
1649
1681
  reply.code(404);
1650
- return { error: "Comment not found" };
1682
+ return { error: "Note not found" };
1651
1683
  }
1652
- return { comment };
1684
+ return { note };
1653
1685
  });
1654
- app.delete("/api/v1/comments/:id", async (request, reply) => {
1686
+ app.delete("/api/v1/notes/:id", async (request, reply) => {
1655
1687
  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
1688
+ const current = getNoteById(id);
1689
+ const linkedEntityType = current?.links.find((link) => isPsycheEntityType(link.entityType))?.entityType ?? current?.links[0]?.entityType ?? null;
1690
+ const auth = requireNoteAccess(request.headers, linkedEntityType, {
1691
+ route: "/api/v1/notes/:id",
1692
+ entityType: linkedEntityType
1660
1693
  });
1661
- const comment = deleteEntity("comment", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
1662
- if (!comment) {
1694
+ const note = deleteEntity("note", id, entityDeleteQuerySchema.parse(request.query ?? {}), toActivityContext(auth));
1695
+ if (!note) {
1663
1696
  reply.code(404);
1664
- return { error: "Comment not found" };
1697
+ return { error: "Note not found" };
1665
1698
  }
1666
- return { comment };
1699
+ return { note };
1667
1700
  });
1668
1701
  app.get("/api/v1/projects", async (request) => {
1669
1702
  const query = projectListQuerySchema.parse(request.query ?? {});
@@ -2116,7 +2149,8 @@ export async function buildServer(options = {}) {
2116
2149
  project: task.projectId ? listProjectSummaries().find((project) => project.id === task.projectId) ?? null : null,
2117
2150
  activeTaskRun: taskRuns.find((run) => run.status === "active") ?? null,
2118
2151
  taskRuns,
2119
- activity: listActivityEventsForTask(id, 20)
2152
+ activity: listActivityEventsForTask(id, 20),
2153
+ notesSummaryByEntity: buildNotesSummaryByEntity()
2120
2154
  });
2121
2155
  });
2122
2156
  app.get("/api/v1/tasks/:id/context", async (request, reply) => {
@@ -2133,7 +2167,8 @@ export async function buildServer(options = {}) {
2133
2167
  project: task.projectId ? listProjectSummaries().find((project) => project.id === task.projectId) ?? null : null,
2134
2168
  activeTaskRun: taskRuns.find((run) => run.status === "active") ?? null,
2135
2169
  taskRuns,
2136
- activity: listActivityEventsForTask(id, 20)
2170
+ activity: listActivityEventsForTask(id, 20),
2171
+ notesSummaryByEntity: buildNotesSummaryByEntity()
2137
2172
  });
2138
2173
  });
2139
2174
  app.post("/api/goals", async (request, reply) => {
@@ -2320,7 +2355,8 @@ export async function buildServer(options = {}) {
2320
2355
  effort: input.effort,
2321
2356
  energy: input.energy,
2322
2357
  points: input.points,
2323
- tagIds: input.tagIds
2358
+ tagIds: input.tagIds,
2359
+ notes: input.closeoutNote ? [input.closeoutNote] : undefined
2324
2360
  }, toActivityContext(auth));
2325
2361
  if (!task) {
2326
2362
  reply.code(404);
@@ -2344,7 +2380,8 @@ export async function buildServer(options = {}) {
2344
2380
  effort: input.effort ?? "deep",
2345
2381
  energy: input.energy ?? "steady",
2346
2382
  points: input.points ?? 40,
2347
- tagIds: input.tagIds ?? []
2383
+ tagIds: input.tagIds ?? [],
2384
+ notes: input.closeoutNote ? [input.closeoutNote] : []
2348
2385
  }), toActivityContext(auth));
2349
2386
  reply.code(201);
2350
2387
  return { task, xp: buildXpMetricsPayload() };
@@ -0,0 +1,49 @@
1
+ import path from "node:path";
2
+ import { closeDatabase, configureDatabase, configureDatabaseSeeding, getDatabase, initializeDatabase } from "./db.js";
3
+ const PERSONAL_CONTENT_TABLES = [
4
+ "goals",
5
+ "projects",
6
+ "tasks",
7
+ "task_runs",
8
+ "notes",
9
+ "insights",
10
+ "psyche_values",
11
+ "belief_entries",
12
+ "psyche_behaviors",
13
+ "behavior_patterns",
14
+ "mode_profiles",
15
+ "trigger_reports"
16
+ ];
17
+ function countRows(tableName) {
18
+ const row = getDatabase().prepare(`SELECT COUNT(*) AS count FROM ${tableName}`).get();
19
+ return row.count;
20
+ }
21
+ function getCounts() {
22
+ return Object.fromEntries(PERSONAL_CONTENT_TABLES.map((tableName) => [tableName, countRows(tableName)]));
23
+ }
24
+ export async function seedDemoDataIntoRuntime(dataRoot = process.env.FORGE_DATA_ROOT ?? process.cwd()) {
25
+ const resolvedDataRoot = path.resolve(dataRoot);
26
+ const databasePath = path.join(resolvedDataRoot, "data", "forge.sqlite");
27
+ closeDatabase();
28
+ configureDatabase({ dataRoot: resolvedDataRoot });
29
+ configureDatabaseSeeding(false);
30
+ await initializeDatabase();
31
+ const existingCounts = getCounts();
32
+ const hasPersonalContent = Object.values(existingCounts).some((count) => count > 0);
33
+ if (hasPersonalContent) {
34
+ closeDatabase();
35
+ throw new Error(`Refusing to seed demo data into a non-empty Forge runtime at ${databasePath}. Use a fresh FORGE_DATA_ROOT instead.`);
36
+ }
37
+ closeDatabase();
38
+ configureDatabase({ dataRoot: resolvedDataRoot });
39
+ configureDatabaseSeeding(true);
40
+ await initializeDatabase();
41
+ const counts = getCounts();
42
+ closeDatabase();
43
+ configureDatabaseSeeding(false);
44
+ return {
45
+ dataRoot: resolvedDataRoot,
46
+ databasePath,
47
+ counts
48
+ };
49
+ }