forge-openclaw-plugin 0.2.40 → 0.2.43

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 (31) hide show
  1. package/README.md +6 -0
  2. package/dist/assets/{board-C3BJqvZu.js → board-CAszQU7Y.js} +2 -2
  3. package/dist/assets/{board-C3BJqvZu.js.map → board-CAszQU7Y.js.map} +1 -1
  4. package/dist/assets/index-Bqqi_RSB.js +91 -0
  5. package/dist/assets/index-Bqqi_RSB.js.map +1 -0
  6. package/dist/assets/index-IrxlatFN.css +1 -0
  7. package/dist/assets/{motion-Cv9HdOn4.js → motion-CU5aNClV.js} +2 -2
  8. package/dist/assets/{motion-Cv9HdOn4.js.map → motion-CU5aNClV.js.map} +1 -1
  9. package/dist/assets/{table-B1MDOEFp.js → table-CK0KcPYW.js} +2 -2
  10. package/dist/assets/{table-B1MDOEFp.js.map → table-CK0KcPYW.js.map} +1 -1
  11. package/dist/assets/{ui-CQzq3TE5.js → ui-B5MjRjKe.js} +2 -2
  12. package/dist/assets/{ui-CQzq3TE5.js.map → ui-B5MjRjKe.js.map} +1 -1
  13. package/dist/assets/{vendor-DK-mJFy6.js → vendor-D_NZFJze.js} +2 -2
  14. package/dist/assets/{vendor-DK-mJFy6.js.map → vendor-D_NZFJze.js.map} +1 -1
  15. package/dist/index.html +7 -7
  16. package/dist/openclaw/plugin-sdk-types.d.ts +6 -0
  17. package/dist/openclaw/routes.js +10 -1
  18. package/dist/server/server/src/app.js +95 -37
  19. package/dist/server/server/src/health.js +49 -0
  20. package/dist/server/server/src/movement.js +79 -5
  21. package/dist/server/src/components/ui/info-tooltip.js +1 -1
  22. package/dist/server/src/lib/knowledge-graph.js +2 -2
  23. package/dist/server/src/lib/schemas.js +1 -1
  24. package/openclaw.plugin.json +1 -1
  25. package/package.json +43 -2
  26. package/skills/forge-openclaw/SKILL.md +2 -0
  27. package/skills/forge-openclaw/entity_conversation_playbooks.md +29 -3
  28. package/skills/forge-openclaw/psyche_entity_playbooks.md +18 -3
  29. package/dist/assets/index-46NL_IaJ.js +0 -91
  30. package/dist/assets/index-46NL_IaJ.js.map +0 -1
  31. package/dist/assets/index-Dj8vB0vh.css +0 -1
package/dist/index.html CHANGED
@@ -13,14 +13,14 @@
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-46NL_IaJ.js"></script>
17
- <link rel="modulepreload" crossorigin href="/forge/assets/vendor-DK-mJFy6.js">
18
- <link rel="modulepreload" crossorigin href="/forge/assets/board-C3BJqvZu.js">
19
- <link rel="modulepreload" crossorigin href="/forge/assets/ui-CQzq3TE5.js">
20
- <link rel="modulepreload" crossorigin href="/forge/assets/motion-Cv9HdOn4.js">
21
- <link rel="modulepreload" crossorigin href="/forge/assets/table-B1MDOEFp.js">
16
+ <script type="module" crossorigin src="/forge/assets/index-Bqqi_RSB.js"></script>
17
+ <link rel="modulepreload" crossorigin href="/forge/assets/vendor-D_NZFJze.js">
18
+ <link rel="modulepreload" crossorigin href="/forge/assets/board-CAszQU7Y.js">
19
+ <link rel="modulepreload" crossorigin href="/forge/assets/ui-B5MjRjKe.js">
20
+ <link rel="modulepreload" crossorigin href="/forge/assets/motion-CU5aNClV.js">
21
+ <link rel="modulepreload" crossorigin href="/forge/assets/table-CK0KcPYW.js">
22
22
  <link rel="stylesheet" crossorigin href="/forge/assets/vendor-DT3pnAKJ.css">
23
- <link rel="stylesheet" crossorigin href="/forge/assets/index-Dj8vB0vh.css">
23
+ <link rel="stylesheet" crossorigin href="/forge/assets/index-IrxlatFN.css">
24
24
  </head>
25
25
  <body class="bg-canvas text-ink antialiased">
26
26
  <div id="root"></div>
@@ -42,6 +42,11 @@ export type ForgeCliRegistrarContext = {
42
42
  debug?(message: string): void;
43
43
  };
44
44
  };
45
+ export type ForgeCliCommandDescriptor = {
46
+ name: string;
47
+ description: string;
48
+ hasSubcommands?: boolean;
49
+ };
45
50
  export type ForgePluginServiceContext = {
46
51
  config?: unknown;
47
52
  workspaceDir?: string;
@@ -77,6 +82,7 @@ export type ForgePluginRegistrationApi = {
77
82
  }): void;
78
83
  registerCli?(registrar: (context: ForgeCliRegistrarContext) => void, options?: {
79
84
  commands?: string[];
85
+ descriptors?: ForgeCliCommandDescriptor[];
80
86
  }): void;
81
87
  registerService?(service: ForgeRegisteredService): void;
82
88
  };
@@ -1158,5 +1158,14 @@ export function registerForgePluginCli(api, config) {
1158
1158
  .action(async () => {
1159
1159
  console.log(JSON.stringify(await runRouteCheck(config), null, 2));
1160
1160
  });
1161
- }, { commands: ["forge"] });
1161
+ }, {
1162
+ commands: ["forge"],
1163
+ descriptors: [
1164
+ {
1165
+ name: "forge",
1166
+ description: "Inspect and operate Forge through the OpenClaw plugin",
1167
+ hasSubcommands: true
1168
+ }
1169
+ ]
1170
+ });
1162
1171
  }
@@ -61,8 +61,8 @@ import { buildOpenApiDocument } from "./openapi.js";
61
61
  import { registerWebRoutes } from "./web.js";
62
62
  import { createManagerRuntime } from "./managers/runtime.js";
63
63
  import { isManagerError } from "./managers/type-guards.js";
64
- import { createCompanionPairingSession, createCompanionPairingSessionSchema, createSleepSession, createSleepSessionSchema, createWorkoutSession, createWorkoutSessionSchema, deleteSleepSession, deleteWorkoutSession, getCompanionPairingSessionById, getCompanionOverview, getFitnessViewData, getSleepSessionById, getSleepSessionDetailById, getSleepViewData, getVitalsViewData, getWorkoutSessionById, ingestMobileHealthSync, mobileHealthSyncSchema, patchCompanionPairingSourceState, patchCompanionPairingSourceStateSchema, companionSourceKeySchema, requireValidPairing, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, updateMobileCompanionSourceState, updateMobileCompanionSourceStateSchema, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
65
- import { analyzeMovementUserBoxPreflight, createMovementUserBox, createMovementPlace, deleteMovementUserBox, getMovementAllTimeSummary, getMovementBoxDetail, getMovementDayDetail, getMovementMobileBootstrap, getMovementTimeline, getMovementSelectionAggregate, getMovementSettings, getMovementTripDetail, getMovementMonthSummary, invalidateAutomaticMovementBox, listMovementPlaces, movementAutomaticBoxInvalidateSchema, movementMobileBootstrapSchema, movementMobilePlaceMutationSchema, movementMobileUserBoxCreateSchema, movementMobileUserBoxPreflightSchema, movementMobileUserBoxPatchSchema, movementMobileAutomaticBoxInvalidateSchema, movementMobileTimelineSchema, movementPlaceMutationSchema, movementPlacePatchSchema, movementSelectionAggregateSchema, movementStayPatchSchema, movementTripPatchSchema, movementUserBoxCreateSchema, movementUserBoxPreflightSchema, movementUserBoxPatchSchema, movementSettingsPatchSchema, movementTimelineQuerySchema, movementTripPointPatchSchema, deleteMovementStay, deleteMovementTrip, deleteMovementTripPoint, updateMovementPlace, updateMovementSettings, updateMovementStay, updateMovementTrip, updateMovementUserBox, updateMovementTripPoint, resolveMovementTimelineSegmentForBox } from "./movement.js";
64
+ import { createCompanionPairingSession, createCompanionPairingSessionSchema, createSleepSession, createSleepSessionSchema, createWorkoutSession, createWorkoutSessionSchema, deleteSleepSession, deleteWorkoutSession, getCompanionPairingSessionById, getCompanionOverview, getFitnessViewData, getSleepSessionById, getSleepSessionDetailById, getSleepTimelineOverlaysForRange, getSleepViewData, getVitalsViewData, getWorkoutSessionById, ingestMobileHealthSync, mobileHealthSyncSchema, patchCompanionPairingSourceState, patchCompanionPairingSourceStateSchema, companionSourceKeySchema, requireValidPairing, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, updateMobileCompanionSourceState, updateMobileCompanionSourceStateSchema, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
65
+ import { analyzeMovementUserBoxPreflight, createMovementUserBox, createMovementPlace, deleteMovementUserBox, getMovementAllTimeSummary, getMovementBoxDetail, getMovementDayDetail, getMovementMobileBootstrap, getMovementTimeline, getMovementSelectionAggregate, getMovementSettings, getMovementTripDetail, getMovementMonthSummary, invalidateAutomaticMovementBox, listMovementPlaces, movementAutomaticBoxInvalidateSchema, movementMobileBootstrapSchema, movementMobilePlaceMutationSchema, movementMobileStayPatchSchema, movementMobileUserBoxCreateSchema, movementMobileUserBoxPreflightSchema, movementMobileUserBoxPatchSchema, movementMobileAutomaticBoxInvalidateSchema, movementMobileTimelineSchema, movementPlaceMutationSchema, movementPlacePatchSchema, movementSelectionAggregateSchema, movementStayPatchSchema, movementTripPatchSchema, movementUserBoxCreateSchema, movementUserBoxPreflightSchema, movementUserBoxPatchSchema, movementSettingsPatchSchema, movementTimelineQuerySchema, movementTripPointPatchSchema, deleteMovementStay, deleteMovementTrip, deleteMovementTripPoint, updateMovementPlace, updateMovementSettings, updateMovementStay, updateMovementTrip, updateMovementUserBox, updateMovementTripPoint, resolveMovementTimelineSegmentForBox } from "./movement.js";
66
66
  import { getScreenTimeAllTimeSummary, getScreenTimeDayDetail, getScreenTimeMonthSummary, getScreenTimeSettings, screenTimeSettingsPatchSchema, updateScreenTimeSettings } from "./screen-time.js";
67
67
  import { assertWatchReady, buildWatchBootstrap, ingestWatchCaptureBatch, mobileWatchBootstrapSchema, mobileWatchCaptureBatchSchema, mobileWatchHabitCheckInSchema } from "./watch-mobile.js";
68
68
  const COMPATIBILITY_SUNSET = "transitional-node";
@@ -2654,7 +2654,8 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
2654
2654
  ],
2655
2655
  searchHints: [
2656
2656
  "Clarify whether the user wants a behavioral query, one trip or place, a missing-gap overlay, a manual add or update, or a link before choosing the route.",
2657
- "If the user already named a concrete missing span, confirm only the remaining time or place ambiguity, then use the movement overlay route and read the timeline back."
2657
+ "If the user already named a concrete missing span, confirm only the remaining time or place ambiguity, then use the movement overlay route and read the timeline back.",
2658
+ "If the user wants to revise or remove an already-saved correction, identify whether it is a user-defined box, automatic box, recorded stay, recorded trip, or trip point before choosing the repair or delete route."
2658
2659
  ],
2659
2660
  fieldGuide: []
2660
2661
  }),
@@ -2668,7 +2669,8 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
2668
2669
  ],
2669
2670
  searchHints: [
2670
2671
  "Clarify whether the user wants explanation, durable model changes, or a real-time tired or recovered signal before choosing the route.",
2671
- "Separate durable profile assumptions, weekday-template edits, and right-now fatigue signals before choosing the mutation path."
2672
+ "Separate durable profile assumptions, weekday-template edits, and right-now fatigue signals before choosing the mutation path.",
2673
+ "When the user is trying to understand the practical result of a change, read the overview again after the write instead of stopping at the mutation response."
2672
2674
  ],
2673
2675
  fieldGuide: []
2674
2676
  }),
@@ -2682,7 +2684,8 @@ const AGENT_ONBOARDING_ENTITY_CATALOG = [
2682
2684
  ],
2683
2685
  searchHints: [
2684
2686
  "Clarify whether the user wants flow discovery, editing, execution, published output, run inspection, or node-level output before choosing the route.",
2685
- "Distinguish flow contract, published output, run history, and latest-node-output questions before reaching for a route."
2687
+ "Distinguish flow contract, published output, run history, latest-node-output, and chat follow-up questions before reaching for a route.",
2688
+ "If the user is still deciding how to run or edit a flow, read flow detail or the box catalog before asking them for payload structure."
2686
2689
  ],
2687
2690
  fieldGuide: []
2688
2691
  }),
@@ -3061,7 +3064,7 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3061
3064
  },
3062
3065
  {
3063
3066
  focus: "movement",
3064
- openingQuestion: "Are you trying to understand where you stayed and traveled, change one stay or trip, or answer a question about your movement behavior?",
3067
+ openingQuestion: "What are you trying to understand, correct, or preserve about where you stayed and traveled?",
3065
3068
  coachingGoal: "Clarify whether the user wants a time-in-place query, travel-history review, a missing-gap overlay, one stay or trip change, one place summary, or a link before choosing the dedicated movement route.",
3066
3069
  askSequence: [
3067
3070
  "Ask whether the user is trying to query behavior, add something manually, update an existing movement item, or link movement to another Forge entity.",
@@ -3071,6 +3074,7 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3071
3074
  "Skip the meta lane question when the user already named the exact correction or review target and only one ambiguity remains.",
3072
3075
  "If the request is filling a missing-data gap, use a user-defined movement box rather than a raw stay or trip patch.",
3073
3076
  "If the request is repairing already-saved movement data, use the repair route that matches the saved object instead of treating it like a missing span.",
3077
+ "If the request is removing an already-saved correction, identify whether it is a user-defined box, recorded stay, recorded trip, or trip point before choosing the delete route.",
3074
3078
  "When the user wants place creation or place cleanup, use the dedicated movement place routes rather than a generic entity mutation.",
3075
3079
  "When the user already gave a concrete correction like 'I stayed home during that missing block', confirm only the interval or place if needed, then create the overlay and read the timeline back."
3076
3080
  ]
@@ -3086,12 +3090,13 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3086
3090
  "Ask whether the user is describing a stable weekly shape or just how today feels when the lane is still blurred.",
3087
3091
  "If the user already named the life-force lane clearly, skip the meta lane question and ask only for the specific weekday, profile field, or signal that still matters.",
3088
3092
  "If the request is about a durable baseline, use profile or weekday-template mutation rather than a fatigue signal; if it is about right now, prefer the fatigue-signal route.",
3093
+ "If the user wants to see what changed after a write, read the overview back instead of leaving the result implicit.",
3089
3094
  "Route to the dedicated life-force path once the lane is clear."
3090
3095
  ]
3091
3096
  },
3092
3097
  {
3093
3098
  focus: "workbench",
3094
- openingQuestion: "Are you trying to inspect a flow, change it, run it, or inspect one run's outputs?",
3099
+ openingQuestion: "What are you trying to inspect, change, run, or publish through Workbench?",
3095
3100
  coachingGoal: "Clarify whether the user wants flow discovery, editing, execution, run history, published outputs, or node-level inspection before using the dedicated workbench route family.",
3096
3101
  askSequence: [
3097
3102
  "Ask whether the job is flow discovery, one flow edit, execution, run history, published output, node-level inspection, or latest-node-output lookup.",
@@ -3099,6 +3104,7 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3099
3104
  "Ask whether they need the flow contract, a run result, a published output, or a node result.",
3100
3105
  "Ask what the user is trying to learn, repair, or publish through that flow.",
3101
3106
  "If the user already named the flow and action clearly, skip the meta lane question and ask only for the missing run, node, or output scope.",
3107
+ "If the user is still shaping a payload or edit, prefer flow detail or box catalog reads before asking for structured inputs.",
3102
3108
  "Prefer flow detail or published-output reads for stable contracts, and use run or node-result routes only when the user is asking about execution history or debugging.",
3103
3109
  "Route to the dedicated workbench route family once the execution lane is clear."
3104
3110
  ]
@@ -4065,25 +4071,32 @@ function buildAgentOnboardingPayload(request) {
4065
4071
  allTime: "/api/v1/movement/all-time",
4066
4072
  timeline: "/api/v1/movement/timeline",
4067
4073
  places: "/api/v1/movement/places",
4074
+ boxDetail: "/api/v1/movement/boxes/:id",
4068
4075
  tripDetail: "/api/v1/movement/trips/:id",
4069
4076
  selection: "/api/v1/movement/selection",
4070
4077
  settings: "/api/v1/movement/settings"
4071
4078
  },
4072
4079
  writeRoutes: {
4080
+ settingsUpdate: "/api/v1/movement/settings",
4073
4081
  placeCreate: "/api/v1/movement/places",
4074
4082
  placeUpdate: "/api/v1/movement/places/:id",
4075
4083
  userBoxCreate: "/api/v1/movement/user-boxes",
4076
4084
  userBoxPreflight: "/api/v1/movement/user-boxes/preflight",
4077
4085
  userBoxUpdate: "/api/v1/movement/user-boxes/:id",
4086
+ userBoxDelete: "/api/v1/movement/user-boxes/:id",
4078
4087
  automaticBoxInvalidate: "/api/v1/movement/automatic-boxes/:id/invalidate",
4079
4088
  stayUpdate: "/api/v1/movement/stays/:id",
4089
+ stayDelete: "/api/v1/movement/stays/:id",
4080
4090
  tripUpdate: "/api/v1/movement/trips/:id",
4081
- tripPointUpdate: "/api/v1/movement/trips/:id/points/:pointId"
4091
+ tripDelete: "/api/v1/movement/trips/:id",
4092
+ tripPointUpdate: "/api/v1/movement/trips/:id/points/:pointId",
4093
+ tripPointDelete: "/api/v1/movement/trips/:id/points/:pointId"
4082
4094
  },
4083
4095
  notes: [
4084
4096
  "Movement is not a normal batch CRUD entity family. It is a dedicated record of stays and trips: a stay means the user remained in the same place for a span of time, and a trip means they traveled between places.",
4085
4097
  "Use /api/v1/movement/day, /month, /all-time, /timeline, or /selection when the user wants behavioral answers such as how long they stayed at home, when they traveled, which places dominated a period, or what happened across a selected span.",
4086
4098
  "Use the movement write routes when the user wants to add a place or manual overlay, update a specific stay or trip, repair one recorded movement span, or attach movement context to another Forge record. If the user is filling a missing-data gap, the usual write path is a user-defined overlay box rather than a raw stay or trip patch.",
4099
+ "If the user is revising or removing an existing correction, first identify whether the saved object is a user-defined box, automatic box, recorded stay, recorded trip, or trip point so the repair or delete path stays truthful.",
4087
4100
  "For an explicit statement like 'that missing block was me staying home', do not reopen broad intake. Preflight only if timing overlap is unclear, then create a user-defined `stay` box for that interval and read the updated timeline back."
4088
4101
  ]
4089
4102
  },
@@ -4101,6 +4114,7 @@ function buildAgentOnboardingPayload(request) {
4101
4114
  "Life Force is a focused domain surface, not a batch CRUD entity type.",
4102
4115
  "Use GET /api/v1/life-force for the current overview payload with stats, drains, recommendations, and current-curve state.",
4103
4116
  "Patch the profile only for durable personal settings, update weekday templates only for the curve itself, and post fatigue signals for real-time tired or recovered observations.",
4117
+ "If the user is asking what changed after a profile, template, or fatigue write, read the overview back so the effect stays visible.",
4104
4118
  "If the user already knows they want a profile change, weekday-template edit, or right-now fatigue signal, skip the broad lane question and ask only for the missing weekday, profile field, or signal detail."
4105
4119
  ]
4106
4120
  },
@@ -4129,6 +4143,7 @@ function buildAgentOnboardingPayload(request) {
4129
4143
  notes: [
4130
4144
  "Workbench is a dedicated execution surface, not a batch CRUD entity family.",
4131
4145
  "Use the flow routes when the agent needs stable public input contracts, published outputs, node-level results, or reusable execution history.",
4146
+ "If the user is still figuring out inputs or editable structure, read flow detail or box catalog before asking them to author a payload from memory.",
4132
4147
  "Prefer the dedicated output and node-result routes over reverse-engineering raw traces.",
4133
4148
  "If the user already named the flow and wants one output or one run, skip the broad lane question and ask only for the missing run, node, or output scope."
4134
4149
  ]
@@ -4183,10 +4198,12 @@ function buildAgentOnboardingPayload(request) {
4183
4198
  verifyCommands: [
4184
4199
  `curl -s ${origin}/api/v1/health`,
4185
4200
  "openclaw plugins install ./projects/forge/openclaw-plugin",
4201
+ "openclaw plugins info forge-openclaw-plugin",
4186
4202
  "openclaw gateway restart"
4187
4203
  ],
4188
4204
  configNotes: [
4189
4205
  "Localhost and Tailscale targets can usually use the operator-session path without a long-lived token.",
4206
+ "If your current OpenClaw build blocks the repo-local install because of the package scanner, keep the repo folder on plugins.load.paths and verify that plugins info still points at the local Forge source path before continuing.",
4190
4207
  "Use a distinct actor label such as Albert (claw) so OpenClaw-originated work stays obvious in Forge provenance.",
4191
4208
  "Create each agent as a Forge bot user, then use userId or userIds in tool inputs whenever the agent should focus on one human, one bot, or a specific collaboration slice."
4192
4209
  ]
@@ -4330,8 +4347,8 @@ function buildAgentOnboardingPayload(request) {
4330
4347
  saveSuggestionPlacement: "end_of_message",
4331
4348
  saveSuggestionTone: "gentle_optional",
4332
4349
  maxQuestionsPerTurn: 1,
4333
- psycheExplorationRule: "When a Psyche entity needs understanding first, begin with one exploratory question before any working formulation, replacement belief, suggested title, or save pitch. Keep the opening reflection to one or two short sentences, stay in plain prose instead of bullets or numbered lists, keep that first reply short, do not mention Forge search or save structure yet, avoid colons or list-shaped phrasing, prefer what/when/how over why until the experience is grounded, wait for the user's answer before offering a fuller formulation, ask permission before moving from charged exploration into naming or challenge when needed, do not widen into adjacent entities until the current one has a working sentence the user recognizes, and once the lived experience is coherent stop deepening and help the user name it cleanly. If the user accepts the wording, move toward the save instead of reopening deeper exploration.",
4334
- specializedSurfaceRule: "For Movement, Life Force, and Workbench, clarify the lane first, then name the dedicated route family in plain language and do not guess at a generic CRUD path. When the user already named a precise correction or review target, confirm only the route-selecting detail that is still missing. The canonical runtime routes stay under /api/v1/*, and the OpenClaw HTTP mirror exposes the same families under /forge/v1/movement, /forge/v1/life-force, and /forge/v1/workbench.",
4350
+ psycheExplorationRule: "When a Psyche entity needs understanding first, begin with one exploratory question before any working formulation, replacement belief, suggested title, or save pitch. Keep the opening reflection to one or two short sentences, stay in plain prose instead of bullets or numbered lists, keep that first reply short, do not mention Forge search or save structure yet, avoid colons or list-shaped phrasing, prefer what/when/how over why until the experience is grounded, wait for the user's answer before offering a fuller formulation, ask permission before moving from charged exploration into naming or challenge when needed, do not widen into adjacent entities until the current one has a working sentence the user recognizes, and once the lived experience is coherent stop deepening and help the user name it cleanly. When the user is updating a Psyche record because of one fresh episode, anchor in that episode before renaming the durable formulation. If the user accepts the wording, move toward the save instead of reopening deeper exploration.",
4351
+ specializedSurfaceRule: "For Movement, Life Force, and Workbench, clarify the lane first, then name the dedicated route family in plain language and do not guess at a generic CRUD path. When the user already named a precise correction or review target, confirm only the route-selecting detail that is still missing. After a concrete specialized-surface correction, read the relevant specialized view back when the user is trying to understand the result rather than just store it. The canonical runtime routes stay under /api/v1/*, and the OpenClaw HTTP mirror exposes the same families under /forge/v1/movement, /forge/v1/life-force, and /forge/v1/workbench.",
4335
4352
  reviewShortcutRule: "When the user is reviewing or correcting an existing record, narrow the saved object, timeframe, or route family first. Do not reopen the whole intake unless the user is actually redefining the record.",
4336
4353
  readModelWriteRule: "Self-observation is note-backed and should be written through observed notes with frontmatter.observedAt. Sleep and workout sessions stay on batch CRUD by default; use the reflective review helpers only when enriching one already-known record after review.",
4337
4354
  psycheOpeningQuestionRule: "Prefer a concrete opening question tied to the entity: ask when the value mattered, what happened the last time the pattern appeared, what cue or body signal came first before the behavior, what the belief starts saying about self or outcome, what feels most at risk inside the mode, what the part is trying to get the user to do or stop doing, or where the shift began in the incident. Reflect briefly before the question, choose one follow-up lane at a time, say what is becoming clearer before the next deeper question, and if several Psyche entities are visible hold the adjacent ones lightly until the main container is clear.",
@@ -4543,6 +4560,30 @@ function resolveScopedUserIds(query) {
4543
4560
  const unique = Array.from(new Set(values));
4544
4561
  return unique.length > 0 ? unique : undefined;
4545
4562
  }
4563
+ function attachMovementTimelineSleepOverlays(movement, userIds) {
4564
+ const rangeStart = movement.segments.reduce((earliest, segment) => {
4565
+ if (!earliest || Date.parse(segment.startedAt) < Date.parse(earliest)) {
4566
+ return segment.startedAt;
4567
+ }
4568
+ return earliest;
4569
+ }, null);
4570
+ const rangeEnd = movement.segments.reduce((latest, segment) => {
4571
+ if (!latest || Date.parse(segment.endedAt) > Date.parse(latest)) {
4572
+ return segment.endedAt;
4573
+ }
4574
+ return latest;
4575
+ }, null);
4576
+ return {
4577
+ ...movement,
4578
+ sleepOverlays: rangeStart && rangeEnd
4579
+ ? getSleepTimelineOverlaysForRange({
4580
+ startedAt: rangeStart,
4581
+ endedAt: rangeEnd,
4582
+ userIds
4583
+ })
4584
+ : []
4585
+ };
4586
+ }
4546
4587
  function readRequestedUserIdFromBody(body) {
4547
4588
  if (!body || typeof body !== "object" || Array.isArray(body)) {
4548
4589
  return undefined;
@@ -4566,13 +4607,17 @@ function syncEntityOwnerFromBody(options) {
4566
4607
  setEntityOwner(options.entityType, options.entityId, owner.id);
4567
4608
  }
4568
4609
  function buildV1Context(userIds) {
4569
- const goals = filterOwnedEntities("goal", listGoals(), userIds);
4570
- const tasks = filterOwnedEntities("task", listTasks(), userIds);
4571
- const habits = filterOwnedEntities("habit", listHabits(), userIds);
4572
4610
  const users = listUsers();
4573
- const dashboard = getDashboard({ userIds });
4574
- const selectedUsers = userIds && userIds.length > 0
4575
- ? users.filter((user) => userIds.includes(user.id))
4611
+ const validUserIdSet = new Set(users.map((user) => user.id));
4612
+ const scopedUserIds = userIds && userIds.length > 0
4613
+ ? userIds.filter((userId) => validUserIdSet.has(userId))
4614
+ : undefined;
4615
+ const goals = filterOwnedEntities("goal", listGoals(), scopedUserIds);
4616
+ const tasks = filterOwnedEntities("task", listTasks(), scopedUserIds);
4617
+ const habits = filterOwnedEntities("habit", listHabits(), scopedUserIds);
4618
+ const dashboard = getDashboard({ userIds: scopedUserIds });
4619
+ const selectedUsers = scopedUserIds && scopedUserIds.length > 0
4620
+ ? users.filter((user) => scopedUserIds.includes(user.id))
4576
4621
  : users;
4577
4622
  return {
4578
4623
  meta: {
@@ -4584,23 +4629,23 @@ function buildV1Context(userIds) {
4584
4629
  },
4585
4630
  metrics: buildGamificationProfile(goals, tasks, habits),
4586
4631
  dashboard,
4587
- overview: getOverviewContext(new Date(), { userIds }),
4588
- today: getTodayContext(new Date(), { userIds }),
4589
- risk: getRiskContext(new Date(), { userIds }),
4632
+ overview: getOverviewContext(new Date(), { userIds: scopedUserIds }),
4633
+ today: getTodayContext(new Date(), { userIds: scopedUserIds }),
4634
+ risk: getRiskContext(new Date(), { userIds: scopedUserIds }),
4590
4635
  goals,
4591
- projects: listProjectSummaries({ userIds }),
4636
+ projects: listProjectSummaries({ userIds: scopedUserIds }),
4592
4637
  tags: listTags(),
4593
4638
  tasks,
4594
4639
  habits,
4595
4640
  users,
4596
- strategies: listStrategies({ userIds }),
4641
+ strategies: listStrategies({ userIds: scopedUserIds }),
4597
4642
  userScope: {
4598
- selectedUserIds: userIds ?? [],
4643
+ selectedUserIds: scopedUserIds ?? [],
4599
4644
  selectedUsers
4600
4645
  },
4601
4646
  activeTaskRuns: listTaskRuns({ active: true, limit: 25 }),
4602
4647
  activity: dashboard.recentActivity,
4603
- lifeForce: buildLifeForcePayload(new Date(), userIds)
4648
+ lifeForce: buildLifeForcePayload(new Date(), scopedUserIds)
4604
4649
  };
4605
4650
  }
4606
4651
  function buildXpMetricsPayload() {
@@ -5499,14 +5544,15 @@ export async function buildServer(options = {}) {
5499
5544
  });
5500
5545
  app.get("/api/v1/movement/timeline", async (request) => {
5501
5546
  const parsed = movementTimelineQuerySchema.parse(request.query ?? {});
5547
+ const userIds = parsed.userIds.length > 0
5548
+ ? parsed.userIds
5549
+ : (resolveScopedUserIds(request.query) ?? []);
5550
+ const movement = getMovementTimeline({
5551
+ ...parsed,
5552
+ userIds
5553
+ });
5502
5554
  return {
5503
- movement: getMovementTimeline({
5504
- ...parsed,
5505
- userIds: parsed.userIds.length > 0
5506
- ? parsed.userIds
5507
- : (resolveScopedUserIds(request.query) ??
5508
- [])
5509
- })
5555
+ movement: attachMovementTimelineSleepOverlays(movement, userIds)
5510
5556
  };
5511
5557
  });
5512
5558
  app.get("/api/v1/movement/settings", async (request) => ({
@@ -5775,12 +5821,13 @@ export async function buildServer(options = {}) {
5775
5821
  app.post("/api/v1/mobile/movement/timeline", async (request) => {
5776
5822
  const parsed = movementMobileTimelineSchema.parse(request.body ?? {});
5777
5823
  const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
5824
+ const movement = getMovementTimeline({
5825
+ before: parsed.before,
5826
+ limit: parsed.limit,
5827
+ userIds: [pairing.user_id]
5828
+ });
5778
5829
  return {
5779
- movement: getMovementTimeline({
5780
- before: parsed.before,
5781
- limit: parsed.limit,
5782
- userIds: [pairing.user_id]
5783
- })
5830
+ movement: attachMovementTimelineSleepOverlays(movement, [pairing.user_id])
5784
5831
  };
5785
5832
  });
5786
5833
  app.post("/api/v1/mobile/movement/boxes/:id/detail", async (request, reply) => {
@@ -5868,9 +5915,20 @@ export async function buildServer(options = {}) {
5868
5915
  };
5869
5916
  });
5870
5917
  app.patch("/api/v1/mobile/movement/stays/:id", async (request, reply) => {
5871
- reply.code(409);
5918
+ const parsed = movementMobileStayPatchSchema.parse(request.body ?? {});
5919
+ const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
5920
+ const { id } = request.params;
5921
+ const stay = updateMovementStay(id, parsed.patch, {
5922
+ actor: "Forge Companion",
5923
+ source: "system"
5924
+ }, { userId: pairing.user_id });
5925
+ if (!stay) {
5926
+ reply.code(404);
5927
+ return { error: "Movement stay not found" };
5928
+ }
5872
5929
  return {
5873
- error: "Recorded stays are immutable in product UI. Create or edit a user-defined movement box instead."
5930
+ stay,
5931
+ place: stay.place
5874
5932
  };
5875
5933
  });
5876
5934
  app.patch("/api/v1/mobile/movement/trips/:id", async (request, reply) => {
@@ -838,6 +838,55 @@ function listSleepRows(userIds) {
838
838
  ORDER BY started_at DESC`)
839
839
  .all(...params);
840
840
  }
841
+ export function getSleepTimelineOverlaysForRange(input) {
842
+ const rangeStartMs = Date.parse(input.startedAt);
843
+ const rangeEndMs = Date.parse(input.endedAt);
844
+ if (Number.isNaN(rangeStartMs) ||
845
+ Number.isNaN(rangeEndMs) ||
846
+ rangeEndMs <= rangeStartMs) {
847
+ return [];
848
+ }
849
+ return listSleepRows(input.userIds)
850
+ .map((row) => mapSleepSession(row))
851
+ .filter((session) => {
852
+ const sessionStartMs = Date.parse(session.startedAt);
853
+ const sessionEndMs = Date.parse(session.endedAt);
854
+ if (Number.isNaN(sessionStartMs) || Number.isNaN(sessionEndMs)) {
855
+ return false;
856
+ }
857
+ return sessionStartMs < rangeEndMs && sessionEndMs > rangeStartMs;
858
+ })
859
+ .sort((left, right) => {
860
+ const startedDelta = Date.parse(left.startedAt) - Date.parse(right.startedAt);
861
+ if (startedDelta !== 0) {
862
+ return startedDelta;
863
+ }
864
+ const endedDelta = Date.parse(left.endedAt) - Date.parse(right.endedAt);
865
+ if (endedDelta !== 0) {
866
+ return endedDelta;
867
+ }
868
+ return left.id.localeCompare(right.id);
869
+ })
870
+ .map((session) => {
871
+ const derived = session.derived;
872
+ return {
873
+ id: session.id,
874
+ externalUid: session.externalUid,
875
+ startedAt: session.startedAt,
876
+ endedAt: session.endedAt,
877
+ localDateKey: session.localDateKey,
878
+ sourceTimezone: session.sourceTimezone,
879
+ asleepSeconds: session.asleepSeconds,
880
+ timeInBedSeconds: session.timeInBedSeconds,
881
+ sleepScore: session.sleepScore,
882
+ regularityScore: session.regularityScore,
883
+ efficiency: typeof derived?.efficiency === "number" ? derived.efficiency : null,
884
+ recoveryState: typeof derived?.recoveryState === "string"
885
+ ? derived.recoveryState
886
+ : null
887
+ };
888
+ });
889
+ }
841
890
  function listWorkoutRows(userIds) {
842
891
  const params = [];
843
892
  const where = userIds && userIds.length > 0
@@ -130,7 +130,7 @@ const linkedPersonSchema = z.object({
130
130
  label: z.string().trim().min(1)
131
131
  });
132
132
  const movementPlaceInputSchema = z.object({
133
- externalUid: z.string().trim().min(1).default(""),
133
+ externalUid: z.string().trim().default(""),
134
134
  label: z.string().trim().min(1),
135
135
  aliases: z.array(z.string().trim()).default([]),
136
136
  latitude: z.number().finite(),
@@ -1011,6 +1011,61 @@ function mapMovementPlace(row) {
1011
1011
  : null
1012
1012
  };
1013
1013
  }
1014
+ function readMovementPlaceLearningState(row) {
1015
+ const metadata = safeJsonParse(row.metadata_json, {});
1016
+ const sampleCount = Number(metadata.distributionSampleCount);
1017
+ const averageLatitude = Number(metadata.distributionAverageLatitude);
1018
+ const averageLongitude = Number(metadata.distributionAverageLongitude);
1019
+ const maxDistanceMeters = Number(metadata.distributionMaxDistanceMeters);
1020
+ return {
1021
+ sampleCount: Number.isFinite(sampleCount) && sampleCount >= 1 ? Math.floor(sampleCount) : 1,
1022
+ averageLatitude: Number.isFinite(averageLatitude) ? averageLatitude : row.latitude,
1023
+ averageLongitude: Number.isFinite(averageLongitude) ? averageLongitude : row.longitude,
1024
+ maxDistanceMeters: Number.isFinite(maxDistanceMeters) && maxDistanceMeters >= 0
1025
+ ? maxDistanceMeters
1026
+ : 0
1027
+ };
1028
+ }
1029
+ function learnMovementPlaceObservation(input) {
1030
+ const existing = getDatabase()
1031
+ .prepare(`SELECT *
1032
+ FROM movement_places
1033
+ WHERE id = ?`)
1034
+ .get(input.placeId);
1035
+ if (!existing) {
1036
+ return null;
1037
+ }
1038
+ const metadata = safeJsonParse(existing.metadata_json, {});
1039
+ const learning = readMovementPlaceLearningState(existing);
1040
+ const nextSampleCount = learning.sampleCount + 1;
1041
+ const nextAverageLatitude = (learning.averageLatitude * learning.sampleCount + input.observation.latitude) /
1042
+ nextSampleCount;
1043
+ const nextAverageLongitude = (learning.averageLongitude * learning.sampleCount + input.observation.longitude) /
1044
+ nextSampleCount;
1045
+ const observationDistance = haversineDistanceMeters({ latitude: nextAverageLatitude, longitude: nextAverageLongitude }, input.observation);
1046
+ const nextMaxDistanceMeters = Math.max(learning.maxDistanceMeters, observationDistance);
1047
+ const nextRadiusMeters = Math.min(2000, Math.max(existing.radius_meters, Math.ceil(nextMaxDistanceMeters + 25)));
1048
+ const nextMetadata = {
1049
+ ...metadata,
1050
+ distributionSampleCount: String(nextSampleCount),
1051
+ distributionAverageLatitude: nextAverageLatitude.toFixed(6),
1052
+ distributionAverageLongitude: nextAverageLongitude.toFixed(6),
1053
+ distributionMaxDistanceMeters: nextMaxDistanceMeters.toFixed(1),
1054
+ distributionLastObservedAt: nowIso()
1055
+ };
1056
+ getDatabase()
1057
+ .prepare(`UPDATE movement_places
1058
+ SET latitude = ?,
1059
+ longitude = ?,
1060
+ radius_meters = ?,
1061
+ metadata_json = ?,
1062
+ updated_at = ?
1063
+ WHERE id = ?`)
1064
+ .run(nextAverageLatitude, nextAverageLongitude, nextRadiusMeters, JSON.stringify(nextMetadata), nowIso(), input.placeId);
1065
+ return mapMovementPlace(getDatabase()
1066
+ .prepare(`SELECT * FROM movement_places WHERE id = ?`)
1067
+ .get(input.placeId));
1068
+ }
1014
1069
  function mapMovementStay(row, placesById) {
1015
1070
  const note = row.published_note_id ? getNoteById(row.published_note_id) : null;
1016
1071
  const metrics = safeJsonParse(row.metrics_json, {});
@@ -1637,6 +1692,9 @@ function upsertMovementSettings(userId, input) {
1637
1692
  }
1638
1693
  function upsertMovementPlaceInternal(input) {
1639
1694
  const parsed = movementPlaceInputSchema.parse(input.place);
1695
+ const externalUid = parsed.externalUid.trim().length > 0
1696
+ ? parsed.externalUid.trim()
1697
+ : `movement-place-${randomUUID().replaceAll("-", "").slice(0, 20)}`;
1640
1698
  const now = nowIso();
1641
1699
  const existing = input.id && input.id.trim().length > 0
1642
1700
  ? getDatabase()
@@ -1644,14 +1702,14 @@ function upsertMovementPlaceInternal(input) {
1644
1702
  FROM movement_places
1645
1703
  WHERE id = ?`)
1646
1704
  .get(input.id)
1647
- : parsed.externalUid.trim().length > 0
1705
+ : externalUid.length > 0
1648
1706
  ? getDatabase()
1649
1707
  .prepare(`SELECT *
1650
1708
  FROM movement_places
1651
1709
  WHERE user_id = ?
1652
1710
  AND source = ?
1653
1711
  AND external_uid = ?`)
1654
- .get(input.userId, input.source, parsed.externalUid)
1712
+ .get(input.userId, input.source, externalUid)
1655
1713
  : undefined;
1656
1714
  const id = existing?.id ?? input.id ?? `mpl_${randomUUID().replaceAll("-", "").slice(0, 10)}`;
1657
1715
  getDatabase()
@@ -1677,7 +1735,7 @@ function upsertMovementPlaceInternal(input) {
1677
1735
  metadata_json = excluded.metadata_json,
1678
1736
  source = excluded.source,
1679
1737
  updated_at = excluded.updated_at`)
1680
- .run(id, parsed.externalUid, input.userId, parsed.label, JSON.stringify(uniqStrings(parsed.aliases)), parsed.latitude, parsed.longitude, parsed.radiusMeters, JSON.stringify(canonicalizeMovementCategoryTags(parsed.categoryTags)), parsed.visibility, parsed.wikiNoteId, JSON.stringify(parsed.linkedEntities), JSON.stringify(parsed.linkedPeople), JSON.stringify(parsed.metadata), input.source, existing?.created_at ?? now, now);
1738
+ .run(id, externalUid, input.userId, parsed.label, JSON.stringify(uniqStrings(parsed.aliases)), parsed.latitude, parsed.longitude, parsed.radiusMeters, JSON.stringify(canonicalizeMovementCategoryTags(parsed.categoryTags)), parsed.visibility, parsed.wikiNoteId, JSON.stringify(parsed.linkedEntities), JSON.stringify(parsed.linkedPeople), JSON.stringify(parsed.metadata), input.source, existing?.created_at ?? now, now);
1681
1739
  syncPlaceWikiMetadata(id);
1682
1740
  return mapMovementPlace(getDatabase()
1683
1741
  .prepare(`SELECT * FROM movement_places WHERE id = ?`)
@@ -3868,6 +3926,8 @@ export function updateMovementStay(stayId, patch, context, options = {}) {
3868
3926
  ...safeJsonParse(existing.metadata_json, {}),
3869
3927
  ...(parsed.metadata ?? {})
3870
3928
  };
3929
+ const shouldLearnPlaceObservation = resolvedPlace !== null &&
3930
+ (hasOwn(parsed, "placeId") || hasOwn(parsed, "placeExternalUid"));
3871
3931
  getDatabase()
3872
3932
  .prepare(`UPDATE movement_stays
3873
3933
  SET place_id = ?,
@@ -3886,6 +3946,17 @@ export function updateMovementStay(stayId, patch, context, options = {}) {
3886
3946
  WHERE id = ?`)
3887
3947
  .run(resolvedPlace?.id ?? null, parsed.label ?? parsed.placeLabel ?? existing.label, parsed.status ?? existing.status, parsed.classification ?? existing.classification, startedAt, endedAt, parsed.centerLatitude ?? existing.center_latitude, parsed.centerLongitude ?? existing.center_longitude, parsed.radiusMeters ?? existing.radius_meters, parsed.sampleCount ?? existing.sample_count, JSON.stringify(metrics), JSON.stringify(metadata), nowIso(), stayId);
3888
3948
  reconcileMovementOverlapValidation(existing.user_id);
3949
+ let learnedPlace = resolvedPlace ? mapMovementPlace(resolvedPlace) : null;
3950
+ if (resolvedPlace && shouldLearnPlaceObservation) {
3951
+ learnedPlace =
3952
+ learnMovementPlaceObservation({
3953
+ placeId: resolvedPlace.id,
3954
+ observation: {
3955
+ latitude: parsed.centerLatitude ?? existing.center_latitude,
3956
+ longitude: parsed.centerLongitude ?? existing.center_longitude
3957
+ }
3958
+ }) ?? learnedPlace;
3959
+ }
3889
3960
  const places = listMovementPlaceRows([existing.user_id]).map(mapMovementPlace);
3890
3961
  const placesById = new Map(places.map((place) => [place.id, place]));
3891
3962
  const updated = mapMovementStay(getDatabase()
@@ -3906,7 +3977,10 @@ export function updateMovementStay(stayId, patch, context, options = {}) {
3906
3977
  durationSeconds: updated.durationSeconds
3907
3978
  }
3908
3979
  });
3909
- return updated;
3980
+ return {
3981
+ ...updated,
3982
+ place: learnedPlace && updated.placeId === learnedPlace.id ? learnedPlace : updated.place
3983
+ };
3910
3984
  }
3911
3985
  export function updateMovementTrip(tripId, patch, context, options = {}) {
3912
3986
  const existing = getDatabase()
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useId, useRef, useState } from "react";
3
3
  import { CircleHelp } from "lucide-react";
4
- import { cn } from "@/lib/utils";
4
+ import { cn } from "../../lib/utils.js";
5
5
  export function FieldHint({ children, className }) {
6
6
  return _jsx("div", { className: cn("text-sm leading-6 text-white/50", className), children: children });
7
7
  }
@@ -1,5 +1,5 @@
1
- import { getEntityVisual, isEntityKind } from "@/lib/entity-visuals";
2
- import { KNOWLEDGE_GRAPH_HIERARCHY_LANES, KNOWLEDGE_GRAPH_HIERARCHY_ORDER, KNOWLEDGE_GRAPH_RELATION_FAMILY_LABELS, KNOWLEDGE_GRAPH_RELATION_LABELS, buildKnowledgeGraphNodeId } from "@/lib/knowledge-graph-types";
1
+ import { getEntityVisual, isEntityKind } from "./entity-visuals.js";
2
+ import { KNOWLEDGE_GRAPH_HIERARCHY_LANES, KNOWLEDGE_GRAPH_HIERARCHY_ORDER, KNOWLEDGE_GRAPH_RELATION_FAMILY_LABELS, KNOWLEDGE_GRAPH_RELATION_LABELS, buildKnowledgeGraphNodeId } from "./knowledge-graph-types.js";
3
3
  export function getKnowledgeGraphNodeVisual(node) {
4
4
  const kind = isEntityKind(node.entityKind) ? node.entityKind : "note";
5
5
  return getEntityVisual(kind);
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { forgeCustomThemeSchema, forgeThemePreferenceSchema } from "@/lib/theme-system";
2
+ import { forgeCustomThemeSchema, forgeThemePreferenceSchema } from "./theme-system.js";
3
3
  export const appLocaleSchema = z.enum(["en", "fr"]);
4
4
  export const inlineCreateNoteSchema = z.object({
5
5
  contentMarkdown: z.string().trim().min(1, "Note content is required"),
@@ -2,7 +2,7 @@
2
2
  "id": "forge-openclaw-plugin",
3
3
  "name": "Forge",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
- "version": "0.2.40",
5
+ "version": "0.2.43",
6
6
  "skills": [
7
7
  "./skills"
8
8
  ],