forge-openclaw-plugin 0.2.61 → 0.2.66

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 +28 -5
  2. package/dist/assets/{board-DThHV1D8.js → board-DFNV9VAZ.js} +1 -1
  3. package/dist/assets/index-CFZxwOFB.js +90 -0
  4. package/dist/assets/{index-7gvVCqnV.css → index-lFN9z5op.css} +1 -1
  5. package/dist/assets/{motion-BtTJtHCw.js → motion-CXdn34ih.js} +1 -1
  6. package/dist/assets/{table-Bnw6pcwN.js → table-CEq3bTDv.js} +1 -1
  7. package/dist/assets/{ui-CnVxFkj0.js → ui-g7FaEglG.js} +1 -1
  8. package/dist/assets/{vendor-BgZ3YrRd.js → vendor-BcOHGipZ.js} +236 -216
  9. package/dist/companion-iroh/darwin-arm64/forge-companion-iroh +0 -0
  10. package/dist/companion-iroh/darwin-x64/forge-companion-iroh +0 -0
  11. package/dist/companion-iroh/linux-x64/forge-companion-iroh +0 -0
  12. package/dist/companion-iroh-src/Cargo.lock +4559 -0
  13. package/dist/companion-iroh-src/Cargo.toml +37 -0
  14. package/dist/companion-iroh-src/src/lib.rs +279 -0
  15. package/dist/companion-iroh-src/src/main.rs +478 -0
  16. package/dist/companion-iroh-src/src/protocol.rs +129 -0
  17. package/dist/index.html +7 -7
  18. package/dist/server/server/src/app.js +163 -18
  19. package/dist/server/server/src/discovery-advertiser.js +13 -0
  20. package/dist/server/server/src/health.js +18 -3
  21. package/dist/server/server/src/movement.js +16 -1
  22. package/dist/server/server/src/openapi.js +12 -2
  23. package/dist/server/server/src/services/companion-iroh.js +425 -0
  24. package/dist/server/server/src/services/life-force.js +166 -25
  25. package/dist/server/server/src/web.js +88 -12
  26. package/openclaw.plugin.json +1 -1
  27. package/package.json +3 -3
  28. package/skills/forge-openclaw/SKILL.md +44 -2
  29. package/skills/forge-openclaw/entity_conversation_playbooks.md +217 -17
  30. package/skills/forge-openclaw/psyche_entity_playbooks.md +59 -0
  31. package/dist/assets/index-_Cn6Prym.js +0 -90
@@ -64,6 +64,7 @@ import { buildOpenApiDocument } from "./openapi.js";
64
64
  import { registerWebRoutes } from "./web.js";
65
65
  import { createManagerRuntime } from "./managers/runtime.js";
66
66
  import { isManagerError } from "./managers/type-guards.js";
67
+ import { buildCompanionPairingTransport, getCompanionIrohStatus, stopCompanionIroh } from "./services/companion-iroh.js";
67
68
  import { createCompanionPairingSession, createCompanionPairingSessionSchema, createSleepSession, createSleepSessionSchema, createWorkoutSession, createWorkoutSessionSchema, deleteSleepSession, deleteWorkoutSession, getCompanionPairingSessionById, getCompanionOverview, getFitnessViewData, getSleepSessionById, getSleepSessionDetailById, getSleepTimelineOverlaysForRange, getSleepViewData, getVitalsViewData, getWorkoutSessionById, heartbeatCompanionPairing, heartbeatCompanionPairingSchema, ingestMobileHealthSync, mobileHealthSyncSchema, patchCompanionPairingSourceState, patchCompanionPairingSourceStateSchema, companionSourceKeySchema, requireValidPairing, revokeAllCompanionPairingSessions, revokeAllCompanionPairingSessionsSchema, revokeCompanionPairingSession, updateMobileCompanionSourceState, updateMobileCompanionSourceStateSchema, verifyCompanionPairing, verifyCompanionPairingSchema, updateSleepMetadata, updateSleepMetadataSchema, updateWorkoutMetadata, updateWorkoutMetadataSchema } from "./health.js";
68
69
  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";
69
70
  import { getScreenTimeAllTimeSummary, getScreenTimeDayDetail, getScreenTimeMonthSummary, getScreenTimeSettings, screenTimeSettingsPatchSchema, updateScreenTimeSettings } from "./screen-time.js";
@@ -154,6 +155,18 @@ function buildApiBaseUrl(request) {
154
155
  const basePath = forwardedPrefix.replace(/\/$/, "");
155
156
  return `${request.protocol}://${host}${basePath}/api/v1`;
156
157
  }
158
+ function buildUiBaseUrlFromApiBaseUrl(apiBaseUrl) {
159
+ try {
160
+ const url = new URL(apiBaseUrl);
161
+ url.pathname = "/forge/";
162
+ url.search = "";
163
+ url.hash = "";
164
+ return url.toString();
165
+ }
166
+ catch {
167
+ return apiBaseUrl;
168
+ }
169
+ }
157
170
  function readSingleForwardedHeader(value) {
158
171
  if (Array.isArray(value)) {
159
172
  return value[0]?.split(",")[0]?.trim() || null;
@@ -2803,6 +2816,7 @@ const AGENT_ONBOARDING_CONVERSATION_RULES = [
2803
2816
  "Once the route family is clear, say it plainly enough that another agent could follow the same path without guessing.",
2804
2817
  "For Movement specifically, treat missing-data corrections as user-defined overlay boxes unless the user is editing an already-recorded stay or trip. When the user already gave a clear instruction like 'that missing block was home', act after only the last ambiguity is resolved.",
2805
2818
  "For action workflows such as task_run, work_adjustment, questionnaire_run, preference_judgment, preference_signal, and self_observation, keep the question focused on the missing action payload and do not downgrade the request into generic batch CRUD.",
2819
+ "For read-model-only health surfaces such as sleep_overview and sports_overview, use the dedicated overview reads first when the user wants review, pattern interpretation, recovery context, or training-load context. Move to sleep_session or workout_session writes only after one specific stored session needs enrichment.",
2806
2820
  "For normal stored Preferences and questionnaire records, use batch CRUD by default; switch to dedicated action routes only for judgments, signals, run answers, clone/draft/publish lifecycle, or visual comparison gameplay.",
2807
2821
  "When the user wants to remember a book, article, paper, source, concept, person, conversation, project reference, recurring explanation, or personal manual, consider wiki_page before note or self_observation.",
2808
2822
  "For meaning-bearing updates, especially in Psyche, briefly say what feels newly true before you ask for the one structural detail that still changes the save."
@@ -3019,6 +3033,30 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3019
3033
  "Ask about tags only if they help later retrieval."
3020
3034
  ]
3021
3035
  },
3036
+ {
3037
+ focus: "sleep_overview",
3038
+ openingQuestion: "What are you trying to understand from your sleep picture right now?",
3039
+ coachingGoal: "Review recent nights, regularity, score, stages, and recovery context before deciding whether one sleep_session needs enrichment.",
3040
+ askSequence: [
3041
+ "Ask what practical sleep question the user wants the overview to answer.",
3042
+ "Ask which night or date range matters only if the scope is not already clear.",
3043
+ "Use the dedicated sleep overview read before asking the user to reconstruct metrics from memory.",
3044
+ "Reflect the pattern or decision the user is trying to understand.",
3045
+ "Move to a sleep_session write only when one specific night needs reflective context, tags, notes, or links."
3046
+ ]
3047
+ },
3048
+ {
3049
+ focus: "sports_overview",
3050
+ openingQuestion: "What are you trying to understand from your workout picture right now?",
3051
+ coachingGoal: "Review workouts, training load, effort, activity types, and recovery context before deciding whether one workout_session needs enrichment.",
3052
+ askSequence: [
3053
+ "Ask what practical training or recovery question the user wants the overview to answer.",
3054
+ "Ask which workout or date range matters only if the scope is not already clear.",
3055
+ "Use the dedicated sports overview read before asking the user to reconstruct metrics from memory.",
3056
+ "Reflect the decision the user is trying to make from the training picture.",
3057
+ "Move to a workout_session write only when one specific workout needs reflective context, tags, notes, or links."
3058
+ ]
3059
+ },
3022
3060
  {
3023
3061
  focus: "preference_catalog",
3024
3062
  openingQuestion: "What decision or taste question should this catalog help with?",
@@ -3123,10 +3161,12 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3123
3161
  askSequence: [
3124
3162
  "Ask what they are trying to make clearer, repair, or preserve about where they were before you narrow to the exact movement lane.",
3125
3163
  "Ask whether the user is trying to query behavior, add something manually, update an existing movement item, or link movement to another Forge entity.",
3164
+ "Treat day, month, all-time, timeline, trip detail, and selection as internal route lanes. With the user, ask for the useful time window, place, selected span, stay, or trip instead of listing route choices.",
3126
3165
  "Ask whether the focus is a stay, a trip, a place, a timeline window, or a selected span.",
3127
3166
  "Ask for the time window, place, or movement item that makes the question concrete.",
3128
3167
  "Ask what they are trying to notice, preserve, or answer through that movement context.",
3129
3168
  "Choose the dedicated day, month, all-time, timeline, places, trip-detail, or selection route once the question shape is clear.",
3169
+ "Use allTime for whole-history aggregates, selection for a bounded selected-span aggregate, and tripDetail only when a concrete trip id is known.",
3130
3170
  "If the truth of one uncertain span is still unclear, read the timeline or saved-box detail before you mutate it.",
3131
3171
  "Skip the meta lane question when the user already named the exact correction or review target and only one ambiguity remains.",
3132
3172
  "If the request is filling a missing-data gap, use a user-defined movement box rather than a raw stay or trip patch.",
@@ -3143,6 +3183,8 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3143
3183
  askSequence: [
3144
3184
  "Ask what feels off, important, or worth tracking in their energy picture before you reduce it to one life-force lane.",
3145
3185
  "Ask whether the job is overview, profile change, weekday-template change, or fatigue signaling.",
3186
+ "Treat overview, profile, weekday-template, and fatigue-signal as internal route lanes. With the user, ask whether this is a current read, a durable assumption, a repeated weekday rhythm, or a right-now state instead of reciting route names.",
3187
+ "Use routeKey overview for the current read; it maps to GET /api/v1/life-force, not /api/v1/life-force/overview.",
3146
3188
  "Ask what part of the current energy picture feels most important or inaccurate.",
3147
3189
  "Ask what should stay true if they are changing profile or template assumptions.",
3148
3190
  "Ask whether the user is describing a stable weekly shape or just how today feels when the lane is still blurred.",
@@ -3160,6 +3202,8 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3160
3202
  askSequence: [
3161
3203
  "Ask what they are trying to learn, repair, publish, or run through Workbench before you narrow to flow discovery, editing, execution, or results.",
3162
3204
  "Ask whether the job is flow discovery, one flow edit, execution, run history, published output, node-level inspection, or latest-node-output lookup.",
3205
+ "Treat saved-flow catalog, box catalog, run history, run detail, node result, latest node output, and published output as internal read lanes. With the user, ask whether they need the saved flow, its input contract, one run, one node, or the public result instead of listing route keys.",
3206
+ "Use listFlows for the saved flow catalog and boxCatalog for available input-box contracts; do not collapse both into a vague catalog read.",
3163
3207
  "Ask which flow, slug, run, or node the request is about.",
3164
3208
  "Ask whether they need the stable flow contract, one run result, one published output, one node result, or the latest node output.",
3165
3209
  "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.",
@@ -3173,26 +3217,29 @@ const AGENT_ONBOARDING_ENTITY_CONVERSATION_PLAYBOOKS = [
3173
3217
  {
3174
3218
  focus: "event_type",
3175
3219
  openingQuestion: "What kind of moment keeps happening that you want future reports to name the same way each time?",
3176
- coachingGoal: "Create a reusable incident category that will actually help future reports stay consistent.",
3220
+ coachingGoal: "Bridge into Psyche-quality questioning for a reusable incident category without flattening the lived episode into cold taxonomy.",
3177
3221
  askSequence: [
3178
- "Ask what kind of moment or incident this label should capture in lived terms.",
3179
- "Reflect the repeated moment back in plain language before narrowing the wording.",
3180
- "Ask how narrow or broad it should be.",
3181
- "Ask what would count as inside versus outside the category if that boundary is still fuzzy.",
3182
- "Offer a concise label if the lived meaning is clearer than the wording.",
3183
- "Ask for a short description only if the label could be ambiguous later."
3222
+ "Treat event_type as Psyche taxonomy: use the event_type Psyche coaching playbook when the user is exploring meaning, and keep batch CRUD as the storage path.",
3223
+ "Ask what kind of emotionally meaningful moment keeps recurring and why naming it consistently would help future trigger reports.",
3224
+ "Reflect the repeated moment back in plain language by naming the emotional or relational stake before narrowing the wording.",
3225
+ "Ask for one recent example if the boundary is still abstract.",
3226
+ "Clarify what belongs inside this event type and what should stay outside it.",
3227
+ "Offer one concise candidate label once the repeated moment is clear.",
3228
+ "Link it to trigger reports, beliefs, patterns, modes, or emotion definitions only after the category itself feels accurate."
3184
3229
  ]
3185
3230
  },
3186
3231
  {
3187
3232
  focus: "emotion_definition",
3188
3233
  openingQuestion: "When this feeling is present, what tells you it is this feeling and not a nearby one?",
3189
- coachingGoal: "Create a reusable emotion label with enough clarity to use consistently later.",
3234
+ coachingGoal: "Bridge into Psyche-quality questioning for a reusable emotion label by its lived signature, not by a dictionary definition.",
3190
3235
  askSequence: [
3191
- "Ask what this feeling is like in lived terms when the user says it.",
3192
- "Reflect the felt signature back in plain language before you settle the label.",
3236
+ "Treat emotion_definition as Psyche taxonomy: use the emotion_definition Psyche coaching playbook when the user is exploring the feeling, and keep batch CRUD as the storage path.",
3237
+ "Ask when this feeling was present recently and what made it recognizable.",
3238
+ "Reflect the felt signature back in plain language before asking for category or label polish.",
3193
3239
  "Ask what distinguishes it from nearby emotions if that matters.",
3194
- "Offer a concise label if the felt meaning is clearer than the wording.",
3195
- "Ask for a broader category only if it will help later browsing or reporting."
3240
+ "Ask what the feeling tends to signal, protect, warn about, long for, or demand.",
3241
+ "Offer one concise definition in the user's language and invite correction.",
3242
+ "Link it to trigger reports, modes, beliefs, or patterns only after the definition feels steady."
3196
3243
  ]
3197
3244
  }
3198
3245
  ];
@@ -4324,6 +4371,31 @@ function buildAgentOnboardingPayload(request) {
4324
4371
  "Is this a missing-gap overlay, a saved-overlay repair, or an edit to one already-recorded stay, trip, or trip point?",
4325
4372
  "If the target is already known, what one time, place, or saved-object detail is still missing before acting?"
4326
4373
  ],
4374
+ methodRoutes: {
4375
+ day: "GET /api/v1/movement/day",
4376
+ month: "GET /api/v1/movement/month",
4377
+ allTime: "GET /api/v1/movement/all-time",
4378
+ timeline: "GET /api/v1/movement/timeline",
4379
+ places: "GET /api/v1/movement/places",
4380
+ boxDetail: "GET /api/v1/movement/boxes/:id",
4381
+ tripDetail: "GET /api/v1/movement/trips/:id",
4382
+ selection: "POST /api/v1/movement/selection",
4383
+ settings: "GET /api/v1/movement/settings",
4384
+ settingsUpdate: "PATCH /api/v1/movement/settings",
4385
+ placeCreate: "POST /api/v1/movement/places",
4386
+ placeUpdate: "PATCH /api/v1/movement/places/:id",
4387
+ userBoxCreate: "POST /api/v1/movement/user-boxes",
4388
+ userBoxPreflight: "POST /api/v1/movement/user-boxes/preflight",
4389
+ userBoxUpdate: "PATCH /api/v1/movement/user-boxes/:id",
4390
+ userBoxDelete: "DELETE /api/v1/movement/user-boxes/:id",
4391
+ automaticBoxInvalidate: "POST /api/v1/movement/automatic-boxes/:id/invalidate",
4392
+ stayUpdate: "PATCH /api/v1/movement/stays/:id",
4393
+ stayDelete: "DELETE /api/v1/movement/stays/:id",
4394
+ tripUpdate: "PATCH /api/v1/movement/trips/:id",
4395
+ tripDelete: "DELETE /api/v1/movement/trips/:id",
4396
+ tripPointUpdate: "PATCH /api/v1/movement/trips/:id/points/:pointId",
4397
+ tripPointDelete: "DELETE /api/v1/movement/trips/:id/points/:pointId"
4398
+ },
4327
4399
  readRoutes: {
4328
4400
  day: "/api/v1/movement/day",
4329
4401
  month: "/api/v1/movement/month",
@@ -4353,6 +4425,7 @@ function buildAgentOnboardingPayload(request) {
4353
4425
  },
4354
4426
  notes: [
4355
4427
  "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.",
4428
+ "Route-selection questions are internal. User-facing questions should ask for the useful time window, place, selected span, stay, or trip instead of reciting day/month/all-time/timeline/selection route keys.",
4356
4429
  "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.",
4357
4430
  "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.",
4358
4431
  "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.",
@@ -4368,6 +4441,12 @@ function buildAgentOnboardingPayload(request) {
4368
4441
  "Are they describing a repeatable weekly shape or a one-off current state?",
4369
4442
  "If the lane is already clear, what one weekday, profile field, or signal detail is still missing?"
4370
4443
  ],
4444
+ methodRoutes: {
4445
+ overview: "GET /api/v1/life-force",
4446
+ profile: "PATCH /api/v1/life-force/profile",
4447
+ weekdayTemplate: "PUT /api/v1/life-force/templates/:weekday",
4448
+ fatigueSignal: "POST /api/v1/life-force/fatigue-signals"
4449
+ },
4371
4450
  readRoutes: {
4372
4451
  overview: "/api/v1/life-force"
4373
4452
  },
@@ -4378,6 +4457,7 @@ function buildAgentOnboardingPayload(request) {
4378
4457
  },
4379
4458
  notes: [
4380
4459
  "Life Force is a focused domain surface, not a batch CRUD entity type.",
4460
+ "Route-selection questions are internal. User-facing questions should ask whether this is a current read, durable assumption, repeated weekday rhythm, or right-now state instead of reciting overview/profile/template/signal route keys.",
4381
4461
  "Use GET /api/v1/life-force for the current overview payload with stats, drains, recommendations, and current-curve state.",
4382
4462
  "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.",
4383
4463
  "If the user says something like 'I always dip on Tuesdays after lunch', treat that as a weekday-template change rather than a one-off fatigue signal.",
@@ -4394,6 +4474,12 @@ function buildAgentOnboardingPayload(request) {
4394
4474
  "Are they describing a repeatable weekly shape or a one-off current state?",
4395
4475
  "If the lane is already clear, what one weekday, profile field, or signal detail is still missing?"
4396
4476
  ],
4477
+ methodRoutes: {
4478
+ overview: "GET /api/v1/life-force",
4479
+ profile: "PATCH /api/v1/life-force/profile",
4480
+ weekdayTemplate: "PUT /api/v1/life-force/templates/:weekday",
4481
+ fatigueSignal: "POST /api/v1/life-force/fatigue-signals"
4482
+ },
4397
4483
  readRoutes: {
4398
4484
  overview: "/api/v1/life-force"
4399
4485
  },
@@ -4405,6 +4491,7 @@ function buildAgentOnboardingPayload(request) {
4405
4491
  notes: [
4406
4492
  "This `life_force` key exists so agents can look up the specialized route family by the entity catalog name without guessing that the canonical surface key is `lifeForce`.",
4407
4493
  "Life Force is a focused domain surface, not a batch CRUD entity type.",
4494
+ "Route-selection questions are internal. User-facing questions should ask whether this is a current read, durable assumption, repeated weekday rhythm, or right-now state instead of reciting overview/profile/template/signal route keys.",
4408
4495
  "Use GET /api/v1/life-force for the current overview payload with stats, drains, recommendations, and current-curve state.",
4409
4496
  "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.",
4410
4497
  "If the user says something like 'I always dip on Tuesdays after lunch', treat that as a weekday-template change rather than a one-off fatigue signal.",
@@ -4421,6 +4508,24 @@ function buildAgentOnboardingPayload(request) {
4421
4508
  "Does the user need a stable public contract or one execution artifact?",
4422
4509
  "If the flow is already known, what one run, node, or output scope detail is still missing before acting?"
4423
4510
  ],
4511
+ methodRoutes: {
4512
+ listFlows: "GET /api/v1/workbench/flows",
4513
+ flowById: "GET /api/v1/workbench/flows/:id",
4514
+ flowBySlug: "GET /api/v1/workbench/flows/by-slug/:slug",
4515
+ publishedOutput: "GET /api/v1/workbench/flows/:id/output",
4516
+ runs: "GET /api/v1/workbench/flows/:id/runs",
4517
+ runDetail: "GET /api/v1/workbench/flows/:id/runs/:runId",
4518
+ runNodes: "GET /api/v1/workbench/flows/:id/runs/:runId/nodes",
4519
+ nodeResult: "GET /api/v1/workbench/flows/:id/runs/:runId/nodes/:nodeId",
4520
+ latestNodeOutput: "GET /api/v1/workbench/flows/:id/nodes/:nodeId/output",
4521
+ boxCatalog: "GET /api/v1/workbench/catalog/boxes",
4522
+ createFlow: "POST /api/v1/workbench/flows",
4523
+ updateFlow: "PATCH /api/v1/workbench/flows/:id",
4524
+ deleteFlow: "DELETE /api/v1/workbench/flows/:id",
4525
+ runFlow: "POST /api/v1/workbench/flows/:id/run",
4526
+ runByPayload: "POST /api/v1/workbench/run",
4527
+ chatFlow: "POST /api/v1/workbench/flows/:id/chat"
4528
+ },
4424
4529
  readRoutes: {
4425
4530
  listFlows: "/api/v1/workbench/flows",
4426
4531
  flowById: "/api/v1/workbench/flows/:id",
@@ -4443,6 +4548,7 @@ function buildAgentOnboardingPayload(request) {
4443
4548
  },
4444
4549
  notes: [
4445
4550
  "Workbench is a dedicated execution surface, not a batch CRUD entity family.",
4551
+ "Route-selection questions are internal. User-facing questions should ask whether the user needs the saved flow, its input contract, one run, one node, or the public result instead of reciting Workbench route keys.",
4446
4552
  "Use the flow routes when the agent needs stable public input contracts, published outputs, node-level results, or reusable execution history.",
4447
4553
  "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.",
4448
4554
  "Prefer the dedicated output and node-result routes over reverse-engineering raw traces.",
@@ -4453,10 +4559,15 @@ function buildAgentOnboardingPayload(request) {
4453
4559
  },
4454
4560
  readModelOnlySurfaces: {
4455
4561
  sleepOverview: "/api/v1/health/sleep",
4562
+ sleep_overview: "/api/v1/health/sleep",
4456
4563
  sportsOverview: "/api/v1/health/fitness",
4564
+ sports_overview: "/api/v1/health/fitness",
4457
4565
  selfObservation: "/api/v1/psyche/self-observation/calendar",
4566
+ self_observation: "/api/v1/psyche/self-observation/calendar",
4458
4567
  calendarOverview: "/api/v1/calendar/overview",
4568
+ calendar_overview: "/api/v1/calendar/overview",
4459
4569
  operatorOverview: "/api/v1/operator/overview",
4570
+ operator_overview: "/api/v1/operator/overview",
4460
4571
  operatorContext: "/api/v1/operator/context"
4461
4572
  }
4462
4573
  },
@@ -4668,7 +4779,7 @@ function buildAgentOnboardingPayload(request) {
4668
4779
  saveSuggestionPlacement: "end_of_message",
4669
4780
  saveSuggestionTone: "gentle_optional",
4670
4781
  maxQuestionsPerTurn: 1,
4671
- 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, make the next question help the user feel more able to name the experience rather than more examined, 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, begin with the smallest part of the old wording that no longer fits, and do not reopen the full origin story unless the new understanding is truly structural. If the user accepts the wording, move toward the save instead of reopening deeper exploration.",
4782
+ 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, make the next question help the user feel more able to name the experience rather than more examined, 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. After one concrete example is clear and a hypothesis lands or is corrected, translate it into a saveable record shape such as a belief sentence, functional loop, behavior, mode, trigger report, value, event type, or emotion definition; ask one accuracy question instead of reopening broad exploration, then use the shared batch entity routes after the user accepts the wording or explicitly asks to save. When the user is updating a Psyche record because of one fresh episode, anchor in that episode before renaming the durable formulation, begin with the smallest part of the old wording that no longer fits, and do not reopen the full origin story unless the new understanding is truly structural. If the user accepts the wording, move toward the save instead of reopening deeper exploration.",
4672
4783
  specializedSurfaceRule: "For Movement, Life Force, and Workbench, clarify the job first, then choose the dedicated route family internally and do not guess at a generic CRUD path. Use specializedDomainSurfaces.routeSelectionQuestions when they are present so the next follow-up selects the right route instead of asking generic questions. When available, use forge_call_movement_route, forge_call_life_force_route, or forge_call_workbench_route after the lane is clear. In user-facing language, talk about timeline, overlay, weekday template, published output, run detail, or node result rather than surfaces, payloads, read paths, mutation paths, or CRUD. If the truth of the current state is still uncertain, read the relevant dedicated view before you mutate it. When the user already named a precise correction or review target, confirm only the route-selecting detail that is still missing. After a concrete Movement, Life Force, or Workbench correction, read the relevant 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.",
4673
4784
  reviewShortcutRule: "When the user is reviewing or correcting an existing record, ask what practical question they want the read or correction to answer, then narrow the saved object, timeframe, or route family first. Do not reopen the whole intake unless the user is actually redefining the record.",
4674
4785
  readModelWriteRule: "Self-observation is note-backed and should be written through observed notes with frontmatter.observedAt only when a lightweight episode observation is the right container. Do not use it as the default bucket for Psyche material: prefer trigger_report for one emotionally meaningful episode, behavior_pattern for functional analysis of a recurring loop, behavior for one repeated move, belief_entry for a core sentence, mode_guide_session or mode_profile for a central part-state, and wiki_page for durable memory such as books, articles, concepts, sources, or personal manuals. Sleep and workout sessions stay on batch CRUD by default; use the reflective review helpers only when enriching one already-known record after review.",
@@ -4694,10 +4805,28 @@ function buildAgentOnboardingPayload(request) {
4694
4805
  searchRule: "forge_search_entities accepts searches as an array. Search before create or update when duplicate risk exists.",
4695
4806
  createRule: "Each create operation must include entityType and full data. entityType alone is not enough. This includes calendar_event, work_block_template, task_timebox, sleep_session, workout_session, preference CRUD entities, and questionnaire_instrument alongside the usual planning and Psyche entities.",
4696
4807
  updateRule: "Each update operation must include entityType, id, and patch. For projects, lifecycle changes are status patches: active to restart, paused to suspend, completed to finish. Keep task and project scheduling rules on those same patch payloads. Official habit outcomes can also be logged through forge_update_entities by patching the habit with checkIn: { status, dateKey?, note?, description? } instead of route-hunting. Calendar-event updates still run downstream provider projection sync, and manual health-session field edits belong on the batch route by default rather than on the reflective review helpers.",
4697
- specializedRouteToolRule: "forge_call_movement_route, forge_call_life_force_route, and forge_call_workbench_route expect { routeKey, pathParams?, query?, body? }. Choose routeKey from the tool schema or entityRouteModel.specializedDomainSurfaces, fill pathParams for placeholders such as id, weekday, slug, runId, nodeId, or pointId, use query for read filters and userIds, and use body only for POST, PATCH, or PUT route keys. Normal stored entities still use the shared batch entity tools.",
4808
+ specializedRouteToolRule: "forge_call_movement_route, forge_call_life_force_route, and forge_call_workbench_route expect { routeKey, pathParams?, query?, body? }. Choose routeKey from the tool schema or entityRouteModel.specializedDomainSurfaces, fill pathParams for placeholders such as id, weekday, slug, runId, nodeId, or pointId, use query for read filters and userIds, and use body only for POST, PATCH, or PUT route keys. The Life Force overview route key maps to GET /api/v1/life-force; do not invent /api/v1/life-force/overview. Normal stored entities still use the shared batch entity tools.",
4698
4809
  createExample: '{"operations":[{"entityType":"goal","data":{"title":"Create meaningfully"},"clientRef":"goal-create-1"},{"entityType":"goal","data":{"title":"Build a beautiful family"},"clientRef":"goal-create-2"}]}',
4699
4810
  updateExample: '{"operations":[{"entityType":"project","id":"project_123","patch":{"status":"paused","schedulingRules":{"blockWorkBlockKinds":["main_activity"],"allowWorkBlockKinds":["secondary_activity"]}},"clientRef":"project-suspend-1"},{"entityType":"habit","id":"habit_456","patch":{"checkIn":{"status":"missed","note":"Resisted the urge after dinner.","description":"85 sec reset"}},"clientRef":"habit-check-in-1"}]}',
4700
- specializedRouteToolExample: '{"routeKey":"weekdayTemplate","pathParams":{"weekday":"monday"},"body":{"points":[{"hour":13,"freeAp":-4}]}}'
4811
+ specializedRouteToolExample: '{"routeKey":"weekdayTemplate","pathParams":{"weekday":"monday"},"body":{"points":[{"hour":13,"freeAp":-4}]}}',
4812
+ specializedRouteToolExamples: {
4813
+ movementAllTime: '{"routeKey":"allTime","query":{"userIds":["user_operator"]}}',
4814
+ movementTimeline: '{"routeKey":"timeline","query":{"from":"2026-05-01T00:00:00.000Z","to":"2026-05-06T23:59:59.999Z","userIds":["user_operator"]}}',
4815
+ movementSelection: '{"routeKey":"selection","query":{"from":"2026-05-01T00:00:00.000Z","to":"2026-05-14T23:59:59.999Z","placeIds":["place_home"],"userIds":["user_operator"]}}',
4816
+ movementTripDetail: '{"routeKey":"tripDetail","pathParams":{"id":"trip_123"}}',
4817
+ movementMissingStayPreflight: '{"routeKey":"userBoxPreflight","body":{"kind":"stay","startedAt":"2026-05-06T13:00:00.000Z","endedAt":"2026-05-06T15:00:00.000Z","placeLabel":"Home","userId":"user_operator"}}',
4818
+ movementMissingStayCreate: '{"routeKey":"userBoxCreate","body":{"kind":"stay","startedAt":"2026-05-06T13:00:00.000Z","endedAt":"2026-05-06T15:00:00.000Z","placeLabel":"Home","userId":"user_operator","note":"Manual correction after reviewing the timeline."}}',
4819
+ lifeForceOverview: '{"routeKey":"overview"}',
4820
+ lifeForceProfile: '{"routeKey":"profile","body":{"baselineDailyAp":24,"recoveryNotes":"Clinic-admin days need a lower expected afternoon load."}}',
4821
+ lifeForceWeekdayTemplate: '{"routeKey":"weekdayTemplate","pathParams":{"weekday":"monday"},"body":{"points":[{"hour":13,"freeAp":-4}]}}',
4822
+ lifeForceFatigueSignal: '{"routeKey":"fatigueSignal","body":{"signal":"tired","intensity":7,"note":"Sharp post-lunch dip after clinic admin."}}',
4823
+ workbenchFlowCatalog: '{"routeKey":"listFlows","query":{"includeArchived":false}}',
4824
+ workbenchBoxCatalog: '{"routeKey":"boxCatalog"}',
4825
+ workbenchRunDetail: '{"routeKey":"runDetail","pathParams":{"id":"flow_research_digest","runId":"run_123"}}',
4826
+ workbenchPublishedOutput: '{"routeKey":"publishedOutput","pathParams":{"id":"flow_research_digest"}}',
4827
+ workbenchLatestNodeOutput: '{"routeKey":"latestNodeOutput","pathParams":{"id":"flow_research_digest","nodeId":"node_summary"}}',
4828
+ workbenchRunFlow: '{"routeKey":"runFlow","pathParams":{"id":"flow_research_digest"},"body":{"input":{"topic":"question flow quality"}}}'
4829
+ }
4701
4830
  }
4702
4831
  };
4703
4832
  }
@@ -6097,6 +6226,7 @@ export async function buildServer(options = {}) {
6097
6226
  clearInterval(cronSchedulerTimer);
6098
6227
  clearInterval(dataBackupTimer);
6099
6228
  taskRunWatchdog?.stop();
6229
+ await stopCompanionIroh();
6100
6230
  await managers.backgroundJobs.stop();
6101
6231
  });
6102
6232
  const enqueueWikiIngestJob = (jobId) => {
@@ -6884,11 +7014,26 @@ export async function buildServer(options = {}) {
6884
7014
  requireOperatorSession(request.headers, {
6885
7015
  route: "/api/v1/health/pairing-sessions"
6886
7016
  });
6887
- reply.code(201);
6888
- return createCompanionPairingSession(buildApiBaseUrl({
7017
+ const parsed = createCompanionPairingSessionSchema.parse(request.body ?? {});
7018
+ const requestApiBaseUrl = buildApiBaseUrl({
6889
7019
  protocol: request.protocol,
6890
7020
  headers: request.headers
6891
- }), createCompanionPairingSessionSchema.parse(request.body ?? {}));
7021
+ });
7022
+ const pairingTransport = await buildCompanionPairingTransport({
7023
+ requestedMode: parsed.transportMode,
7024
+ requestApiBaseUrl,
7025
+ requestUiBaseUrl: buildUiBaseUrlFromApiBaseUrl(requestApiBaseUrl)
7026
+ });
7027
+ reply.code(201);
7028
+ return createCompanionPairingSession(pairingTransport, parsed);
7029
+ });
7030
+ app.get("/api/v1/health/companion-iroh", async (request) => {
7031
+ requireOperatorSession(request.headers, {
7032
+ route: "/api/v1/health/companion-iroh"
7033
+ });
7034
+ return {
7035
+ iroh: getCompanionIrohStatus()
7036
+ };
6892
7037
  });
6893
7038
  app.delete("/api/v1/health/pairing-sessions/:id", async (request, reply) => {
6894
7039
  const auth = requireOperatorSession(request.headers, {
@@ -3,6 +3,7 @@ import os from "node:os";
3
3
  import { promisify } from "node:util";
4
4
  import { Bonjour } from "bonjour-service";
5
5
  import { logForgeDebug } from "./debug.js";
6
+ import { companionIrohApiBaseUrlFromNodeId, companionIrohUiBaseUrlFromNodeId, getCompanionIrohStatus } from "./services/companion-iroh.js";
6
7
  const execFileAsync = promisify(execFile);
7
8
  export async function startForgeDiscoveryAdvertiser(options) {
8
9
  if (options.enabled === false ||
@@ -15,6 +16,8 @@ export async function startForgeDiscoveryAdvertiser(options) {
15
16
  uiBaseUrl: options.tailscaleUiBaseUrl,
16
17
  basePath
17
18
  });
19
+ const irohTransport = getCompanionIrohStatus();
20
+ const irohNodeId = irohTransport.pairPayload?.node_id;
18
21
  const bonjour = new Bonjour({}, (error) => {
19
22
  logForgeDebug(`[forge-discovery] ignored mDNS advertisement error: ${formatDiscoveryError(error)}`);
20
23
  });
@@ -31,6 +34,16 @@ export async function startForgeDiscoveryAdvertiser(options) {
31
34
  tsApiBaseUrl: tailscaleTargets.apiBaseUrl ?? "",
32
35
  tsUiBaseUrl: tailscaleTargets.uiBaseUrl ?? "",
33
36
  tsDnsName: tailscaleTargets.dnsName ?? "",
37
+ irohApiBaseUrl: irohNodeId
38
+ ? companionIrohApiBaseUrlFromNodeId(irohNodeId)
39
+ : "",
40
+ irohUiBaseUrl: irohNodeId
41
+ ? companionIrohUiBaseUrlFromNodeId(irohNodeId)
42
+ : "",
43
+ irohProvider: irohNodeId ? "forge-companion-iroh" : "",
44
+ irohNodeId: irohNodeId ?? "",
45
+ irohRelay: irohTransport.pairPayload?.relay ?? "",
46
+ irohAlpn: irohTransport.alpn ?? "",
34
47
  watchReady: "1"
35
48
  }
36
49
  });
@@ -109,6 +109,10 @@ export const createCompanionPairingSessionSchema = z.object({
109
109
  label: z.string().trim().default("Forge Companion"),
110
110
  userId: z.string().trim().nullable().optional(),
111
111
  expiresInMinutes: z.coerce.number().int().min(5).max(24 * 60).default(30),
112
+ transportMode: z
113
+ .enum(["iroh", "manual-http"])
114
+ .default("iroh")
115
+ .transform((mode) => mode),
112
116
  capabilities: z
113
117
  .array(z.enum([
114
118
  "healthkit.sleep",
@@ -1471,8 +1475,17 @@ export function updateMobileCompanionSourceState(payload) {
1471
1475
  }
1472
1476
  });
1473
1477
  }
1474
- export function createCompanionPairingSession(baseApiUrl, input) {
1478
+ export function createCompanionPairingSession(pairingTransport, input) {
1475
1479
  const parsed = createCompanionPairingSessionSchema.parse(input);
1480
+ const resolvedTransport = typeof pairingTransport === "string"
1481
+ ? {
1482
+ apiBaseUrl: pairingTransport,
1483
+ uiBaseUrl: null,
1484
+ transportMode: "manual-http",
1485
+ transport: undefined
1486
+ }
1487
+ : pairingTransport;
1488
+ const baseApiUrl = resolvedTransport.apiBaseUrl;
1476
1489
  const now = new Date();
1477
1490
  const userId = parsed.userId ?? "user_operator";
1478
1491
  const serializedCapabilities = JSON.stringify(parsed.capabilities);
@@ -1484,10 +1497,9 @@ export function createCompanionPairingSession(baseApiUrl, input) {
1484
1497
  FROM companion_pairing_sessions
1485
1498
  WHERE user_id = ?
1486
1499
  AND label = ?
1487
- AND api_base_url = ?
1488
1500
  AND capability_flags_json = ?
1489
1501
  AND status = 'pending'`)
1490
- .all(userId, parsed.label, baseApiUrl, serializedCapabilities);
1502
+ .all(userId, parsed.label, serializedCapabilities);
1491
1503
  if (stalePendingRows.length > 0) {
1492
1504
  revokePairingRows(stalePendingRows, {
1493
1505
  actor: null,
@@ -1510,6 +1522,9 @@ export function createCompanionPairingSession(baseApiUrl, input) {
1510
1522
  const qrPayload = {
1511
1523
  kind: "forge-companion-pairing",
1512
1524
  apiBaseUrl: baseApiUrl,
1525
+ uiBaseUrl: resolvedTransport.uiBaseUrl ?? undefined,
1526
+ transportMode: resolvedTransport.transportMode ?? parsed.transportMode,
1527
+ transport: resolvedTransport.transport,
1513
1528
  sessionId: id,
1514
1529
  pairingToken,
1515
1530
  expiresAt,
@@ -233,11 +233,26 @@ export const movementSelectionAggregateSchema = z.object({
233
233
  });
234
234
  export const movementSettingsPatchSchema = movementSettingsInputSchema.partial();
235
235
  const MOVEMENT_TIMELINE_MAX_LIMIT = 360;
236
+ const queryStringArraySchema = z.preprocess((value) => {
237
+ if (value === undefined || value === null) {
238
+ return [];
239
+ }
240
+ const rawValues = Array.isArray(value) ? value : [value];
241
+ return rawValues.flatMap((item) => {
242
+ if (typeof item !== "string") {
243
+ return [];
244
+ }
245
+ return item
246
+ .split(",")
247
+ .map((part) => part.trim())
248
+ .filter(Boolean);
249
+ });
250
+ }, z.array(z.string().trim().min(1)));
236
251
  export const movementTimelineQuerySchema = z.object({
237
252
  before: z.string().trim().min(1).optional(),
238
253
  limit: z.coerce.number().int().min(1).max(MOVEMENT_TIMELINE_MAX_LIMIT).default(40),
239
254
  includeInvalid: z.coerce.boolean().default(false),
240
- userIds: z.array(z.string().trim().min(1)).default([])
255
+ userIds: queryStringArraySchema
241
256
  });
242
257
  export const movementStayPatchSchema = z.object({
243
258
  label: z.string().trim().optional(),
@@ -3753,6 +3753,7 @@ export function buildOpenApiDocument() {
3753
3753
  "classification",
3754
3754
  "aliases",
3755
3755
  "summary",
3756
+ "methodRoutes",
3756
3757
  "readRoutes",
3757
3758
  "writeRoutes",
3758
3759
  "routeSelectionQuestions",
@@ -3765,6 +3766,10 @@ export function buildOpenApiDocument() {
3765
3766
  },
3766
3767
  aliases: arrayOf({ type: "string" }),
3767
3768
  summary: { type: "string" },
3769
+ methodRoutes: {
3770
+ type: "object",
3771
+ additionalProperties: { type: "string" }
3772
+ },
3768
3773
  readRoutes: {
3769
3774
  type: "object",
3770
3775
  additionalProperties: { type: "string" }
@@ -4085,7 +4090,8 @@ export function buildOpenApiDocument() {
4085
4090
  "specializedRouteToolRule",
4086
4091
  "createExample",
4087
4092
  "updateExample",
4088
- "specializedRouteToolExample"
4093
+ "specializedRouteToolExample",
4094
+ "specializedRouteToolExamples"
4089
4095
  ],
4090
4096
  properties: {
4091
4097
  preferredBatchRoutes: {
@@ -4111,7 +4117,11 @@ export function buildOpenApiDocument() {
4111
4117
  specializedRouteToolRule: { type: "string" },
4112
4118
  createExample: { type: "string" },
4113
4119
  updateExample: { type: "string" },
4114
- specializedRouteToolExample: { type: "string" }
4120
+ specializedRouteToolExample: { type: "string" },
4121
+ specializedRouteToolExamples: {
4122
+ type: "object",
4123
+ additionalProperties: { type: "string" }
4124
+ }
4115
4125
  }
4116
4126
  }
4117
4127
  }