forge-openclaw-plugin 0.2.60 → 0.2.61

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 (46) hide show
  1. package/README.md +93 -46
  2. package/dist/assets/{board-B1V3M__K.js → board-DThHV1D8.js} +1 -1
  3. package/dist/assets/index-7gvVCqnV.css +1 -0
  4. package/dist/assets/index-_Cn6Prym.js +90 -0
  5. package/dist/assets/{motion-CltSTItx.js → motion-BtTJtHCw.js} +1 -1
  6. package/dist/assets/{table-B-VrSFx8.js → table-Bnw6pcwN.js} +1 -1
  7. package/dist/assets/{ui-DUqM4jkt.js → ui-CnVxFkj0.js} +1 -1
  8. package/dist/assets/{vendor-C0otBhgu.js → vendor-BgZ3YrRd.js} +212 -207
  9. package/dist/gamification-previews/dark-fantasy-item-trophy-tasks-anvil-marathon.webp +0 -0
  10. package/dist/gamification-previews/dark-fantasy-item-trophy-xp-levels-the-first-heat.webp +0 -0
  11. package/dist/gamification-previews/dark-fantasy-item-unlock-streaks-molten-crown-fire.webp +0 -0
  12. package/dist/gamification-previews/dark-fantasy-mascot.webp +0 -0
  13. package/dist/gamification-previews/dramatic-smithie-item-trophy-tasks-anvil-marathon.webp +0 -0
  14. package/dist/gamification-previews/dramatic-smithie-item-trophy-xp-levels-the-first-heat.webp +0 -0
  15. package/dist/gamification-previews/dramatic-smithie-item-unlock-streaks-molten-crown-fire.webp +0 -0
  16. package/dist/gamification-previews/dramatic-smithie-mascot.webp +0 -0
  17. package/dist/gamification-previews/mind-locksmith-item-trophy-tasks-anvil-marathon.webp +0 -0
  18. package/dist/gamification-previews/mind-locksmith-item-trophy-xp-levels-the-first-heat.webp +0 -0
  19. package/dist/gamification-previews/mind-locksmith-item-unlock-streaks-molten-crown-fire.webp +0 -0
  20. package/dist/gamification-previews/mind-locksmith-mascot.webp +0 -0
  21. package/dist/index.html +7 -7
  22. package/dist/openclaw/parity.js +27 -0
  23. package/dist/openclaw/plugin-entry-shared.js +2 -2
  24. package/dist/openclaw/plugin-sdk-types.d.ts +2 -1
  25. package/dist/openclaw/routes.d.ts +4 -0
  26. package/dist/openclaw/routes.js +112 -3
  27. package/dist/openclaw/tools.js +32 -4
  28. package/dist/server/server/migrations/059_data_backup_retention.sql +2 -0
  29. package/dist/server/server/src/app.js +125 -43
  30. package/dist/server/server/src/data-management-types.js +2 -0
  31. package/dist/server/server/src/health.js +40 -0
  32. package/dist/server/server/src/openapi.js +398 -7
  33. package/dist/server/server/src/repositories/rewards.js +60 -0
  34. package/dist/server/server/src/services/data-management.js +32 -2
  35. package/dist/server/server/src/services/doctor.js +762 -0
  36. package/dist/server/server/src/services/gamification.js +75 -3
  37. package/dist/server/src/lib/api.js +9 -0
  38. package/dist/server/src/lib/gamification-catalog.js +1 -1
  39. package/openclaw.plugin.json +85 -3
  40. package/package.json +8 -4
  41. package/server/migrations/059_data_backup_retention.sql +2 -0
  42. package/skills/forge-openclaw/SKILL.md +38 -19
  43. package/skills/forge-openclaw/entity_conversation_playbooks.md +66 -8
  44. package/skills/forge-openclaw/psyche_entity_playbooks.md +23 -0
  45. package/dist/assets/index-BwKAPo98.css +0 -1
  46. package/dist/assets/index-Dy7c-dRY.js +0 -90
@@ -26,6 +26,7 @@ import { createProject, updateProject } from "./repositories/projects.js";
26
26
  import { createPreferenceCatalog, createPreferenceCatalogItem, createPreferenceContext, createPreferenceItem, createPreferenceItemFromEntity, deletePreferenceCatalog, deletePreferenceCatalogItem, deletePreferenceContext, deletePreferenceItem, getPreferenceCatalogById, getPreferenceCatalogItemById, getPreferenceContextById, getPreferenceItemById, getPreferenceWorkspace, listPreferenceCatalogItems, listPreferenceCatalogs, listPreferenceContexts, listPreferenceItems, mergePreferenceContexts, startPreferenceGame, submitAbsoluteSignal, submitPairwiseJudgment, updatePreferenceCatalog, updatePreferenceCatalogItem, updatePreferenceContext, updatePreferenceItem, updatePreferenceScore } from "./repositories/preferences.js";
27
27
  import { createStrategy, getStrategyById, listStrategies, updateStrategy } from "./repositories/strategies.js";
28
28
  import { buildKnowledgeGraph, buildKnowledgeGraphFocus } from "./services/knowledge-graph.js";
29
+ import { applyForgeDoctorFixes, buildForgeDoctorReport } from "./services/doctor.js";
29
30
  import { createManualRewardGrant, getRewardRuleById, listRewardLedger, listRewardRules, recordWorkAdjustmentReward, recordSessionEvent, updateRewardRule } from "./repositories/rewards.js";
30
31
  import { markGamificationCelebrationSeen } from "./repositories/gamification.js";
31
32
  import { getSettingsFileStatus, listAgentIdentities, getSettings, isPsycheAuthRequired, mirrorSettingsFileFromCurrentState, updateSettings, verifyAgentToken } from "./repositories/settings.js";
@@ -63,7 +64,7 @@ import { buildOpenApiDocument } from "./openapi.js";
63
64
  import { registerWebRoutes } from "./web.js";
64
65
  import { createManagerRuntime } from "./managers/runtime.js";
65
66
  import { isManagerError } from "./managers/type-guards.js";
66
- 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";
67
+ 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";
67
68
  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";
68
69
  import { getScreenTimeAllTimeSummary, getScreenTimeDayDetail, getScreenTimeMonthSummary, getScreenTimeSettings, screenTimeSettingsPatchSchema, updateScreenTimeSettings } from "./screen-time.js";
69
70
  import { assertWatchReady, buildWatchBootstrap, ingestWatchCaptureBatch, mobileWatchBootstrapSchema, mobileWatchCaptureBatchSchema, mobileWatchHabitCheckInSchema } from "./watch-mobile.js";
@@ -1956,7 +1957,7 @@ function buildPreferredMutationPath(entityType) {
1956
1957
  case "wiki_page":
1957
1958
  return "Use /api/v1/wiki/pages with POST or PATCH for page CRUD.";
1958
1959
  case "calendar_connection":
1959
- return "Use /api/v1/calendar/connections plus provider-specific setup flows.";
1960
+ return "Use /api/v1/calendar/discovery or /api/v1/calendar/macos-local/discovery before setup when needed; use /api/v1/calendar/connections with POST, PATCH, DELETE, rediscovery, and sync for connection lifecycle work.";
1960
1961
  case "task_run":
1961
1962
  return "Use the task-run action routes to start, heartbeat, focus, complete, or release live work.";
1962
1963
  case "questionnaire_run":
@@ -1991,7 +1992,7 @@ function buildPreferredMutationTool(entityType) {
1991
1992
  case "wiki_page":
1992
1993
  return "forge_upsert_wiki_page";
1993
1994
  case "calendar_connection":
1994
- return "forge_connect_calendar_provider | forge_sync_calendar_connection";
1995
+ return "forge_connect_calendar_provider | forge_sync_calendar_connection | mirrored calendar connection routes";
1995
1996
  case "task_run":
1996
1997
  return "forge_start_task_run | forge_heartbeat_task_run | forge_focus_task_run | forge_complete_task_run | forge_release_task_run";
1997
1998
  case "questionnaire_run":
@@ -3201,9 +3202,10 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
3201
3202
  useWhen: "Use for a lived direction, quality of being, or way of showing up that matters to the user and should guide actions rather than just describe an outcome.",
3202
3203
  coachingGoal: "Clarify the value as a chosen direction, distinguish it from a goal, and gather one concrete way the user wants to embody it now.",
3203
3204
  askSequence: [
3204
- "Start with what matters and why it matters now.",
3205
- "Ask for one concrete example of what living this value would look like in ordinary life.",
3206
- "Separate the value direction from any specific outcome or achievement goal.",
3205
+ "Start with one ordinary recent moment where the value felt alive, absent, or painfully important.",
3206
+ "Ask what mattered in that moment and why it matters now.",
3207
+ "Reflect the direction in the user's own language before separating it from any specific outcome or achievement goal.",
3208
+ "Ask what someone would see this week if the user lived this value a little more.",
3207
3209
  "Notice tensions, barriers, or situations where the value gets lost.",
3208
3210
  "Name one small committed action that would move toward the value."
3209
3211
  ],
@@ -3218,6 +3220,7 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
3218
3220
  "linkedTaskIds"
3219
3221
  ],
3220
3222
  exampleQuestions: [
3223
+ "Where did this value show up recently, even faintly?",
3221
3224
  "What feels deeply important about this to you?",
3222
3225
  "If you were living this value a little more this week, what would someone be able to see?",
3223
3226
  "What goal or area of life does this value belong to most clearly?",
@@ -3322,12 +3325,12 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
3322
3325
  useWhen: "Use for a belief, rule, or self-statement that keeps showing up in reactions, especially when the user can phrase it as a sentence.",
3323
3326
  coachingGoal: "Turn implicit self-talk or a likely schema theme into one explicit belief statement that can be tested and linked to patterns, reports, and modes without forcing the user into a debate too early.",
3324
3327
  askSequence: [
3325
- "Reflect the likely belief in the user's own words and ask for confirmation or correction.",
3326
- "Decide whether it is absolute or conditional.",
3327
- "Estimate how true it feels from 0 to 100.",
3328
- "Collect evidence for and evidence against.",
3329
- "Notice where the belief may have been learned or reinforced.",
3330
- "Offer a more flexible alternative belief.",
3328
+ "Anchor the belief in one recent moment or reaction before abstracting it.",
3329
+ "Reflect the likely belief sentence in the user's own words and ask for confirmation or correction.",
3330
+ "Condense it into one saveable statement only after the wording feels accurate.",
3331
+ "Decide whether it is absolute or conditional after the sentence lands.",
3332
+ "Estimate how true it feels from 0 to 100 only if that helps the user understand its grip or guide later review.",
3333
+ "Explore evidence, origin, and a flexible alternative only if the user wants to examine or soften the belief now.",
3331
3334
  "Link a schemaId only when a real schema catalog match is known."
3332
3335
  ],
3333
3336
  requiredForCreate: ["statement", "beliefType"],
@@ -3343,6 +3346,7 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
3343
3346
  "linkedModeIds"
3344
3347
  ],
3345
3348
  exampleQuestions: [
3349
+ "When did this belief show up most recently?",
3346
3350
  "If we turned that reaction into one sentence, what would it sound like?",
3347
3351
  "When that reaction hits, what does it start telling you?",
3348
3352
  "Is it more of an always/never belief, or an if-then rule?",
@@ -3355,6 +3359,7 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
3355
3359
  "Schema catalog entries are reference concepts; belief_entry is the user-owned record.",
3356
3360
  "If no schema catalog match is known, omit schemaId rather than inventing one.",
3357
3361
  "Do not argue the user out of the belief. Reflect it, understand its function, and then collaboratively test for flexibility.",
3362
+ "Do not rush to confidence, evidence, or flexible alternatives before the user feels the belief has been captured.",
3358
3363
  "When the wording is nearly there, ask whether it feels true enough before you move into confidence, evidence, or alternative-belief details.",
3359
3364
  "A useful hypothesis should name the rule or prediction the moment seems to activate and then invite correction before saving it as the belief sentence."
3360
3365
  ]
@@ -3400,6 +3405,7 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
3400
3405
  "Mode profiles are durable parts descriptions.",
3401
3406
  "Mode guide sessions are the guided reasoning process that can lead toward a mode profile.",
3402
3407
  "Do not overpathologize. The point is to understand the part's job and cost, then increase choice.",
3408
+ "Do not start by asking for the mode family; choose the family after the lived description, protective job, fear, or burden is visible enough.",
3403
3409
  "If the user asks to understand the mode first, start from a recent moment and ask what the part is trying to do before you name it.",
3404
3410
  "When enough evidence is present, offer one tentative hypothesis about the mode's protective job, fear, or burden before choosing the family label."
3405
3411
  ]
@@ -3436,12 +3442,13 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
3436
3442
  useWhen: "Use for one specific emotionally meaningful incident that should be mapped from situation through emotions, thoughts, behaviors, consequences, and next moves.",
3437
3443
  coachingGoal: "Help the user build a clear incident chain with enough structure to learn from one episode while staying grounded and not rushing past the user's felt experience.",
3438
3444
  askSequence: [
3439
- "Name the incident briefly and anchor it in one concrete sequence.",
3440
- "Describe what happened in the situation.",
3441
- "Capture emotions and intensity.",
3445
+ "Start with the situation and felt stake: what happened, and why did it hit enough to save.",
3446
+ "Name the incident briefly only after the concrete sequence is clear.",
3447
+ "Capture emotions, body state, and intensity before moving into interpretation.",
3442
3448
  "Capture thoughts, meanings, or belief-linked interpretations.",
3443
- "Capture behaviors and immediate coping moves.",
3444
- "Capture short-term and long-term consequences.",
3449
+ "Capture behaviors, urges, and immediate coping moves.",
3450
+ "Capture what the move did short term and what it cost or changed later.",
3451
+ "Offer one careful hypothesis about the sequence only after situation, emotion, meaning, behavior, and consequence are partly visible.",
3445
3452
  "Identify next moves and linked patterns, beliefs, modes, values, or tasks."
3446
3453
  ],
3447
3454
  requiredForCreate: ["title"],
@@ -3473,6 +3480,7 @@ const AGENT_ONBOARDING_PSYCHE_PLAYBOOKS = [
3473
3480
  notes: [
3474
3481
  "Use eventTypeId only when a known event taxonomy item fits; otherwise use customEventType.",
3475
3482
  "Use emotionDefinitionId only when a known emotion definition fits; otherwise keep the raw label.",
3483
+ "Do not turn the report into a worksheet dump before the felt stake is clear; reflect what made the episode matter before asking for the full chain.",
3476
3484
  "If the user becomes overwhelmed, slow down, summarize, and return to one segment of the chain at a time instead of pushing for the full report in one turn.",
3477
3485
  "Only hypothesize about the incident sequence after the situation, emotion, meaning, behavior, and consequence are at least partly visible."
3478
3486
  ]
@@ -4230,8 +4238,12 @@ function buildAgentOnboardingPayload(request) {
4230
4238
  },
4231
4239
  calendar_connection: {
4232
4240
  list: "/api/v1/calendar/connections",
4241
+ discover: "/api/v1/calendar/discovery",
4242
+ discoverMacOSLocal: "/api/v1/calendar/macos-local/discovery",
4243
+ rediscover: "/api/v1/calendar/connections/:id/discovery",
4233
4244
  create: "/api/v1/calendar/connections",
4234
4245
  update: "/api/v1/calendar/connections/:id",
4246
+ sync: "/api/v1/calendar/connections/:id/sync",
4235
4247
  delete: "/api/v1/calendar/connections/:id"
4236
4248
  }
4237
4249
  },
@@ -4305,6 +4317,7 @@ function buildAgentOnboardingPayload(request) {
4305
4317
  specializedDomainSurfaces: {
4306
4318
  movement: {
4307
4319
  classification: "specialized_domain_surface",
4320
+ aliases: ["movement", "Movement"],
4308
4321
  summary: "Dedicated movement workspace API. Use these routes for stays, trips, time-in-place questions, visited places, trip detail, selection aggregates, user-defined overlays, and repair actions on already-recorded movement data.",
4309
4322
  routeSelectionQuestions: [
4310
4323
  "Is the user asking for a day, month, all-time, timeline, place, trip detail, or selected-span answer?",
@@ -4348,6 +4361,7 @@ function buildAgentOnboardingPayload(request) {
4348
4361
  },
4349
4362
  lifeForce: {
4350
4363
  classification: "specialized_domain_surface",
4364
+ aliases: ["life_force", "life-force", "Life Force"],
4351
4365
  summary: "Dedicated life-force API. Use it to read the current energy budget, drains, recommendations, and warnings, then patch only the parts that are meant to be user-controlled.",
4352
4366
  routeSelectionQuestions: [
4353
4367
  "Is the user trying to understand the overview, change durable profile assumptions, change a weekday curve, or log a right-now fatigue signal?",
@@ -4371,11 +4385,39 @@ function buildAgentOnboardingPayload(request) {
4371
4385
  "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."
4372
4386
  ]
4373
4387
  },
4388
+ life_force: {
4389
+ classification: "specialized_domain_surface",
4390
+ aliases: ["lifeForce", "life-force", "Life Force"],
4391
+ summary: "Alias for the dedicated Life Force API keyed to the entity-style name `life_force`. Use the same overview, profile, weekday-template, and fatigue-signal routes as `lifeForce`.",
4392
+ routeSelectionQuestions: [
4393
+ "Is the user trying to understand the overview, change durable profile assumptions, change a weekday curve, or log a right-now fatigue signal?",
4394
+ "Are they describing a repeatable weekly shape or a one-off current state?",
4395
+ "If the lane is already clear, what one weekday, profile field, or signal detail is still missing?"
4396
+ ],
4397
+ readRoutes: {
4398
+ overview: "/api/v1/life-force"
4399
+ },
4400
+ writeRoutes: {
4401
+ profile: "/api/v1/life-force/profile",
4402
+ weekdayTemplate: "/api/v1/life-force/templates/:weekday",
4403
+ fatigueSignal: "/api/v1/life-force/fatigue-signals"
4404
+ },
4405
+ notes: [
4406
+ "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
+ "Life Force is a focused domain surface, not a batch CRUD entity type.",
4408
+ "Use GET /api/v1/life-force for the current overview payload with stats, drains, recommendations, and current-curve state.",
4409
+ "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
+ "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.",
4411
+ "If the user is asking what changed after a profile, template, or fatigue write, read the overview back so the effect stays visible.",
4412
+ "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."
4413
+ ]
4414
+ },
4374
4415
  workbench: {
4375
4416
  classification: "specialized_domain_surface",
4417
+ aliases: ["workbench", "Workbench"],
4376
4418
  summary: "Dedicated graph-flow API. Use it for flow catalog reads, flow CRUD, execution, run history, published outputs, node results, and latest successful node outputs.",
4377
4419
  routeSelectionQuestions: [
4378
- "Is the job flow discovery, flow editing, execution, published output, run detail, node result, latest node output, or flow chat follow-up?",
4420
+ "Is the job flow discovery, flow editing, execution, run history, published output, run detail, node result, latest node output, or flow chat follow-up?",
4379
4421
  "Does the user need a stable public contract or one execution artifact?",
4380
4422
  "If the flow is already known, what one run, node, or output scope detail is still missing before acting?"
4381
4423
  ],
@@ -6377,6 +6419,7 @@ export async function buildServer(options = {}) {
6377
6419
  storageRoot: getEffectiveDataRoot(),
6378
6420
  dataDir: resolveDataDir(),
6379
6421
  databasePath: resolveDatabasePathForDataRoot(),
6422
+ port: runtimeConfig.port,
6380
6423
  basePath: runtimeConfig.basePath,
6381
6424
  devWebOrigin: process.env.FORGE_DEV_WEB_ORIGIN?.trim() || null
6382
6425
  };
@@ -6385,34 +6428,72 @@ export async function buildServer(options = {}) {
6385
6428
  backend: "forge-node-runtime",
6386
6429
  runtime
6387
6430
  });
6388
- const warnings = [];
6389
- if (!settingsFile.valid) {
6390
- warnings.push(`forge.json is invalid at ${settingsFile.path}. Forge ignored file precedence until the JSON is repaired or rewritten.`);
6391
- }
6392
- if (settingsFile.syncState === "applied_file_overrides") {
6393
- warnings.push("forge.json overrode one or more persisted database settings on this run.");
6394
- }
6395
- if (health.ok === false) {
6396
- warnings.push("The task-run watchdog reported degraded health.");
6397
- }
6398
6431
  return {
6399
- doctor: {
6400
- ok: health.ok && settingsFile.valid,
6401
- now: new Date().toISOString(),
6432
+ doctor: await buildForgeDoctorReport({
6433
+ settings,
6434
+ settingsFile,
6402
6435
  runtime,
6403
- health,
6436
+ health
6437
+ })
6438
+ };
6439
+ });
6440
+ app.post("/api/v1/doctor/fixes", async (request) => {
6441
+ requireScopedAccess(request.headers, ["write"], { route: "/api/v1/doctor/fixes" });
6442
+ const parsed = z
6443
+ .object({
6444
+ fixIds: z.array(z.string().min(1)).optional(),
6445
+ applyAllSafe: z.boolean().optional()
6446
+ })
6447
+ .parse(request.body ?? {});
6448
+ const initialSettings = getSettings();
6449
+ const initialSettingsFile = getSettingsFileStatus();
6450
+ const initialRuntime = {
6451
+ pid: process.pid,
6452
+ storageRoot: getEffectiveDataRoot(),
6453
+ dataDir: resolveDataDir(),
6454
+ databasePath: resolveDatabasePathForDataRoot(),
6455
+ port: runtimeConfig.port,
6456
+ basePath: runtimeConfig.basePath,
6457
+ devWebOrigin: process.env.FORGE_DEV_WEB_ORIGIN?.trim() || null
6458
+ };
6459
+ const initialHealth = buildHealthPayload(taskRunWatchdog, {
6460
+ apiVersion: "v1",
6461
+ backend: "forge-node-runtime",
6462
+ runtime: initialRuntime
6463
+ });
6464
+ const initialDoctor = await buildForgeDoctorReport({
6465
+ settings: initialSettings,
6466
+ settingsFile: initialSettingsFile,
6467
+ runtime: initialRuntime,
6468
+ health: initialHealth
6469
+ });
6470
+ const fixResult = applyForgeDoctorFixes(parsed, {
6471
+ integrityScore: initialDoctor.integrity.score
6472
+ });
6473
+ const settings = getSettings();
6474
+ const settingsFile = getSettingsFileStatus();
6475
+ const runtime = {
6476
+ pid: process.pid,
6477
+ storageRoot: getEffectiveDataRoot(),
6478
+ dataDir: resolveDataDir(),
6479
+ databasePath: resolveDatabasePathForDataRoot(),
6480
+ port: runtimeConfig.port,
6481
+ basePath: runtimeConfig.basePath,
6482
+ devWebOrigin: process.env.FORGE_DEV_WEB_ORIGIN?.trim() || null
6483
+ };
6484
+ const health = buildHealthPayload(taskRunWatchdog, {
6485
+ apiVersion: "v1",
6486
+ backend: "forge-node-runtime",
6487
+ runtime
6488
+ });
6489
+ return {
6490
+ ...fixResult,
6491
+ doctor: await buildForgeDoctorReport({
6492
+ settings,
6404
6493
  settingsFile,
6405
- settingsSummary: {
6406
- themePreference: settings.themePreference,
6407
- localePreference: settings.localePreference,
6408
- operatorName: settings.profile.operatorName,
6409
- maxActiveTasks: settings.execution.maxActiveTasks,
6410
- timeAccountingMode: settings.execution.timeAccountingMode,
6411
- psycheAuthRequired: settings.security.psycheAuthRequired,
6412
- webAppUrl: `http://127.0.0.1:${runtimeConfig.port}${runtimeConfig.basePath}`
6413
- },
6414
- warnings
6415
- }
6494
+ runtime,
6495
+ health
6496
+ })
6416
6497
  };
6417
6498
  });
6418
6499
  app.get("/api/v1/auth/operator-session", async (request, reply) => ({
@@ -6853,6 +6934,7 @@ export async function buildServer(options = {}) {
6853
6934
  app.post("/api/v1/mobile/pairing/verify", async (request) => ({
6854
6935
  pairing: verifyCompanionPairing(verifyCompanionPairingSchema.parse(request.body ?? {}))
6855
6936
  }));
6937
+ app.post("/api/v1/mobile/pairing/heartbeat", async (request) => heartbeatCompanionPairing(heartbeatCompanionPairingSchema.parse(request.body ?? {})));
6856
6938
  app.post("/api/v1/mobile/movement/bootstrap", async (request) => {
6857
6939
  const parsed = movementMobileBootstrapSchema.parse(request.body ?? {});
6858
6940
  const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
@@ -75,6 +75,7 @@ export const dataManagementSettingsSchema = z.object({
75
75
  preferredDataRoot: z.string(),
76
76
  backupDirectory: z.string(),
77
77
  backupFrequencyHours: z.number().int().positive().nullable(),
78
+ backupRetentionDays: z.number().int().positive().nullable(),
78
79
  autoRepairEnabled: z.boolean(),
79
80
  lastAutoBackupAt: z.string().nullable(),
80
81
  lastManualBackupAt: z.string().nullable()
@@ -89,6 +90,7 @@ export const dataManagementStateSchema = z.object({
89
90
  export const updateDataManagementSettingsSchema = z.object({
90
91
  backupDirectory: z.string().trim().optional(),
91
92
  backupFrequencyHours: z.number().int().positive().nullable().optional(),
93
+ backupRetentionDays: z.number().int().positive().nullable().optional(),
92
94
  autoRepairEnabled: z.boolean().optional()
93
95
  });
94
96
  export const createDataBackupSchema = z.object({
@@ -260,6 +260,16 @@ export const verifyCompanionPairingSchema = z.object({
260
260
  sourceDevice: z.string().trim().default("iPhone")
261
261
  })
262
262
  });
263
+ export const heartbeatCompanionPairingSchema = z.object({
264
+ sessionId: z.string().trim().min(1),
265
+ pairingToken: z.string().trim().min(1),
266
+ device: z.object({
267
+ name: z.string().trim().default("iPhone"),
268
+ platform: z.string().trim().default("ios"),
269
+ appVersion: z.string().trim().default(""),
270
+ sourceDevice: z.string().trim().default("iPhone")
271
+ })
272
+ });
263
273
  export const updateWorkoutMetadataSchema = z.object({
264
274
  subjectiveEffort: z.number().int().min(1).max(10).nullable().optional(),
265
275
  moodBefore: z.string().trim().optional(),
@@ -1555,6 +1565,36 @@ export function verifyCompanionPairing(payload) {
1555
1565
  .get(pairing.id))
1556
1566
  };
1557
1567
  }
1568
+ export function heartbeatCompanionPairing(payload) {
1569
+ const parsed = heartbeatCompanionPairingSchema.parse(payload);
1570
+ const pairing = requireValidPairing(parsed.sessionId, parsed.pairingToken);
1571
+ const now = nowIso();
1572
+ const renewedExpiry = nextVerifiedCompanionPairingExpiry(new Date());
1573
+ const nextStatus = pairing.status === "revoked"
1574
+ ? pairing.status
1575
+ : pairing.status === "permission_denied"
1576
+ ? pairing.status
1577
+ : pairing.status === "stale"
1578
+ ? "paired"
1579
+ : pairing.status === "pending"
1580
+ ? "paired"
1581
+ : pairing.status;
1582
+ getDatabase()
1583
+ .prepare(`UPDATE companion_pairing_sessions
1584
+ SET status = ?, device_name = ?, platform = ?, app_version = ?,
1585
+ last_seen_at = ?, paired_at = COALESCE(paired_at, ?),
1586
+ expires_at = ?, updated_at = ?
1587
+ WHERE id = ?`)
1588
+ .run(nextStatus, parsed.device.name, parsed.device.platform, parsed.device.appVersion, now, now, renewedExpiry, now, pairing.id);
1589
+ ensurePairingSourceStates(getDatabase()
1590
+ .prepare(`SELECT * FROM companion_pairing_sessions WHERE id = ?`)
1591
+ .get(pairing.id));
1592
+ return {
1593
+ pairingSession: mapPairingSession(getDatabase()
1594
+ .prepare(`SELECT * FROM companion_pairing_sessions WHERE id = ?`)
1595
+ .get(pairing.id))
1596
+ };
1597
+ }
1558
1598
  export function requireValidPairing(sessionId, pairingToken) {
1559
1599
  const row = getDatabase()
1560
1600
  .prepare(`SELECT * FROM companion_pairing_sessions WHERE id = ?`)