forge-openclaw-plugin 0.3.0 → 0.3.2

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 (119) hide show
  1. package/dist/assets/{activity-page-DPPiiyLO.js → activity-page-DRRbpVK-.js} +1 -1
  2. package/dist/assets/{ai-surface-workspace-B_fV9C4s.js → ai-surface-workspace-DJPt-OaW.js} +1 -1
  3. package/dist/assets/{atlas-panel-BPTJyMcI.js → atlas-panel-G3wGxjYf.js} +1 -1
  4. package/dist/assets/{calendar-page-D4DcRQv7.js → calendar-page-y8G1ige8.js} +1 -1
  5. package/dist/assets/{calendar-rules-Cl_xMrBn.js → calendar-rules-CRhXqGQu.js} +1 -1
  6. package/dist/assets/{calendar-week-toolbar-BARSOEO9.js → calendar-week-toolbar-CWSK5hJl.js} +1 -1
  7. package/dist/assets/{companion-sync-lab-page-COFYDI3w.js → companion-sync-lab-page-kEgOF3c6.js} +1 -1
  8. package/dist/assets/daily-metrics-dashboard-BJjUIZVA.js +1 -0
  9. package/dist/assets/{entity-note-count-link-BX93WYgh.js → entity-note-count-link-ef3ZOpKM.js} +1 -1
  10. package/dist/assets/{entity-notes-surface-CBlKc-x-.js → entity-notes-surface-CaCjjUaq.js} +1 -1
  11. package/dist/assets/{execution-board-Dh41UeKR.js → execution-board-RRWm2srb.js} +1 -1
  12. package/dist/assets/{faceted-token-search-BgxYH_gW.js → faceted-token-search-CZmtyJTT.js} +1 -1
  13. package/dist/assets/{flagship-signal-deck-bolzzK8q.js → flagship-signal-deck-DxRfmfpN.js} +1 -1
  14. package/dist/assets/{floating-action-menu-C5H-Wr2B.js → floating-action-menu-cKjNsPYD.js} +1 -1
  15. package/dist/assets/{goal-detail-page-CQeFnGDJ.js → goal-detail-page-BgQlK7yT.js} +1 -1
  16. package/dist/assets/{goals-page-BTg9crEK.js → goals-page-Cz2i3pVy.js} +1 -1
  17. package/dist/assets/{habits-page-C4vqk346.js → habits-page-BmEmWCGv.js} +1 -1
  18. package/dist/assets/index-Bcem7l5u.css +1 -0
  19. package/dist/assets/{index-By3tQxiE.js → index-QJkjKYzZ.js} +2 -2
  20. package/dist/assets/{insight-flow-dialog-JPYCsTKI.js → insight-flow-dialog-CdYMHwMP.js} +1 -1
  21. package/dist/assets/{insights-page-CFhVMlVL.js → insights-page-DHG-o9P_.js} +1 -1
  22. package/dist/assets/{kanban-page-DC3IDsnh.js → kanban-page-C6S_fJRc.js} +1 -1
  23. package/dist/assets/{knowledge-graph-page-DucyRYTZ.js → knowledge-graph-page-juA9QCy0.js} +1 -1
  24. package/dist/assets/{life-force-page-UrXfxaSO.js → life-force-page-ChTIA4mV.js} +1 -1
  25. package/dist/assets/{life-force-workspace-BhPNH7ad.js → life-force-workspace-sszLD-Yn.js} +1 -1
  26. package/dist/assets/{metric-tile-DtRPjN4V.js → metric-tile-B-tDTcBt.js} +1 -1
  27. package/dist/assets/{movement-page-DCIdTQEN.js → movement-page-Dx3T3HrG.js} +1 -1
  28. package/dist/assets/{note-markdown-CepDxj3v.js → note-markdown-CCyyE85h.js} +1 -1
  29. package/dist/assets/{note-tags-input-BXYaDkOr.js → note-tags-input-BS14YXHn.js} +1 -1
  30. package/dist/assets/{notes-page-Bl1LlCLo.js → notes-page-D3yGl8TN.js} +1 -1
  31. package/dist/assets/{open-in-graph-button-C263zl5l.js → open-in-graph-button-CMjJZX7G.js} +1 -1
  32. package/dist/assets/{orbit-map-Cb_AJyIj.js → orbit-map-v2Pv9RU_.js} +1 -1
  33. package/dist/assets/{overview-page-C2yLity7.js → overview-page-DSOpBpHf.js} +1 -1
  34. package/dist/assets/{page-hero-5RTqAI88.js → page-hero-CONuJu5J.js} +1 -1
  35. package/dist/assets/{pill-cluster-CBrrtZBm.js → pill-cluster-W2fY48OM.js} +1 -1
  36. package/dist/assets/{preference-entity-handoff-button-BQzg2J4k.js → preference-entity-handoff-button-CKDxCnbl.js} +1 -1
  37. package/dist/assets/{preferences-page-DekYF-Yz.js → preferences-page-Br3zrLlH.js} +1 -1
  38. package/dist/assets/{project-collections-BVXB8PPF.js → project-collections-NBwk6yV-.js} +1 -1
  39. package/dist/assets/{project-detail-page-CQlrLEn9.js → project-detail-page-BIU9_TRN.js} +1 -1
  40. package/dist/assets/{project-management-hierarchy-page--mzM_zRE.js → project-management-hierarchy-page-C2H620xD.js} +1 -1
  41. package/dist/assets/{project-management-section-nav-DQl0qRWy.js → project-management-section-nav-DRPVS1Eh.js} +1 -1
  42. package/dist/assets/{projects-page-Dr_0NdVP.js → projects-page-w7a_L2-z.js} +1 -1
  43. package/dist/assets/{psyche-behaviors-page-DOUaCWON.js → psyche-behaviors-page-Dt4CMC-q.js} +1 -1
  44. package/dist/assets/{psyche-flashcards-page-BA84fO_X.js → psyche-flashcards-page-rXz06dqO.js} +1 -1
  45. package/dist/assets/{psyche-goal-map-page-DpXAg1Vf.js → psyche-goal-map-page-BCcD6Jkl.js} +1 -1
  46. package/dist/assets/{psyche-graph-BxcbAbXt.js → psyche-graph-CXlAj4ED.js} +1 -1
  47. package/dist/assets/psyche-metrics-page-B1otiir6.js +1 -0
  48. package/dist/assets/{psyche-mode-guide-page-CBlIS5N_.js → psyche-mode-guide-page-C6AdVIAv.js} +1 -1
  49. package/dist/assets/{psyche-modes-page-DE6KDK_1.js → psyche-modes-page-DoUqz2mR.js} +1 -1
  50. package/dist/assets/psyche-page-BJNLQn8W.js +1 -0
  51. package/dist/assets/{psyche-patterns-page-DbCNRuww.js → psyche-patterns-page-CzpMSAPu.js} +1 -1
  52. package/dist/assets/{psyche-questionnaire-builder-page-DnraDj1H.js → psyche-questionnaire-builder-page-DjzatGVZ.js} +1 -1
  53. package/dist/assets/{psyche-questionnaire-detail-page-DQ6zNQkk.js → psyche-questionnaire-detail-page-B0Qfowfg.js} +1 -1
  54. package/dist/assets/{psyche-questionnaire-run-detail-page-BGrOIX0B.js → psyche-questionnaire-run-detail-page-B2dNj4jJ.js} +1 -1
  55. package/dist/assets/{psyche-questionnaire-run-page-Bg3BhBHb.js → psyche-questionnaire-run-page-DyD9Ui-g.js} +1 -1
  56. package/dist/assets/{psyche-questionnaires-page-BEreTNjG.js → psyche-questionnaires-page-BxfKcoi3.js} +1 -1
  57. package/dist/assets/{psyche-report-detail-page-Br9KlJBG.js → psyche-report-detail-page-Ci14mNUK.js} +1 -1
  58. package/dist/assets/{psyche-reports-page-Bk6QpAEF.js → psyche-reports-page-ALhNueiV.js} +1 -1
  59. package/dist/assets/{psyche-schemas-beliefs-page-Db8d2ga7.js → psyche-schemas-beliefs-page-C0P2Vw6-.js} +1 -1
  60. package/dist/assets/{psyche-screen-time-page-NazHUAkT.js → psyche-screen-time-page-BYnaT3cb.js} +1 -1
  61. package/dist/assets/{psyche-self-observation-page-H6FToucM.js → psyche-self-observation-page-eM9vpwIB.js} +1 -1
  62. package/dist/assets/{psyche-values-page-DqM382-6.js → psyche-values-page-BqrGIDok.js} +1 -1
  63. package/dist/assets/{report-chain-fields-nW4zndrM.js → report-chain-fields-CbkT7WOO.js} +1 -1
  64. package/dist/assets/{rewards-page-C-tZVnIK.js → rewards-page-DpAJ8IxE.js} +1 -1
  65. package/dist/assets/{scheduling-rules-editor-DGF-044c.js → scheduling-rules-editor-BUGnZJ0g.js} +1 -1
  66. package/dist/assets/{schema-badge-57AJ3FY-.js → schema-badge-CLAA4p-b.js} +1 -1
  67. package/dist/assets/{select-menu-mfaklv7r.js → select-menu-Cqs5t3ng.js} +1 -1
  68. package/dist/assets/{settings-agents-page-CPJeeYg0.js → settings-agents-page-pmkVrOO8.js} +1 -1
  69. package/dist/assets/{settings-bin-page-D3eX5aNf.js → settings-bin-page-BwtLrweW.js} +1 -1
  70. package/dist/assets/{settings-calendar-page-Bgx8NIsP.js → settings-calendar-page-BBjAMOCf.js} +1 -1
  71. package/dist/assets/{settings-data-page-BuMSUjpp.js → settings-data-page-Bqy5SukM.js} +1 -1
  72. package/dist/assets/{settings-logs-page-CHb2GNUM.js → settings-logs-page-8khtrf4T.js} +1 -1
  73. package/dist/assets/{settings-mobile-page-ClUvXhJg.js → settings-mobile-page-BcyRjEU_.js} +1 -1
  74. package/dist/assets/{settings-models-page-njK6D7zc.js → settings-models-page-DlSnO9WX.js} +1 -1
  75. package/dist/assets/{settings-page-Dm--cx6B.js → settings-page-CyBQ2qcB.js} +1 -1
  76. package/dist/assets/{settings-rewards-page-3aShLM3A.js → settings-rewards-page-BBX2WtVM.js} +1 -1
  77. package/dist/assets/{settings-section-nav-BWox23Qv.js → settings-section-nav-Co0AaDG-.js} +1 -1
  78. package/dist/assets/{settings-users-page-D-hEPpgQ.js → settings-users-page-JSnfFFPu.js} +1 -1
  79. package/dist/assets/{settings-wiki-page-CC-rOtDm.js → settings-wiki-page--nT23Fdy.js} +1 -1
  80. package/dist/assets/{sleep-page-Cm_Ei2w1.js → sleep-page-CGMi3MtV.js} +1 -1
  81. package/dist/assets/{sports-page-CqSI-j-H.js → sports-page-Kv5bBoLr.js} +1 -1
  82. package/dist/assets/{strategies-page-BuJ4zLLv.js → strategies-page-Br7RdUJO.js} +1 -1
  83. package/dist/assets/{strategy-detail-page-CLmy0jbW.js → strategy-detail-page-CBWXjUhC.js} +1 -1
  84. package/dist/assets/{strategy-dialog-DnJix1ys.js → strategy-dialog-BTwkaSxI.js} +1 -1
  85. package/dist/assets/{surface-DRNu5Fpz.js → surface-CyWI7e1R.js} +1 -1
  86. package/dist/assets/{task-detail-page-DTnNjW33.js → task-detail-page-Qis9354n.js} +1 -1
  87. package/dist/assets/{timebox-planning-dialog-DckIr1K4.js → timebox-planning-dialog-DB8snOBD.js} +1 -1
  88. package/dist/assets/{today-page-Cgk4HBPK.js → today-page-D9A5PU0S.js} +1 -1
  89. package/dist/assets/{training-load-page-C-7iINaB.js → training-load-page-Bpd4DQAh.js} +1 -1
  90. package/dist/assets/vitals-page-CVYC-4RG.js +1 -0
  91. package/dist/assets/{weekly-review-page-DFmuzzig.js → weekly-review-page-BP7N3fNB.js} +1 -1
  92. package/dist/assets/{weight-loss-page-CWtgeuti.js → weight-loss-page-_fX3N1UK.js} +1 -1
  93. package/dist/assets/{wiki-article-markdown-BkQg8Hne.js → wiki-article-markdown-Bi02AsVt.js} +1 -1
  94. package/dist/assets/{wiki-editor-page-BYKcdX5I.js → wiki-editor-page-OZf9l4Tg.js} +1 -1
  95. package/dist/assets/{wiki-ingest-history-page-D_p4myw1.js → wiki-ingest-history-page-CzeNSL7N.js} +1 -1
  96. package/dist/assets/{wiki-ingest-modal-C-CidueO.js → wiki-ingest-modal-Ig_xnyc6.js} +1 -1
  97. package/dist/assets/wiki-page-B4xuNgY4.js +1 -0
  98. package/dist/assets/{workbench-flow-page-Q3r2IJty.js → workbench-flow-page-BhRJCALC.js} +1 -1
  99. package/dist/assets/{workbench-page-BFfWMGm6.js → workbench-page-DxVs2AOY.js} +1 -1
  100. package/dist/assets/{workout-detail-page-DeDyz1Kk.js → workout-detail-page-D4wfkE98.js} +1 -1
  101. package/dist/index.html +2 -2
  102. package/dist/server/server/migrations/069_psyche_devrage_cumulative_rage.sql +5 -0
  103. package/dist/server/server/src/app.js +3 -2
  104. package/dist/server/server/src/openapi.js +48 -8
  105. package/dist/server/server/src/psyche-types.js +18 -4
  106. package/dist/server/server/src/services/devrage-scanner.js +115 -10
  107. package/dist/server/server/src/services/devrage.js +82 -13
  108. package/openclaw.plugin.json +1 -1
  109. package/package.json +1 -1
  110. package/server/migrations/069_psyche_devrage_cumulative_rage.sql +5 -0
  111. package/skills/forge-openclaw/SKILL.md +7 -0
  112. package/skills/forge-openclaw/entity_conversation_playbooks.md +23 -0
  113. package/skills/forge-openclaw/psyche_entity_playbooks.md +32 -0
  114. package/dist/assets/daily-metrics-dashboard-BNGQlJkQ.js +0 -1
  115. package/dist/assets/index-BAXYM89v.css +0 -1
  116. package/dist/assets/psyche-metrics-page-D8JQEoOl.js +0 -1
  117. package/dist/assets/psyche-page-Dkh7R2Dc.js +0 -1
  118. package/dist/assets/vitals-page-CqE4bYMf.js +0 -1
  119. package/dist/assets/wiki-page-DYpg9ZEO.js +0 -1
@@ -3114,6 +3114,7 @@ const AGENT_ONBOARDING_CONVERSATION_RULES = [
3114
3114
  "For direct update or review requests, the next question should usually narrow the saved object, timeframe, or route family instead of reopening the whole meaning-making arc.",
3115
3115
  "For updates, start with the smallest thing that now feels wrong, newly true, or newly visible rather than restarting the whole story.",
3116
3116
  "For review requests, ask what practical question the user wants the read to answer before you ask for more scope.",
3117
+ "For review-first requests, use the correct read posture before asking write-shaped questions: shared batch search or read hints for normal entities, wiki/calendar dedicated reads for specialized CRUD, read-model routes for overviews, and Movement, Life Force, or Workbench dedicated reads for those domain surfaces. After the read, answer the practical question before asking for any save, correction, link, run, enrichment, or publish detail.",
3117
3118
  "After a review, overview, navigation, or specialized read returns data, first answer the user's practical question in plain language, then name one implication or uncertainty that matters for the next decision. Ask a follow-up only if it changes the next action: save, update, correct, link, schedule, run, publish, enrich, or open the UI.",
3118
3119
  "Treat userId, owner, and human/bot assignees as accountability and scope, not as opening form fields. Ask whose record or owner scope matters only when it changes visibility, review results, collaboration, automation behavior, or later filtering.",
3119
3120
  "For read and overview requests, ask for human or bot user scope only when the answer would meaningfully differ across owners; otherwise keep the next question focused on the user's practical question.",
@@ -5455,10 +5456,10 @@ function buildAgentOnboardingPayload(request) {
5455
5456
  maxQuestionsPerTurn: 1,
5456
5457
  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.",
5457
5458
  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.",
5458
- 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.",
5459
+ 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. Use the correct read posture before asking write-shaped questions: shared batch search or read hints for normal entities, wiki/calendar dedicated reads for specialized CRUD, read-model routes for overviews, and Movement, Life Force, or Workbench dedicated reads for those domain surfaces. After the read, answer the practical question before asking for any save, correction, link, run, enrichment, or publish detail. Do not reopen the whole intake unless the user is actually redefining the record.",
5459
5460
  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.",
5460
5461
  psycheOpeningQuestionRule: "Prefer a concrete opening question tied to the entity: ask when the value mattered, what happened the last time the pattern appeared, what cue or body signal came first before the behavior, what the belief starts saying about self or outcome, what feels most at risk inside the mode, what the part is trying to get the user to do or stop doing, or where the shift began in the incident. Reflect briefly before the question, choose one follow-up lane at a time, say what is becoming clearer before the next deeper question, and if several Psyche entities are visible hold the adjacent ones lightly until the main container is clear.",
5461
- psycheHypothesisRule: "When one concrete Psyche example is visible, a helpful hypothesis should start from evidence in the user's own example, offer one testable interpretation, name the function without blame such as protection, prediction, relief, or cost, and ask whether the danger, need, or wording fits. Do not present schema, mode, belief, or pattern language as a verdict. If the user corrects the hypothesis, revise it once and move toward the saveable record shape instead of asking for another broad story.",
5462
+ psycheHypothesisRule: "When one concrete Psyche example is visible, a helpful hypothesis should start from evidence in the user's own example, offer one testable interpretation, name the function without blame such as protection, prediction, relief, or cost, and ask whether the danger, need, or wording fits. Use the hypothesis timing checkpoint before asking a second or third deepening question: offer a hypothesis when one concrete episode, body cue, belief sentence, behavior, or mode voice is visible and the hypothesis would change the record shape, wording, links, or next action. Do not hypothesize yet when no concrete moment is visible, the user only wants a direct mechanical save, the user is flooded or unsafe, or the only available interpretation would be diagnosis-like, an origin story, or a certainty claim. Do not present schema, mode, belief, or pattern language as a verdict. If the user corrects the hypothesis, revise it once and move toward the saveable record shape instead of asking for another broad story.",
5462
5463
  mixedIntentSequencingRule: "When one user message combines several Forge jobs, identify the primary job and the order of operations before asking a follow-up. If a read changes the truth of a later write, read first: Movement timeline or box detail before correction, Workbench run or node detail before editing or publishing, and Life Force overview before changing durable assumptions when the current energy picture is uncertain. If the user asks to understand and save Psyche material plus create a support record, formulate the primary Psyche record first, then derive the flashcard, note, link, task, or habit from the accepted wording. If the user already gave the concrete action, do not ask a broad lane question; say the product sequence briefly and ask only for the missing span, wording, flow, run, node, weekday, or link that changes the next action.",
5463
5464
  duplicateDisambiguationRule: "Before creating or updating a normal stored entity when duplicate risk is plausible, search the shared batch entity route by entity type, distinctive title or wording, owner scope, and linked content. If a likely existing record appears, ask whether the user wants to update that record, link to it, or save a separate new record; do not reopen the whole create flow. For Psyche records, a similar belief, pattern, mode, trigger report, value, or flashcard is a formulation choice, not a duplicate error: compare the sentence, cue/payoff/cost, protective job, episode, urge sentence, or message and let the user choose update, link, or new version. For wiki_page and calendar_connection, use dedicated search/list/read routes before creating another page or connection. For Movement, Life Force, and Workbench, use the dedicated read lanes instead of batch duplicate search.",
5464
5465
  destructiveActionRule: "Before deleting, archiving, invalidating, overwriting, disconnecting, or substantially replacing a Forge record or specialized object, confirm the exact target and what should remain understandable. Prefer normal soft-delete for stored entities unless the user explicitly asks for permanent removal. For Psyche records, preserve therapeutic history by asking whether the old belief, pattern, mode, trigger report, value, or flashcard should be updated, linked as history, archived, or kept distinct; do not delete it just because a cleaner formulation exists. For Movement, distinguish user-defined overlay deletion from automatic-box invalidation and stay/trip/point deletion, and read the specific span first when the target is uncertain. For calendar connections, Workbench flows, wiki pages, and questionnaire instruments, ask what downstream sync, published output, backlinks, run history, or completed runs should remain understandable before deleting or replacing the saved object.",
@@ -4766,6 +4766,9 @@ export function buildOpenApiDocument() {
4766
4766
  "latestDateKey",
4767
4767
  "rawSwearCount",
4768
4768
  "swearingMessagePercent",
4769
+ "averageMaxCumulativeRage",
4770
+ "maxCumulativeRage",
4771
+ "maxSwearingStreak",
4769
4772
  "conversationsScanned",
4770
4773
  "messagesScanned",
4771
4774
  "messagesWithSwears",
@@ -4780,25 +4783,42 @@ export function buildOpenApiDocument() {
4780
4783
  latestDateKey: nullable({ type: "string" }),
4781
4784
  rawSwearCount: { type: "number" },
4782
4785
  swearingMessagePercent: { type: "number" },
4786
+ averageMaxCumulativeRage: { type: "number" },
4787
+ maxCumulativeRage: { type: "number" },
4788
+ maxSwearingStreak: { type: "integer" },
4783
4789
  conversationsScanned: { type: "integer" },
4784
4790
  messagesScanned: { type: "integer" },
4785
4791
  messagesWithSwears: { type: "integer" },
4786
4792
  dailyAverage: {
4787
4793
  type: "object",
4788
4794
  additionalProperties: false,
4789
- required: ["rawSwearCount", "swearingMessagePercent"],
4795
+ required: [
4796
+ "rawSwearCount",
4797
+ "swearingMessagePercent",
4798
+ "averageMaxCumulativeRage",
4799
+ "maxCumulativeRage"
4800
+ ],
4790
4801
  properties: {
4791
4802
  rawSwearCount: { type: "number" },
4792
- swearingMessagePercent: { type: "number" }
4803
+ swearingMessagePercent: { type: "number" },
4804
+ averageMaxCumulativeRage: { type: "number" },
4805
+ maxCumulativeRage: { type: "number" }
4793
4806
  }
4794
4807
  },
4795
4808
  weeklyAverage: {
4796
4809
  type: "object",
4797
4810
  additionalProperties: false,
4798
- required: ["rawSwearCount", "swearingMessagePercent"],
4811
+ required: [
4812
+ "rawSwearCount",
4813
+ "swearingMessagePercent",
4814
+ "averageMaxCumulativeRage",
4815
+ "maxCumulativeRage"
4816
+ ],
4799
4817
  properties: {
4800
4818
  rawSwearCount: { type: "number" },
4801
- swearingMessagePercent: { type: "number" }
4819
+ swearingMessagePercent: { type: "number" },
4820
+ averageMaxCumulativeRage: { type: "number" },
4821
+ maxCumulativeRage: { type: "number" }
4802
4822
  }
4803
4823
  },
4804
4824
  history: arrayOf({
@@ -4808,6 +4828,9 @@ export function buildOpenApiDocument() {
4808
4828
  "dateKey",
4809
4829
  "rawSwearCount",
4810
4830
  "swearingMessagePercent",
4831
+ "averageMaxCumulativeRage",
4832
+ "maxCumulativeRage",
4833
+ "maxSwearingStreak",
4811
4834
  "conversationsScanned",
4812
4835
  "messagesScanned",
4813
4836
  "messagesWithSwears"
@@ -4816,6 +4839,9 @@ export function buildOpenApiDocument() {
4816
4839
  dateKey: { type: "string" },
4817
4840
  rawSwearCount: { type: "number" },
4818
4841
  swearingMessagePercent: { type: "number" },
4842
+ averageMaxCumulativeRage: { type: "number" },
4843
+ maxCumulativeRage: { type: "number" },
4844
+ maxSwearingStreak: { type: "integer" },
4819
4845
  conversationsScanned: { type: "integer" },
4820
4846
  messagesScanned: { type: "integer" },
4821
4847
  messagesWithSwears: { type: "integer" }
@@ -4952,19 +4978,33 @@ export function buildOpenApiDocument() {
4952
4978
  dailyAverage: {
4953
4979
  type: "object",
4954
4980
  additionalProperties: false,
4955
- required: ["rawSwearCount", "swearingMessagePercent"],
4981
+ required: [
4982
+ "rawSwearCount",
4983
+ "swearingMessagePercent",
4984
+ "averageMaxCumulativeRage",
4985
+ "maxCumulativeRage"
4986
+ ],
4956
4987
  properties: {
4957
4988
  rawSwearCount: { type: "number" },
4958
- swearingMessagePercent: { type: "number" }
4989
+ swearingMessagePercent: { type: "number" },
4990
+ averageMaxCumulativeRage: { type: "number" },
4991
+ maxCumulativeRage: { type: "number" }
4959
4992
  }
4960
4993
  },
4961
4994
  weeklyAverage: {
4962
4995
  type: "object",
4963
4996
  additionalProperties: false,
4964
- required: ["rawSwearCount", "swearingMessagePercent"],
4997
+ required: [
4998
+ "rawSwearCount",
4999
+ "swearingMessagePercent",
5000
+ "averageMaxCumulativeRage",
5001
+ "maxCumulativeRage"
5002
+ ],
4965
5003
  properties: {
4966
5004
  rawSwearCount: { type: "number" },
4967
- swearingMessagePercent: { type: "number" }
5005
+ swearingMessagePercent: { type: "number" },
5006
+ averageMaxCumulativeRage: { type: "number" },
5007
+ maxCumulativeRage: { type: "number" }
4968
5008
  }
4969
5009
  },
4970
5010
  sync: {
@@ -280,21 +280,31 @@ export const devrageMetricPayloadSchema = z.object({
280
280
  latestDateKey: z.string().nullable(),
281
281
  rawSwearCount: z.number().nonnegative(),
282
282
  swearingMessagePercent: z.number().nonnegative(),
283
+ averageMaxCumulativeRage: z.number().nonnegative(),
284
+ maxCumulativeRage: z.number().nonnegative(),
285
+ maxSwearingStreak: z.number().int().nonnegative(),
283
286
  conversationsScanned: z.number().int().nonnegative(),
284
287
  messagesScanned: z.number().int().nonnegative(),
285
288
  messagesWithSwears: z.number().int().nonnegative(),
286
289
  dailyAverage: z.object({
287
290
  rawSwearCount: z.number().nonnegative(),
288
- swearingMessagePercent: z.number().nonnegative()
291
+ swearingMessagePercent: z.number().nonnegative(),
292
+ averageMaxCumulativeRage: z.number().nonnegative(),
293
+ maxCumulativeRage: z.number().nonnegative()
289
294
  }),
290
295
  weeklyAverage: z.object({
291
296
  rawSwearCount: z.number().nonnegative(),
292
- swearingMessagePercent: z.number().nonnegative()
297
+ swearingMessagePercent: z.number().nonnegative(),
298
+ averageMaxCumulativeRage: z.number().nonnegative(),
299
+ maxCumulativeRage: z.number().nonnegative()
293
300
  }),
294
301
  history: z.array(z.object({
295
302
  dateKey: z.string(),
296
303
  rawSwearCount: z.number().nonnegative(),
297
304
  swearingMessagePercent: z.number().nonnegative(),
305
+ averageMaxCumulativeRage: z.number().nonnegative(),
306
+ maxCumulativeRage: z.number().nonnegative(),
307
+ maxSwearingStreak: z.number().int().nonnegative(),
298
308
  conversationsScanned: z.number().int().nonnegative(),
299
309
  messagesScanned: z.number().int().nonnegative(),
300
310
  messagesWithSwears: z.number().int().nonnegative()
@@ -337,11 +347,15 @@ export const psycheMetricsViewDataSchema = z.object({
337
347
  totalSwears: z.number().nonnegative(),
338
348
  dailyAverage: z.object({
339
349
  rawSwearCount: z.number().nonnegative(),
340
- swearingMessagePercent: z.number().nonnegative()
350
+ swearingMessagePercent: z.number().nonnegative(),
351
+ averageMaxCumulativeRage: z.number().nonnegative(),
352
+ maxCumulativeRage: z.number().nonnegative()
341
353
  }),
342
354
  weeklyAverage: z.object({
343
355
  rawSwearCount: z.number().nonnegative(),
344
- swearingMessagePercent: z.number().nonnegative()
356
+ swearingMessagePercent: z.number().nonnegative(),
357
+ averageMaxCumulativeRage: z.number().nonnegative(),
358
+ maxCumulativeRage: z.number().nonnegative()
345
359
  }),
346
360
  sync: z.object({
347
361
  fullSyncCompletedAt: z.string().nullable(),
@@ -7,7 +7,8 @@ import { createInterface } from "node:readline/promises";
7
7
  const require = createRequire(import.meta.url);
8
8
  const tokenPattern = /[a-z][a-z0-9'*_-]*/gi;
9
9
  const defaultSwearLexicon = [
10
- { root: "fuck", variants: ["fuck", "fucked", "fucker", "fuckers", "fuckin", "fucking", "fucks", "motherfuck", "motherfucked", "motherfucker", "motherfuckers", "motherfucking"] },
10
+ { root: "fuck", variants: ["fuck", "f*ck", "f**k", "fck", "fuk", "fucked", "fucker", "fuckers", "fuckin", "fucking", "fucks", "motherfuck", "motherfucked", "motherfucker", "motherfuckers", "motherfucking"] },
11
+ { root: "ffs", variants: ["ffs", "for fucks sake", "for fuck's sake", "for-fucks-sake", "for-fuck's-sake"] },
11
12
  { root: "wtf", variants: ["wtf"] },
12
13
  { root: "shit", variants: ["shit", "shitshow", "shits", "shitty", "bullshit", "bullshitting", "dipshit", "dipshits"] },
13
14
  { root: "dick", variants: ["dick", "dicks", "dickhead", "dickheads"] },
@@ -33,10 +34,7 @@ const ADAPTER_FACTORIES = {
33
34
  join(homedir(), ".hermes", "**/*.{json,jsonl}"),
34
35
  join(homedir(), ".config", "hermes", "**/*.{json,jsonl}")
35
36
  ]),
36
- openclaw: () => genericLocalLogAdapter("openclaw", [
37
- join(homedir(), ".openclaw", "**/*.{json,jsonl}"),
38
- join(homedir(), "Library", "Application Support", "OpenClaw", "**/*.{json,jsonl}")
39
- ]),
37
+ openclaw: openclawAdapter,
40
38
  opencode: opencodeAdapter,
41
39
  zed: zedAdapter
42
40
  };
@@ -69,7 +67,7 @@ function createAdapter(source) {
69
67
  function allAdapters() {
70
68
  return availableSources().map((source) => createAdapter(source));
71
69
  }
72
- function analyzeConversations(conversations, options, generatedAt = new Date().toISOString()) {
70
+ export function analyzeConversations(conversations, options, generatedAt = new Date().toISOString()) {
73
71
  const { tokenIndex, phraseVariants } = buildLexiconIndexes();
74
72
  const agentStats = new Map();
75
73
  const sourceStats = new Map();
@@ -89,6 +87,10 @@ function analyzeConversations(conversations, options, generatedAt = new Date().t
89
87
  let conversationMessages = 0;
90
88
  let conversationMessagesWithSwears = 0;
91
89
  let conversationSwears = 0;
90
+ let cumulativeRage = 0;
91
+ let maxCumulativeRage = 0;
92
+ let swearingStreak = 0;
93
+ let maxSwearingStreak = 0;
92
94
  const currentSource = sourceStats.get(conversation.source) ?? {
93
95
  source: conversation.source,
94
96
  conversations: 0,
@@ -125,7 +127,15 @@ function analyzeConversations(conversations, options, generatedAt = new Date().t
125
127
  currentSource.messagesWithSwears += 1;
126
128
  currentAgent.messagesWithSwears += 1;
127
129
  currentAgent.swears += swearsInMessage;
130
+ cumulativeRage += swearsInMessage;
131
+ swearingStreak += 1;
132
+ }
133
+ else {
134
+ cumulativeRage = Math.max(0, cumulativeRage - 1);
135
+ swearingStreak = 0;
128
136
  }
137
+ maxCumulativeRage = Math.max(maxCumulativeRage, cumulativeRage);
138
+ maxSwearingStreak = Math.max(maxSwearingStreak, swearingStreak);
129
139
  agentStats.set(agent, currentAgent);
130
140
  }
131
141
  sourceStats.set(conversation.source, currentSource);
@@ -138,9 +148,17 @@ function analyzeConversations(conversations, options, generatedAt = new Date().t
138
148
  dateKey,
139
149
  messages: conversationMessages,
140
150
  messagesWithSwears: conversationMessagesWithSwears,
141
- swears: conversationSwears
151
+ swears: conversationSwears,
152
+ maxCumulativeRage,
153
+ maxSwearingStreak
142
154
  });
143
155
  }
156
+ const maxCumulativeRage = Math.max(0, ...conversationStats.map((conversation) => conversation.maxCumulativeRage));
157
+ const maxSwearingStreak = Math.max(0, ...conversationStats.map((conversation) => conversation.maxSwearingStreak));
158
+ const averageMaxCumulativeRage = conversationStats.length === 0
159
+ ? 0
160
+ : conversationStats.reduce((sum, conversation) => sum + conversation.maxCumulativeRage, 0) /
161
+ conversationStats.length;
144
162
  return {
145
163
  generatedAt,
146
164
  filesScanned: [...filesScanned].sort(),
@@ -148,6 +166,9 @@ function analyzeConversations(conversations, options, generatedAt = new Date().t
148
166
  messagesScanned,
149
167
  messagesWithSwears,
150
168
  totalSwears,
169
+ averageMaxCumulativeRage,
170
+ maxCumulativeRage,
171
+ maxSwearingStreak,
151
172
  byAgent: [...agentStats.entries()]
152
173
  .map(([agent, stats]) => ({ agent, ...stats }))
153
174
  .sort((left, right) => right.swears - left.swears ||
@@ -399,6 +420,28 @@ function genericLocalLogAdapter(source, patterns) {
399
420
  }
400
421
  };
401
422
  }
423
+ function openclawAdapter() {
424
+ return {
425
+ source: "openclaw",
426
+ async read() {
427
+ const trajectoryResult = await readJsonlTree("openclaw", [
428
+ join(homedir(), ".openclaw", "agents"),
429
+ join(homedir(), "Library", "Application Support", "OpenClaw", "agents")
430
+ ], parseOpenClawTrajectoryLine);
431
+ const genericResult = await genericLocalLogAdapter("openclaw", [
432
+ join(homedir(), ".openclaw", "**/*.{json,jsonl}"),
433
+ join(homedir(), "Library", "Application Support", "OpenClaw", "**/*.{json,jsonl}")
434
+ ]).read();
435
+ return {
436
+ conversations: [
437
+ ...trajectoryResult.conversations,
438
+ ...genericResult.conversations.filter((conversation) => !conversation.sourceFile.endsWith(".trajectory.jsonl"))
439
+ ],
440
+ warnings: [...trajectoryResult.warnings, ...genericResult.warnings]
441
+ };
442
+ }
443
+ };
444
+ }
402
445
  async function readJsonlTree(source, roots, parser) {
403
446
  const files = (await Promise.all(roots.map((root) => globFiles(`${root.replace(/\/+$/, "")}/**/*.jsonl`))))
404
447
  .flat()
@@ -515,6 +558,47 @@ function parseGenericJsonLine(record, context) {
515
558
  index: context.line
516
559
  });
517
560
  }
561
+ export function parseOpenClawTrajectoryLine(record, context) {
562
+ if (!isObject(record) || record.type !== "prompt.submitted" || !isObject(record.data)) {
563
+ return null;
564
+ }
565
+ const data = record.data;
566
+ const text = typeof data.prompt === "string" && data.prompt.trim().length > 0
567
+ ? data.prompt.trim()
568
+ : latestOpenClawUserMessageText(data.messages);
569
+ if (!text || isContextInjection("user", text)) {
570
+ return null;
571
+ }
572
+ return {
573
+ agent: "openclaw",
574
+ source: "openclaw",
575
+ conversationId: context.conversationId.replace(/\.trajectory$/, ""),
576
+ role: "user",
577
+ text,
578
+ timestamp: stringTimestamp(record.ts) ??
579
+ stringTimestamp(record.timestamp) ??
580
+ stringTimestamp(data.timestamp) ??
581
+ numberTimestamp(record.ts) ??
582
+ context.fallbackTimestamp,
583
+ sourceFile: context.sourceFile
584
+ };
585
+ }
586
+ function latestOpenClawUserMessageText(messages) {
587
+ if (!Array.isArray(messages)) {
588
+ return "";
589
+ }
590
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
591
+ const message = messages[index];
592
+ if (!isObject(message) || normalizeRole(message.role ?? message.type) !== "user") {
593
+ continue;
594
+ }
595
+ const text = extractText(message.content ?? message.text).join("\n").trim();
596
+ if (text) {
597
+ return text;
598
+ }
599
+ }
600
+ return "";
601
+ }
518
602
  function parseGenericMessage(entry, context) {
519
603
  if (!isObject(entry)) {
520
604
  return null;
@@ -590,7 +674,7 @@ function buildLexiconIndexes(lexicon = defaultSwearLexicon) {
590
674
  });
591
675
  continue;
592
676
  }
593
- tokenIndex.set(normalizedVariant, entry.root);
677
+ tokenIndex.set(normalizeToken(normalizedVariant), entry.root);
594
678
  }
595
679
  }
596
680
  phraseVariants.sort((left, right) => right.variant.length - left.variant.length || left.variant.localeCompare(right.variant));
@@ -676,17 +760,38 @@ function buildDailyStats(conversations) {
676
760
  messages: 0,
677
761
  messagesWithSwears: 0,
678
762
  swears: 0,
679
- swearingMessagePercent: 0
763
+ swearingMessagePercent: 0,
764
+ averageMaxCumulativeRage: 0,
765
+ maxCumulativeRage: 0,
766
+ maxSwearingStreak: 0,
767
+ maxCumulativeRageSum: 0
680
768
  };
681
769
  current.conversations += 1;
682
770
  current.messages += conversation.messages;
683
771
  current.messagesWithSwears += conversation.messagesWithSwears;
684
772
  current.swears += conversation.swears;
773
+ current.maxCumulativeRageSum += conversation.maxCumulativeRage;
774
+ current.maxCumulativeRage = Math.max(current.maxCumulativeRage, conversation.maxCumulativeRage);
775
+ current.maxSwearingStreak = Math.max(current.maxSwearingStreak, conversation.maxSwearingStreak);
685
776
  current.swearingMessagePercent =
686
777
  current.messages === 0 ? 0 : (current.messagesWithSwears / current.messages) * 100;
778
+ current.averageMaxCumulativeRage =
779
+ current.conversations === 0 ? 0 : current.maxCumulativeRageSum / current.conversations;
687
780
  byDay.set(conversation.dateKey, current);
688
781
  }
689
- return [...byDay.values()].sort((left, right) => right.dateKey.localeCompare(left.dateKey));
782
+ return [...byDay.values()]
783
+ .map((stats) => ({
784
+ dateKey: stats.dateKey,
785
+ conversations: stats.conversations,
786
+ messages: stats.messages,
787
+ messagesWithSwears: stats.messagesWithSwears,
788
+ swears: stats.swears,
789
+ swearingMessagePercent: stats.swearingMessagePercent,
790
+ averageMaxCumulativeRage: stats.averageMaxCumulativeRage,
791
+ maxCumulativeRage: stats.maxCumulativeRage,
792
+ maxSwearingStreak: stats.maxSwearingStreak
793
+ }))
794
+ .sort((left, right) => right.dateKey.localeCompare(left.dateKey));
690
795
  }
691
796
  function compactMessage(message) {
692
797
  return message ? [message] : [];
@@ -4,6 +4,8 @@ import { getDatabase, runInTransaction } from "../db.js";
4
4
  import { psycheMetricsViewDataSchema } from "../psyche-types.js";
5
5
  const SWEAR_COUNT_KEY = "swear_count";
6
6
  const SWEARING_MESSAGE_PERCENT_KEY = "swearing_message_percent";
7
+ const AVERAGE_MAX_CUMULATIVE_RAGE_KEY = "average_max_cumulative_rage";
8
+ const MAX_CUMULATIVE_RAGE_KEY = "max_cumulative_rage";
7
9
  const DEFAULT_ROLE_FILTER = new Set(["user"]);
8
10
  const DAILY_RESYNC_INTERVAL_MS = 60 * 60 * 1000;
9
11
  const PSYCHE_METRIC_DEFINITIONS = {
@@ -20,6 +22,20 @@ const PSYCHE_METRIC_DEFINITIONS = {
20
22
  category: "conversationTone",
21
23
  unit: "%",
22
24
  aggregation: "discrete"
25
+ },
26
+ [AVERAGE_MAX_CUMULATIVE_RAGE_KEY]: {
27
+ metric: "devrageAverageMaxCumulativeRage",
28
+ label: "Average max cumulative rage",
29
+ category: "conversationTone",
30
+ unit: "score",
31
+ aggregation: "discrete"
32
+ },
33
+ [MAX_CUMULATIVE_RAGE_KEY]: {
34
+ metric: "devrageMaxCumulativeRage",
35
+ label: "Max cumulative rage",
36
+ category: "conversationTone",
37
+ unit: "score",
38
+ aggregation: "discrete"
23
39
  }
24
40
  };
25
41
  let syncInFlight = null;
@@ -34,15 +50,18 @@ export async function syncDevrageMetricHistory(options = {}) {
34
50
  }
35
51
  export async function syncDevrageMetricHistoryIfNeeded() {
36
52
  const state = getDevrageSyncState();
37
- const nextSync = getNextDevrageMetricSync(state);
53
+ const nextSync = getNextDevrageMetricSync(state, new Date(), needsDevrageCumulativeRageBackfill());
38
54
  if (nextSync) {
39
55
  await syncDevrageMetricHistory(nextSync);
40
56
  }
41
57
  }
42
- export function getNextDevrageMetricSync(state, now = new Date()) {
58
+ export function getNextDevrageMetricSync(state, now = new Date(), needsCumulativeRageBackfill = false) {
43
59
  if (!state?.full_sync_completed_at) {
44
60
  return { forceFull: true };
45
61
  }
62
+ if (needsCumulativeRageBackfill) {
63
+ return { forceFull: true };
64
+ }
46
65
  const today = todayDateKey(now);
47
66
  if (state.last_synced_date_key !== today) {
48
67
  return { dateKey: today };
@@ -56,6 +75,22 @@ export function getNextDevrageMetricSync(state, now = new Date()) {
56
75
  }
57
76
  return null;
58
77
  }
78
+ export function needsDevrageCumulativeRageBackfill() {
79
+ const rows = getDatabase()
80
+ .prepare(`SELECT metric_key, COUNT(*) AS count
81
+ FROM psyche_devrage_metric_measures
82
+ WHERE metric_key IN (?, ?, ?, ?)
83
+ GROUP BY metric_key`)
84
+ .all(SWEAR_COUNT_KEY, SWEARING_MESSAGE_PERCENT_KEY, AVERAGE_MAX_CUMULATIVE_RAGE_KEY, MAX_CUMULATIVE_RAGE_KEY);
85
+ const counts = new Map(rows.map((row) => [row.metric_key, Number(row.count) || 0]));
86
+ const legacyRows = (counts.get(SWEAR_COUNT_KEY) ?? 0) > 0 ||
87
+ (counts.get(SWEARING_MESSAGE_PERCENT_KEY) ?? 0) > 0;
88
+ if (!legacyRows) {
89
+ return false;
90
+ }
91
+ return ((counts.get(AVERAGE_MAX_CUMULATIVE_RAGE_KEY) ?? 0) === 0 ||
92
+ (counts.get(MAX_CUMULATIVE_RAGE_KEY) ?? 0) === 0);
93
+ }
59
94
  export function getDevrageMetricPayload() {
60
95
  const generatedAt = nowIso();
61
96
  const state = getDevrageSyncState();
@@ -69,16 +104,23 @@ export function getDevrageMetricPayload() {
69
104
  latestDateKey: latest?.dateKey ?? null,
70
105
  rawSwearCount: latest?.rawSwearCount ?? 0,
71
106
  swearingMessagePercent: latest?.swearingMessagePercent ?? 0,
107
+ averageMaxCumulativeRage: latest?.averageMaxCumulativeRage ?? 0,
108
+ maxCumulativeRage: latest?.maxCumulativeRage ?? 0,
109
+ maxSwearingStreak: latest?.maxSwearingStreak ?? 0,
72
110
  conversationsScanned: latest?.conversationsScanned ?? 0,
73
111
  messagesScanned: latest?.messagesScanned ?? 0,
74
112
  messagesWithSwears: latest?.messagesWithSwears ?? 0,
75
113
  dailyAverage: {
76
114
  rawSwearCount: dailyAverages.rawSwearCount,
77
- swearingMessagePercent: dailyAverages.swearingMessagePercent
115
+ swearingMessagePercent: dailyAverages.swearingMessagePercent,
116
+ averageMaxCumulativeRage: dailyAverages.averageMaxCumulativeRage,
117
+ maxCumulativeRage: dailyAverages.maxCumulativeRage
78
118
  },
79
119
  weeklyAverage: {
80
120
  rawSwearCount: weeklyAverages.rawSwearCount,
81
- swearingMessagePercent: weeklyAverages.swearingMessagePercent
121
+ swearingMessagePercent: weeklyAverages.swearingMessagePercent,
122
+ averageMaxCumulativeRage: weeklyAverages.averageMaxCumulativeRage,
123
+ maxCumulativeRage: weeklyAverages.maxCumulativeRage
82
124
  },
83
125
  history,
84
126
  sync: {
@@ -220,11 +262,15 @@ export function getPsycheMetricsViewData() {
220
262
  totalSwears: Number(context.swear_count) || 0,
221
263
  dailyAverage: {
222
264
  rawSwearCount: dailyAverages.rawSwearCount,
223
- swearingMessagePercent: dailyAverages.swearingMessagePercent
265
+ swearingMessagePercent: dailyAverages.swearingMessagePercent,
266
+ averageMaxCumulativeRage: dailyAverages.averageMaxCumulativeRage,
267
+ maxCumulativeRage: dailyAverages.maxCumulativeRage
224
268
  },
225
269
  weeklyAverage: {
226
270
  rawSwearCount: weeklyAverages.rawSwearCount,
227
- swearingMessagePercent: weeklyAverages.swearingMessagePercent
271
+ swearingMessagePercent: weeklyAverages.swearingMessagePercent,
272
+ averageMaxCumulativeRage: weeklyAverages.averageMaxCumulativeRage,
273
+ maxCumulativeRage: weeklyAverages.maxCumulativeRage
228
274
  },
229
275
  sync: {
230
276
  fullSyncCompletedAt: state?.full_sync_completed_at ?? null,
@@ -255,20 +301,23 @@ export function storeDevrageReport(report, options) {
255
301
  const deleteDate = database.prepare(`DELETE FROM psyche_devrage_conversation_measures WHERE date_key = ?`);
256
302
  const insertConversation = database.prepare(`INSERT INTO psyche_devrage_conversation_measures (
257
303
  id, source, conversation_id, date_key, updated_at, messages,
258
- messages_with_swears, swear_count, scanned_at
304
+ messages_with_swears, swear_count, max_cumulative_rage,
305
+ max_swearing_streak, scanned_at
259
306
  )
260
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
307
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
261
308
  ON CONFLICT(source, conversation_id, date_key) DO UPDATE SET
262
309
  updated_at = excluded.updated_at,
263
310
  messages = excluded.messages,
264
311
  messages_with_swears = excluded.messages_with_swears,
265
312
  swear_count = excluded.swear_count,
313
+ max_cumulative_rage = excluded.max_cumulative_rage,
314
+ max_swearing_streak = excluded.max_swearing_streak,
266
315
  scanned_at = excluded.scanned_at`);
267
316
  for (const dateKey of affectedDateKeys) {
268
317
  deleteDate.run(dateKey);
269
318
  }
270
319
  for (const conversation of report.conversations) {
271
- insertConversation.run(stableId("devrage_conversation", conversation.source, conversation.conversationId, conversation.dateKey), conversation.source, conversation.conversationId, conversation.dateKey, conversation.updatedAt, conversation.messages, conversation.messagesWithSwears, conversation.swears, scannedAt);
320
+ insertConversation.run(stableId("devrage_conversation", conversation.source, conversation.conversationId, conversation.dateKey), conversation.source, conversation.conversationId, conversation.dateKey, conversation.updatedAt, conversation.messages, conversation.messagesWithSwears, conversation.swears, conversation.maxCumulativeRage, conversation.maxSwearingStreak, scannedAt);
272
321
  }
273
322
  for (const dateKey of affectedDateKeys) {
274
323
  recomputeMetricMeasuresForDate(dateKey, scannedAt);
@@ -287,16 +336,23 @@ function recomputeMetricMeasuresForDate(dateKey, computedAt) {
287
336
  COUNT(*) AS conversations,
288
337
  COALESCE(SUM(messages), 0) AS messages,
289
338
  COALESCE(SUM(messages_with_swears), 0) AS messages_with_swears,
290
- COALESCE(SUM(swear_count), 0) AS swear_count
339
+ COALESCE(SUM(swear_count), 0) AS swear_count,
340
+ COALESCE(AVG(max_cumulative_rage), 0) AS average_max_cumulative_rage,
341
+ COALESCE(MAX(max_cumulative_rage), 0) AS max_cumulative_rage,
342
+ COALESCE(MAX(max_swearing_streak), 0) AS max_swearing_streak
291
343
  FROM psyche_devrage_conversation_measures
292
344
  WHERE date_key = ?`)
293
345
  .get(dateKey);
294
346
  const messages = Number(aggregate.messages) || 0;
295
347
  const messagesWithSwears = Number(aggregate.messages_with_swears) || 0;
296
348
  const swearCount = Number(aggregate.swear_count) || 0;
349
+ const averageMaxCumulativeRage = Number(aggregate.average_max_cumulative_rage) || 0;
350
+ const maxCumulativeRage = Number(aggregate.max_cumulative_rage) || 0;
297
351
  const percent = messages > 0 ? (messagesWithSwears / messages) * 100 : 0;
298
352
  upsertMetricMeasure(dateKey, SWEAR_COUNT_KEY, swearCount, "count", Number(aggregate.conversations) || 0, computedAt);
299
353
  upsertMetricMeasure(dateKey, SWEARING_MESSAGE_PERCENT_KEY, percent, "percent", messages, computedAt);
354
+ upsertMetricMeasure(dateKey, AVERAGE_MAX_CUMULATIVE_RAGE_KEY, averageMaxCumulativeRage, "score", Number(aggregate.conversations) || 0, computedAt);
355
+ upsertMetricMeasure(dateKey, MAX_CUMULATIVE_RAGE_KEY, maxCumulativeRage, "score", Number(aggregate.conversations) || 0, computedAt);
300
356
  }
301
357
  function upsertMetricMeasure(dateKey, metricKey, value, unit, sampleCount, computedAt) {
302
358
  getDatabase()
@@ -339,7 +395,10 @@ function getDevrageDailyHistory(limit) {
339
395
  COUNT(*) AS conversations,
340
396
  COALESCE(SUM(messages), 0) AS messages,
341
397
  COALESCE(SUM(messages_with_swears), 0) AS messages_with_swears,
342
- COALESCE(SUM(swear_count), 0) AS swear_count
398
+ COALESCE(SUM(swear_count), 0) AS swear_count,
399
+ COALESCE(AVG(max_cumulative_rage), 0) AS average_max_cumulative_rage,
400
+ COALESCE(MAX(max_cumulative_rage), 0) AS max_cumulative_rage,
401
+ COALESCE(MAX(max_swearing_streak), 0) AS max_swearing_streak
343
402
  FROM psyche_devrage_conversation_measures
344
403
  GROUP BY date_key
345
404
  ORDER BY date_key DESC
@@ -352,6 +411,9 @@ function getDevrageDailyHistory(limit) {
352
411
  dateKey: row.date_key,
353
412
  rawSwearCount: Number(row.swear_count) || 0,
354
413
  swearingMessagePercent: messages > 0 ? (messagesWithSwears / messages) * 100 : 0,
414
+ averageMaxCumulativeRage: Number(row.average_max_cumulative_rage) || 0,
415
+ maxCumulativeRage: Number(row.max_cumulative_rage) || 0,
416
+ maxSwearingStreak: Number(row.max_swearing_streak) || 0,
355
417
  conversationsScanned: Number(row.conversations) || 0,
356
418
  messagesScanned: messages,
357
419
  messagesWithSwears
@@ -375,9 +437,13 @@ function getMetricAverages(days) {
375
437
  .all(...(days ? [days] : []));
376
438
  const swearAverage = rows.find((row) => row.metric_key === SWEAR_COUNT_KEY)?.value ?? 0;
377
439
  const percentAverage = rows.find((row) => row.metric_key === SWEARING_MESSAGE_PERCENT_KEY)?.value ?? 0;
440
+ const averageMaxCumulativeRage = rows.find((row) => row.metric_key === AVERAGE_MAX_CUMULATIVE_RAGE_KEY)?.value ?? 0;
441
+ const maxCumulativeRage = rows.find((row) => row.metric_key === MAX_CUMULATIVE_RAGE_KEY)?.value ?? 0;
378
442
  return {
379
443
  rawSwearCount: round(Number(swearAverage) || 0, 1),
380
- swearingMessagePercent: round(Number(percentAverage) || 0, 1)
444
+ swearingMessagePercent: round(Number(percentAverage) || 0, 1),
445
+ averageMaxCumulativeRage: round(Number(averageMaxCumulativeRage) || 0, 1),
446
+ maxCumulativeRage: round(Number(maxCumulativeRage) || 0, 1)
381
447
  };
382
448
  }
383
449
  function getDevrageConversationTotals() {
@@ -387,7 +453,10 @@ function getDevrageConversationTotals() {
387
453
  COUNT(DISTINCT source) AS sources,
388
454
  COALESCE(SUM(messages), 0) AS messages,
389
455
  COALESCE(SUM(messages_with_swears), 0) AS messages_with_swears,
390
- COALESCE(SUM(swear_count), 0) AS swear_count
456
+ COALESCE(SUM(swear_count), 0) AS swear_count,
457
+ COALESCE(AVG(max_cumulative_rage), 0) AS average_max_cumulative_rage,
458
+ COALESCE(MAX(max_cumulative_rage), 0) AS max_cumulative_rage,
459
+ COALESCE(MAX(max_swearing_streak), 0) AS max_swearing_streak
391
460
  FROM psyche_devrage_conversation_measures`)
392
461
  .get();
393
462
  }
@@ -2,7 +2,7 @@
2
2
  "id": "forge-openclaw-plugin",
3
3
  "name": "Forge",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
- "version": "0.3.0",
5
+ "version": "0.3.2",
6
6
  "activation": {
7
7
  "onStartup": true,
8
8
  "onCapabilities": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forge-openclaw-plugin",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Curated OpenClaw adapter for the Forge collaboration API, UI entrypoint, and localhost auto-start runtime.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",