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
@@ -28,6 +28,13 @@ const HTTP_METHODS = new Set([
28
28
  "options",
29
29
  "head"
30
30
  ]);
31
+ const CALENDAR_PROVIDER_VALUES = [
32
+ "google",
33
+ "apple",
34
+ "microsoft",
35
+ "caldav",
36
+ "macos_local"
37
+ ];
31
38
  const API_TAGS = [
32
39
  {
33
40
  name: "Meta",
@@ -710,7 +717,7 @@ export function buildOpenApiDocument() {
710
717
  ],
711
718
  properties: {
712
719
  id: { type: "string" },
713
- provider: { type: "string", enum: ["google", "apple", "caldav"] },
720
+ provider: { type: "string", enum: CALENDAR_PROVIDER_VALUES },
714
721
  label: { type: "string" },
715
722
  accountLabel: { type: "string" },
716
723
  status: {
@@ -725,6 +732,134 @@ export function buildOpenApiDocument() {
725
732
  updatedAt: { type: "string", format: "date-time" }
726
733
  }
727
734
  };
735
+ const calendarConnectionMutationInput = {
736
+ type: "object",
737
+ additionalProperties: true,
738
+ required: ["provider", "label"],
739
+ properties: {
740
+ provider: { type: "string", enum: CALENDAR_PROVIDER_VALUES },
741
+ label: { type: "string" },
742
+ username: { type: "string" },
743
+ password: { type: "string" },
744
+ serverUrl: { type: "string" },
745
+ authSessionId: { type: "string" },
746
+ sourceId: { type: "string" },
747
+ selectedCalendarUrls: arrayOf({ type: "string" }),
748
+ forgeCalendarUrl: nullable({ type: "string" }),
749
+ createForgeCalendar: { type: "boolean" },
750
+ replaceConnectionIds: arrayOf({ type: "string" })
751
+ }
752
+ };
753
+ const calendarConnectionPatchInput = {
754
+ type: "object",
755
+ additionalProperties: false,
756
+ properties: {
757
+ label: { type: "string" },
758
+ selectedCalendarUrls: arrayOf({ type: "string" })
759
+ }
760
+ };
761
+ const calendarDiscoveryInput = {
762
+ type: "object",
763
+ additionalProperties: true,
764
+ required: ["provider"],
765
+ properties: {
766
+ provider: { type: "string", enum: ["apple", "caldav"] },
767
+ serverUrl: { type: "string" },
768
+ username: { type: "string" },
769
+ password: { type: "string" }
770
+ }
771
+ };
772
+ const calendarDiscoveryCalendar = {
773
+ type: "object",
774
+ additionalProperties: true,
775
+ required: [
776
+ "url",
777
+ "displayName",
778
+ "description",
779
+ "color",
780
+ "timezone",
781
+ "isPrimary",
782
+ "canWrite",
783
+ "selectedByDefault",
784
+ "isForgeCandidate"
785
+ ],
786
+ properties: {
787
+ url: { type: "string" },
788
+ displayName: { type: "string" },
789
+ dedupedName: { type: "string" },
790
+ description: { type: "string" },
791
+ color: { type: "string" },
792
+ timezone: { type: "string" },
793
+ isPrimary: { type: "boolean" },
794
+ canWrite: { type: "boolean" },
795
+ selectedByDefault: { type: "boolean" },
796
+ isForgeCandidate: { type: "boolean" },
797
+ sourceId: nullable({ type: "string" }),
798
+ sourceTitle: nullable({ type: "string" }),
799
+ sourceType: nullable({ type: "string" }),
800
+ calendarType: nullable({ type: "string" }),
801
+ hostCalendarId: nullable({ type: "string" }),
802
+ canonicalKey: nullable({ type: "string" })
803
+ }
804
+ };
805
+ const calendarDiscoveryPayload = {
806
+ type: "object",
807
+ additionalProperties: true,
808
+ required: [
809
+ "provider",
810
+ "accountLabel",
811
+ "serverUrl",
812
+ "principalUrl",
813
+ "homeUrl",
814
+ "calendars"
815
+ ],
816
+ properties: {
817
+ provider: { type: "string", enum: CALENDAR_PROVIDER_VALUES },
818
+ accountLabel: { type: "string" },
819
+ serverUrl: { type: "string" },
820
+ principalUrl: nullable({ type: "string" }),
821
+ homeUrl: nullable({ type: "string" }),
822
+ calendars: arrayOf(calendarDiscoveryCalendar)
823
+ }
824
+ };
825
+ const macOSLocalCalendarDiscoveryPayload = {
826
+ type: "object",
827
+ additionalProperties: true,
828
+ required: ["status", "requestedAt", "sources"],
829
+ properties: {
830
+ status: {
831
+ type: "string",
832
+ enum: [
833
+ "not_determined",
834
+ "denied",
835
+ "restricted",
836
+ "full_access",
837
+ "unavailable"
838
+ ]
839
+ },
840
+ requestedAt: { type: "string", format: "date-time" },
841
+ sources: arrayOf({
842
+ type: "object",
843
+ additionalProperties: true,
844
+ required: [
845
+ "sourceId",
846
+ "sourceTitle",
847
+ "sourceType",
848
+ "accountLabel",
849
+ "accountIdentityKey",
850
+ "calendars"
851
+ ],
852
+ properties: {
853
+ sourceId: { type: "string" },
854
+ sourceTitle: { type: "string" },
855
+ sourceType: { type: "string" },
856
+ accountLabel: { type: "string" },
857
+ accountIdentityKey: { type: "string" },
858
+ calendars: arrayOf(calendarDiscoveryCalendar)
859
+ }
860
+ })
861
+ }
862
+ };
728
863
  const calendarResource = {
729
864
  type: "object",
730
865
  additionalProperties: false,
@@ -781,7 +916,7 @@ export function buildOpenApiDocument() {
781
916
  ],
782
917
  properties: {
783
918
  id: { type: "string" },
784
- provider: { type: "string", enum: ["google", "apple", "caldav"] },
919
+ provider: { type: "string", enum: CALENDAR_PROVIDER_VALUES },
785
920
  connectionId: nullable({ type: "string" }),
786
921
  calendarId: nullable({ type: "string" }),
787
922
  remoteCalendarId: nullable({ type: "string" }),
@@ -3218,6 +3353,95 @@ export function buildOpenApiDocument() {
3218
3353
  agentTokens: arrayOf({ $ref: "#/components/schemas/AgentTokenSummary" })
3219
3354
  }
3220
3355
  };
3356
+ const doctorFixProposal = {
3357
+ type: "object",
3358
+ additionalProperties: false,
3359
+ required: ["id", "kind", "title", "description", "requiresConfirmation"],
3360
+ properties: {
3361
+ id: { type: "string" },
3362
+ kind: { type: "string", enum: ["manual", "safe_auto_fix"] },
3363
+ title: { type: "string" },
3364
+ description: { type: "string" },
3365
+ requiresConfirmation: { type: "boolean" }
3366
+ }
3367
+ };
3368
+ const doctorCheck = {
3369
+ type: "object",
3370
+ additionalProperties: false,
3371
+ required: [
3372
+ "id",
3373
+ "group",
3374
+ "title",
3375
+ "status",
3376
+ "severity",
3377
+ "summary",
3378
+ "evidence",
3379
+ "affectedCount"
3380
+ ],
3381
+ properties: {
3382
+ id: { type: "string" },
3383
+ group: { type: "string" },
3384
+ title: { type: "string" },
3385
+ status: { type: "string", enum: ["pass", "warn", "fail", "skipped"] },
3386
+ severity: { type: "string", enum: ["info", "warning", "error"] },
3387
+ summary: { type: "string" },
3388
+ evidence: arrayOf({ type: "string" }),
3389
+ affectedCount: { type: "integer" },
3390
+ fix: { $ref: "#/components/schemas/DoctorFixProposal" }
3391
+ }
3392
+ };
3393
+ const forgeDoctorReport = {
3394
+ type: "object",
3395
+ additionalProperties: true,
3396
+ required: [
3397
+ "ok",
3398
+ "now",
3399
+ "integrity",
3400
+ "runtime",
3401
+ "health",
3402
+ "settingsFile",
3403
+ "settingsSummary",
3404
+ "checks",
3405
+ "issues",
3406
+ "fixProposals",
3407
+ "warnings"
3408
+ ],
3409
+ properties: {
3410
+ ok: { type: "boolean" },
3411
+ now: { type: "string", format: "date-time" },
3412
+ integrity: {
3413
+ type: "object",
3414
+ additionalProperties: true,
3415
+ required: ["score", "status", "headline", "lastCheckedAt"],
3416
+ properties: {
3417
+ score: { type: "integer" },
3418
+ status: { type: "string", enum: ["healthy", "warning", "critical"] },
3419
+ headline: { type: "string" },
3420
+ lastCheckedAt: { type: "string", format: "date-time" }
3421
+ }
3422
+ },
3423
+ runtime: { type: "object", additionalProperties: true },
3424
+ health: { type: "object", additionalProperties: true },
3425
+ settingsFile: { type: "object", additionalProperties: true },
3426
+ settingsSummary: { type: "object", additionalProperties: true },
3427
+ checks: arrayOf({ $ref: "#/components/schemas/DoctorCheck" }),
3428
+ issues: arrayOf({ $ref: "#/components/schemas/DoctorCheck" }),
3429
+ fixProposals: arrayOf({
3430
+ $ref: "#/components/schemas/DoctorFixProposal"
3431
+ }),
3432
+ warnings: arrayOf({ type: "string" })
3433
+ }
3434
+ };
3435
+ const doctorFixResult = {
3436
+ type: "object",
3437
+ additionalProperties: false,
3438
+ required: ["fixId", "status", "summary"],
3439
+ properties: {
3440
+ fixId: { type: "string" },
3441
+ status: { type: "string", enum: ["applied", "skipped", "failed"] },
3442
+ summary: { type: "string" }
3443
+ }
3444
+ };
3221
3445
  const agentOnboardingPayload = {
3222
3446
  type: "object",
3223
3447
  additionalProperties: false,
@@ -3527,6 +3751,7 @@ export function buildOpenApiDocument() {
3527
3751
  additionalProperties: false,
3528
3752
  required: [
3529
3753
  "classification",
3754
+ "aliases",
3530
3755
  "summary",
3531
3756
  "readRoutes",
3532
3757
  "writeRoutes",
@@ -3538,6 +3763,7 @@ export function buildOpenApiDocument() {
3538
3763
  type: "string",
3539
3764
  enum: ["specialized_domain_surface"]
3540
3765
  },
3766
+ aliases: arrayOf({ type: "string" }),
3541
3767
  summary: { type: "string" },
3542
3768
  readRoutes: {
3543
3769
  type: "object",
@@ -4674,6 +4900,11 @@ export function buildOpenApiDocument() {
4674
4900
  Project: project,
4675
4901
  CalendarSchedulingRules: calendarSchedulingRules,
4676
4902
  CalendarConnection: calendarConnection,
4903
+ CalendarConnectionMutationInput: calendarConnectionMutationInput,
4904
+ CalendarConnectionPatchInput: calendarConnectionPatchInput,
4905
+ CalendarDiscoveryInput: calendarDiscoveryInput,
4906
+ CalendarDiscoveryPayload: calendarDiscoveryPayload,
4907
+ MacOSLocalCalendarDiscoveryPayload: macOSLocalCalendarDiscoveryPayload,
4677
4908
  CalendarResource: calendarResource,
4678
4909
  CalendarEventSource: calendarEventSource,
4679
4910
  CalendarEventLink: calendarEventLink,
@@ -4703,6 +4934,10 @@ export function buildOpenApiDocument() {
4703
4934
  InsightsPayload: insightsPayload,
4704
4935
  WeeklyReviewPayload: weeklyReviewPayload,
4705
4936
  SettingsPayload: settingsPayload,
4937
+ DoctorFixProposal: doctorFixProposal,
4938
+ DoctorCheck: doctorCheck,
4939
+ ForgeDoctorReport: forgeDoctorReport,
4940
+ DoctorFixResult: doctorFixResult,
4706
4941
  ExecutionSettings: executionSettings,
4707
4942
  TaskRunClaimInput: taskRunClaimInput,
4708
4943
  TaskRunHeartbeatInput: taskRunHeartbeatInput,
@@ -4812,6 +5047,52 @@ export function buildOpenApiDocument() {
4812
5047
  }
4813
5048
  }
4814
5049
  },
5050
+ "/api/v1/doctor": {
5051
+ get: {
5052
+ summary: "Run Forge Doctor diagnostics for runtime, settings, storage, entities, hierarchy, rewards, and fix proposals",
5053
+ responses: {
5054
+ "200": jsonResponse({
5055
+ type: "object",
5056
+ required: ["doctor"],
5057
+ properties: {
5058
+ doctor: { $ref: "#/components/schemas/ForgeDoctorReport" }
5059
+ }
5060
+ }, "Forge Doctor report")
5061
+ }
5062
+ }
5063
+ },
5064
+ "/api/v1/doctor/fixes": {
5065
+ post: {
5066
+ summary: "Apply explicitly requested safe Forge Doctor fixes",
5067
+ requestBody: {
5068
+ required: true,
5069
+ content: {
5070
+ "application/json": {
5071
+ schema: {
5072
+ type: "object",
5073
+ additionalProperties: false,
5074
+ properties: {
5075
+ fixIds: arrayOf({ type: "string" }),
5076
+ applyAllSafe: { type: "boolean" }
5077
+ }
5078
+ }
5079
+ }
5080
+ }
5081
+ },
5082
+ responses: {
5083
+ "200": jsonResponse({
5084
+ type: "object",
5085
+ required: ["results", "doctor"],
5086
+ properties: {
5087
+ results: arrayOf({
5088
+ $ref: "#/components/schemas/DoctorFixResult"
5089
+ }),
5090
+ doctor: { $ref: "#/components/schemas/ForgeDoctorReport" }
5091
+ }
5092
+ }, "Forge Doctor fix result")
5093
+ }
5094
+ }
5095
+ },
4815
5096
  "/api/v1/health/sleep": {
4816
5097
  get: {
4817
5098
  summary: "Read the Forge sleep overview surface",
@@ -7555,6 +7836,50 @@ export function buildOpenApiDocument() {
7555
7836
  }
7556
7837
  }
7557
7838
  },
7839
+ "/api/v1/calendar/macos-local/discovery": {
7840
+ get: {
7841
+ summary: "Discover calendars already configured on this Mac through EventKit",
7842
+ responses: {
7843
+ "200": jsonResponse({
7844
+ type: "object",
7845
+ required: ["discovery"],
7846
+ properties: {
7847
+ discovery: {
7848
+ $ref: "#/components/schemas/MacOSLocalCalendarDiscoveryPayload"
7849
+ }
7850
+ }
7851
+ }, "macOS local calendar discovery"),
7852
+ default: { $ref: "#/components/responses/Error" }
7853
+ }
7854
+ }
7855
+ },
7856
+ "/api/v1/calendar/discovery": {
7857
+ post: {
7858
+ summary: "Discover Apple or custom CalDAV calendars before creating a connection",
7859
+ requestBody: {
7860
+ required: true,
7861
+ content: {
7862
+ "application/json": {
7863
+ schema: {
7864
+ $ref: "#/components/schemas/CalendarDiscoveryInput"
7865
+ }
7866
+ }
7867
+ }
7868
+ },
7869
+ responses: {
7870
+ "200": jsonResponse({
7871
+ type: "object",
7872
+ required: ["discovery"],
7873
+ properties: {
7874
+ discovery: {
7875
+ $ref: "#/components/schemas/CalendarDiscoveryPayload"
7876
+ }
7877
+ }
7878
+ }, "Calendar discovery"),
7879
+ default: { $ref: "#/components/responses/Error" }
7880
+ }
7881
+ }
7882
+ },
7558
7883
  "/api/v1/calendar/connections": {
7559
7884
  get: {
7560
7885
  summary: "List connected calendar providers",
@@ -7573,10 +7898,7 @@ export function buildOpenApiDocument() {
7573
7898
  "connectionHelp"
7574
7899
  ],
7575
7900
  properties: {
7576
- provider: {
7577
- type: "string",
7578
- enum: ["google", "apple", "caldav"]
7579
- },
7901
+ provider: { type: "string", enum: CALENDAR_PROVIDER_VALUES },
7580
7902
  label: { type: "string" },
7581
7903
  supportsDedicatedForgeCalendar: { type: "boolean" },
7582
7904
  connectionHelp: { type: "string" }
@@ -7591,8 +7913,18 @@ export function buildOpenApiDocument() {
7591
7913
  }
7592
7914
  },
7593
7915
  post: {
7594
- summary: "Create a Google, Apple, or custom CalDAV calendar connection",
7916
+ summary: "Create a Google, Apple, Exchange Online, local Mac, or custom CalDAV calendar connection",
7595
7917
  description: "Forge first discovers the writable calendars for the account, then stores the chosen mirrored calendars and either reuses the existing shared Forge write target or saves a new one when needed.",
7918
+ requestBody: {
7919
+ required: true,
7920
+ content: {
7921
+ "application/json": {
7922
+ schema: {
7923
+ $ref: "#/components/schemas/CalendarConnectionMutationInput"
7924
+ }
7925
+ }
7926
+ }
7927
+ },
7596
7928
  responses: {
7597
7929
  "201": jsonResponse({
7598
7930
  type: "object",
@@ -7607,6 +7939,65 @@ export function buildOpenApiDocument() {
7607
7939
  }
7608
7940
  }
7609
7941
  },
7942
+ "/api/v1/calendar/connections/{id}": {
7943
+ patch: {
7944
+ summary: "Update one calendar connection label or selected mirrored calendars",
7945
+ requestBody: {
7946
+ required: true,
7947
+ content: {
7948
+ "application/json": {
7949
+ schema: {
7950
+ $ref: "#/components/schemas/CalendarConnectionPatchInput"
7951
+ }
7952
+ }
7953
+ }
7954
+ },
7955
+ responses: {
7956
+ "200": jsonResponse({
7957
+ type: "object",
7958
+ required: ["connection"],
7959
+ properties: {
7960
+ connection: {
7961
+ $ref: "#/components/schemas/CalendarConnection"
7962
+ }
7963
+ }
7964
+ }, "Updated calendar connection"),
7965
+ default: { $ref: "#/components/responses/Error" }
7966
+ }
7967
+ },
7968
+ delete: {
7969
+ summary: "Delete one calendar connection and stop mirroring it",
7970
+ responses: {
7971
+ "200": jsonResponse({
7972
+ type: "object",
7973
+ required: ["connection"],
7974
+ properties: {
7975
+ connection: {
7976
+ $ref: "#/components/schemas/CalendarConnection"
7977
+ }
7978
+ }
7979
+ }, "Deleted calendar connection"),
7980
+ default: { $ref: "#/components/responses/Error" }
7981
+ }
7982
+ }
7983
+ },
7984
+ "/api/v1/calendar/connections/{id}/discovery": {
7985
+ get: {
7986
+ summary: "Rediscover available calendars for an existing calendar connection",
7987
+ responses: {
7988
+ "200": jsonResponse({
7989
+ type: "object",
7990
+ required: ["discovery"],
7991
+ properties: {
7992
+ discovery: {
7993
+ $ref: "#/components/schemas/CalendarDiscoveryPayload"
7994
+ }
7995
+ }
7996
+ }, "Calendar connection discovery"),
7997
+ default: { $ref: "#/components/responses/Error" }
7998
+ }
7999
+ }
8000
+ },
7610
8001
  "/api/v1/calendar/connections/{id}/sync": {
7611
8002
  post: {
7612
8003
  summary: "Sync one connected calendar provider",
@@ -27,6 +27,14 @@ const DEFAULT_RULES = [
27
27
  description: "Award a small XP bounty for each ten credited minutes of active work.",
28
28
  config: { fixedXp: 4, intervalMinutes: 10 }
29
29
  },
30
+ {
31
+ id: "reward_rule_entity_created",
32
+ family: "consistency",
33
+ code: "entity_created",
34
+ title: "Forge entity created",
35
+ description: "Award a small activity bounty when the user creates a real Forge entity.",
36
+ config: { fixedXp: 2 }
37
+ },
30
38
  {
31
39
  id: "reward_rule_task_run_completion",
32
40
  family: "completion",
@@ -673,6 +681,58 @@ export function recordTaskRunProgressRewards(taskRunId, taskId, actor, source, c
673
681
  }
674
682
  return rewards;
675
683
  }
684
+ export function recordEntityCreationReward(input) {
685
+ ensureDefaultRewardRules();
686
+ const reversibleGroup = `entity_created:${input.entityType}:${input.entityId}`;
687
+ const existing = getDatabase()
688
+ .prepare(`SELECT
689
+ id, rule_id, event_log_id, entity_type, entity_id, actor, source,
690
+ delta_xp, reason_title, reason_summary, reversible_group,
691
+ reversed_by_reward_id, metadata_json, created_at
692
+ FROM reward_ledger
693
+ WHERE reversible_group = ?
694
+ ORDER BY created_at ASC
695
+ LIMIT 1`)
696
+ .get(reversibleGroup);
697
+ if (existing) {
698
+ return mapLedger(existing);
699
+ }
700
+ const createdAtDate = Number.isNaN(Date.parse(input.createdAt))
701
+ ? new Date()
702
+ : new Date(input.createdAt);
703
+ const rule = getRuleByCode("entity_created");
704
+ const readableType = input.entityType.replaceAll("_", " ");
705
+ const title = input.title?.trim() || readableType;
706
+ const eventLog = recordEventLog({
707
+ eventKind: "reward.entity_created",
708
+ entityType: input.entityType,
709
+ entityId: input.entityId,
710
+ actor: input.actor ?? null,
711
+ source: input.source ?? "system",
712
+ metadata: {
713
+ entityType: input.entityType,
714
+ entityId: input.entityId,
715
+ title,
716
+ createdAt: input.createdAt
717
+ }
718
+ }, createdAtDate);
719
+ return insertLedgerEvent({
720
+ ruleId: rule?.id ?? null,
721
+ eventLogId: eventLog.id,
722
+ entityType: input.entityType,
723
+ entityId: input.entityId,
724
+ actor: input.actor ?? null,
725
+ source: input.source ?? "system",
726
+ deltaXp: Number(rule?.config.fixedXp ?? 2),
727
+ reasonTitle: `Created ${readableType}: ${title}`,
728
+ reasonSummary: "Small Forge activity XP for creating something concrete.",
729
+ reversibleGroup,
730
+ metadata: {
731
+ entityType: input.entityType,
732
+ title
733
+ }
734
+ }, createdAtDate);
735
+ }
676
736
  export function recordWorkAdjustmentReward(input) {
677
737
  const { rule, intervalMinutes, intervalSeconds, fixedXp } = getTaskRunProgressRewardCadence();
678
738
  const entityType = workAdjustmentEntityTypeSchema.parse(input.entityType);
@@ -84,12 +84,13 @@ function ensureDataManagementSettingsRow() {
84
84
  preferred_data_root,
85
85
  backup_directory,
86
86
  backup_frequency_hours,
87
+ backup_retention_days,
87
88
  auto_repair_enabled,
88
89
  last_auto_backup_at,
89
90
  last_manual_backup_at,
90
91
  created_at,
91
92
  updated_at
92
- ) VALUES (1, ?, ?, NULL, 1, NULL, NULL, ?, ?)`)
93
+ ) VALUES (1, ?, ?, NULL, 30, 1, NULL, NULL, ?, ?)`)
93
94
  .run(dataRoot, backupDirectory, now, now);
94
95
  }
95
96
  function readDataManagementSettingsRow() {
@@ -99,6 +100,7 @@ function readDataManagementSettingsRow() {
99
100
  preferred_data_root,
100
101
  backup_directory,
101
102
  backup_frequency_hours,
103
+ backup_retention_days,
102
104
  auto_repair_enabled,
103
105
  last_auto_backup_at,
104
106
  last_manual_backup_at,
@@ -124,12 +126,13 @@ function writeDataManagementSettingsRow(patch) {
124
126
  SET preferred_data_root = ?,
125
127
  backup_directory = ?,
126
128
  backup_frequency_hours = ?,
129
+ backup_retention_days = ?,
127
130
  auto_repair_enabled = ?,
128
131
  last_auto_backup_at = ?,
129
132
  last_manual_backup_at = ?,
130
133
  updated_at = ?
131
134
  WHERE id = 1`)
132
- .run(next.preferred_data_root, next.backup_directory, next.backup_frequency_hours, next.auto_repair_enabled, next.last_auto_backup_at, next.last_manual_backup_at, next.updated_at);
135
+ .run(next.preferred_data_root, next.backup_directory, next.backup_frequency_hours, next.backup_retention_days, next.auto_repair_enabled, next.last_auto_backup_at, next.last_manual_backup_at, next.updated_at);
133
136
  }
134
137
  function resolveCurrentDataManagementSettings() {
135
138
  const row = readDataManagementSettingsRow();
@@ -139,6 +142,7 @@ function resolveCurrentDataManagementSettings() {
139
142
  preferredDataRoot,
140
143
  backupDirectory,
141
144
  backupFrequencyHours: row.backup_frequency_hours,
145
+ backupRetentionDays: row.backup_retention_days,
142
146
  autoRepairEnabled: row.auto_repair_enabled === 1,
143
147
  lastAutoBackupAt: row.last_auto_backup_at,
144
148
  lastManualBackupAt: row.last_manual_backup_at
@@ -461,6 +465,7 @@ export async function createDataBackup(input = { note: "" }, options = {}) {
461
465
  }
462
466
  if (mode === "automatic") {
463
467
  writeDataManagementSettingsRow({ last_auto_backup_at: createdAt });
468
+ await pruneExpiredAutomaticBackups(settings.backupDirectory, settings.backupRetentionDays);
464
469
  }
465
470
  return backup;
466
471
  }
@@ -468,6 +473,27 @@ export async function createDataBackup(input = { note: "" }, options = {}) {
468
473
  await rm(sqliteSnapshot.tempDir, { recursive: true, force: true });
469
474
  }
470
475
  }
476
+ async function pruneExpiredAutomaticBackups(backupDirectory, retentionDays) {
477
+ if (!retentionDays) {
478
+ return;
479
+ }
480
+ const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
481
+ const backups = await listDataBackups();
482
+ for (const backup of backups) {
483
+ if (backup.mode !== "automatic") {
484
+ continue;
485
+ }
486
+ if (path.resolve(backup.backupDirectory) !== path.resolve(backupDirectory)) {
487
+ continue;
488
+ }
489
+ const createdAtMs = new Date(backup.createdAt).getTime();
490
+ if (!Number.isFinite(createdAtMs) || createdAtMs >= cutoff) {
491
+ continue;
492
+ }
493
+ await rm(backup.archivePath, { force: true });
494
+ await rm(backup.manifestPath, { force: true });
495
+ }
496
+ }
471
497
  async function openDatabaseSnapshot(databasePath) {
472
498
  const database = new DatabaseSync(databasePath);
473
499
  database.exec("PRAGMA busy_timeout = 250;");
@@ -643,6 +669,7 @@ export async function switchDataRoot(input, options = {}) {
643
669
  preferred_data_root: targetDataRoot,
644
670
  backup_directory: nextBackupDirectory,
645
671
  backup_frequency_hours: previousSettings.backupFrequencyHours,
672
+ backup_retention_days: previousSettings.backupRetentionDays,
646
673
  auto_repair_enabled: previousSettings.autoRepairEnabled ? 1 : 0
647
674
  });
648
675
  await (options.persistPreferredDataRoot ?? writeMonorepoPreferredDataRoot)(targetDataRoot);
@@ -698,6 +725,9 @@ export async function updateDataManagementSettings(input) {
698
725
  backup_frequency_hours: parsed.backupFrequencyHours !== undefined
699
726
  ? parsed.backupFrequencyHours
700
727
  : undefined,
728
+ backup_retention_days: parsed.backupRetentionDays !== undefined
729
+ ? parsed.backupRetentionDays
730
+ : undefined,
701
731
  auto_repair_enabled: parsed.autoRepairEnabled !== undefined
702
732
  ? parsed.autoRepairEnabled
703
733
  ? 1