@rudderhq/server 0.2.0-canary.9 → 0.2.0

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 (192) hide show
  1. package/dist/bootstrap/register-api-routes.d.ts.map +1 -1
  2. package/dist/bootstrap/register-api-routes.js +2 -0
  3. package/dist/bootstrap/register-api-routes.js.map +1 -1
  4. package/dist/bundled-plugins/plugin-linear/dist/ui/index.js +8 -1
  5. package/dist/bundled-plugins/plugin-linear/dist/ui/index.js.map +2 -2
  6. package/dist/bundled-plugins/plugin-linear/dist/worker.js +17 -3
  7. package/dist/bundled-plugins/plugin-linear/dist/worker.js.map +2 -2
  8. package/dist/home-paths.d.ts +2 -0
  9. package/dist/home-paths.d.ts.map +1 -1
  10. package/dist/home-paths.js +6 -1
  11. package/dist/home-paths.js.map +1 -1
  12. package/dist/index.d.ts +11 -0
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +55 -2
  15. package/dist/index.js.map +1 -1
  16. package/dist/langfuse-transcript.d.ts.map +1 -1
  17. package/dist/langfuse-transcript.js +16 -2
  18. package/dist/langfuse-transcript.js.map +1 -1
  19. package/dist/middleware/auth.d.ts.map +1 -1
  20. package/dist/middleware/auth.js +54 -1
  21. package/dist/middleware/auth.js.map +1 -1
  22. package/dist/onboarding-assets/ceo/HEARTBEAT.md +8 -4
  23. package/dist/onboarding-assets/default/HEARTBEAT.md +7 -4
  24. package/dist/routes/agents.d.ts.map +1 -1
  25. package/dist/routes/agents.js +62 -3
  26. package/dist/routes/agents.js.map +1 -1
  27. package/dist/routes/approvals.d.ts.map +1 -1
  28. package/dist/routes/approvals.js +30 -1
  29. package/dist/routes/approvals.js.map +1 -1
  30. package/dist/routes/chats.d.ts.map +1 -1
  31. package/dist/routes/chats.js +285 -46
  32. package/dist/routes/chats.js.map +1 -1
  33. package/dist/routes/costs.d.ts.map +1 -1
  34. package/dist/routes/costs.js +20 -0
  35. package/dist/routes/costs.js.map +1 -1
  36. package/dist/routes/issues.d.ts.map +1 -1
  37. package/dist/routes/issues.js +229 -19
  38. package/dist/routes/issues.js.map +1 -1
  39. package/dist/routes/onboarding.d.ts +3 -0
  40. package/dist/routes/onboarding.d.ts.map +1 -0
  41. package/dist/routes/onboarding.js +545 -0
  42. package/dist/routes/onboarding.js.map +1 -0
  43. package/dist/routes/orgs.d.ts.map +1 -1
  44. package/dist/routes/orgs.js +22 -0
  45. package/dist/routes/orgs.js.map +1 -1
  46. package/dist/services/activity.d.ts.map +1 -1
  47. package/dist/services/activity.js +32 -1
  48. package/dist/services/activity.js.map +1 -1
  49. package/dist/services/agent-run-context.d.ts +1 -0
  50. package/dist/services/agent-run-context.d.ts.map +1 -1
  51. package/dist/services/agent-run-context.js +1 -0
  52. package/dist/services/agent-run-context.js.map +1 -1
  53. package/dist/services/agents.d.ts +13 -13
  54. package/dist/services/automations.d.ts +2 -2
  55. package/dist/services/calendar.d.ts +4 -4
  56. package/dist/services/chat-assistant.d.ts +10 -1
  57. package/dist/services/chat-assistant.d.ts.map +1 -1
  58. package/dist/services/chat-assistant.js +102 -3
  59. package/dist/services/chat-assistant.js.map +1 -1
  60. package/dist/services/chats.d.ts +87 -12
  61. package/dist/services/chats.d.ts.map +1 -1
  62. package/dist/services/chats.js +185 -10
  63. package/dist/services/chats.js.map +1 -1
  64. package/dist/services/costs.d.ts +21 -0
  65. package/dist/services/costs.d.ts.map +1 -1
  66. package/dist/services/costs.js +76 -2
  67. package/dist/services/costs.js.map +1 -1
  68. package/dist/services/finance.d.ts +2 -2
  69. package/dist/services/goals.d.ts +12 -12
  70. package/dist/services/instance-settings.d.ts.map +1 -1
  71. package/dist/services/instance-settings.js +25 -16
  72. package/dist/services/instance-settings.js.map +1 -1
  73. package/dist/services/issue-review-wakeup.d.ts +49 -1
  74. package/dist/services/issue-review-wakeup.d.ts.map +1 -1
  75. package/dist/services/issue-review-wakeup.js +39 -2
  76. package/dist/services/issue-review-wakeup.js.map +1 -1
  77. package/dist/services/issues.d.ts +2 -1
  78. package/dist/services/issues.d.ts.map +1 -1
  79. package/dist/services/issues.js +126 -5
  80. package/dist/services/issues.js.map +1 -1
  81. package/dist/services/knowledge-portability/organization-skills.d.ts +1 -0
  82. package/dist/services/knowledge-portability/organization-skills.d.ts.map +1 -1
  83. package/dist/services/knowledge-portability/organization-skills.js +3 -2
  84. package/dist/services/knowledge-portability/organization-skills.js.map +1 -1
  85. package/dist/services/messenger.d.ts +5 -0
  86. package/dist/services/messenger.d.ts.map +1 -1
  87. package/dist/services/messenger.js +147 -9
  88. package/dist/services/messenger.js.map +1 -1
  89. package/dist/services/organization-workspace-browser.d.ts.map +1 -1
  90. package/dist/services/organization-workspace-browser.js +64 -9
  91. package/dist/services/organization-workspace-browser.js.map +1 -1
  92. package/dist/services/orgs.d.ts +1 -1
  93. package/dist/services/plugin-registry.d.ts +4 -4
  94. package/dist/services/projects.d.ts +1 -1
  95. package/dist/services/runtime-kernel/heartbeat.d.ts.map +1 -1
  96. package/dist/services/runtime-kernel/heartbeat.js +567 -29
  97. package/dist/services/runtime-kernel/heartbeat.js.map +1 -1
  98. package/dist/services/secrets.d.ts +5 -5
  99. package/dist/services/workspace-backups.d.ts.map +1 -1
  100. package/dist/services/workspace-backups.js +6 -0
  101. package/dist/services/workspace-backups.js.map +1 -1
  102. package/dist/services/workspace-runtime.d.ts.map +1 -1
  103. package/dist/services/workspace-runtime.js +2 -0
  104. package/dist/services/workspace-runtime.js.map +1 -1
  105. package/package.json +13 -13
  106. package/resources/bundled-skills/rudder/SKILL.md +72 -7
  107. package/resources/bundled-skills/rudder/references/cli-reference.md +34 -9
  108. package/resources/bundled-skills/rudder/references/organization-skills.md +12 -7
  109. package/resources/bundled-skills/rudder-create-agent/references/cli-reference.md +1 -0
  110. package/skills/rudder/SKILL.md +72 -7
  111. package/skills/rudder/references/cli-reference.md +34 -9
  112. package/skills/rudder/references/organization-skills.md +12 -7
  113. package/skills/rudder-create-agent/references/cli-reference.md +1 -0
  114. package/ui-dist/assets/{_basePickBy-aX2f6dVl.js → _basePickBy-3Hg7N37c.js} +1 -1
  115. package/ui-dist/assets/{_baseUniq-BYwL7heN.js → _baseUniq-Bvy8WJh0.js} +1 -1
  116. package/ui-dist/assets/{arc-BG9f5pwY.js → arc-DrmvGX4U.js} +1 -1
  117. package/ui-dist/assets/{architectureDiagram-2XIMDMQ5-BFFQoJJ1.js → architectureDiagram-2XIMDMQ5-vbevcV-8.js} +1 -1
  118. package/ui-dist/assets/{blockDiagram-WCTKOSBZ-Bvx1IB1z.js → blockDiagram-WCTKOSBZ-DvupMRN9.js} +1 -1
  119. package/ui-dist/assets/{c4Diagram-IC4MRINW-DJbCE4sh.js → c4Diagram-IC4MRINW-CbsNVA8e.js} +1 -1
  120. package/ui-dist/assets/channel-DhW0A-FV.js +1 -0
  121. package/ui-dist/assets/{chunk-4BX2VUAB-BOVbLFsN.js → chunk-4BX2VUAB-BL4OUqNV.js} +1 -1
  122. package/ui-dist/assets/{chunk-55IACEB6-D5pKj6S9.js → chunk-55IACEB6-DFwq2ebc.js} +1 -1
  123. package/ui-dist/assets/{chunk-FMBD7UC4-OY5xuJeR.js → chunk-FMBD7UC4-Cyl6kF9G.js} +1 -1
  124. package/ui-dist/assets/{chunk-JSJVCQXG-C5X67KZg.js → chunk-JSJVCQXG-v4mfLtsY.js} +1 -1
  125. package/ui-dist/assets/{chunk-KX2RTZJC-C-4PZ9Q_.js → chunk-KX2RTZJC-Bfg48g5k.js} +1 -1
  126. package/ui-dist/assets/{chunk-NQ4KR5QH-XysPlqPj.js → chunk-NQ4KR5QH-BcSdbequ.js} +1 -1
  127. package/ui-dist/assets/{chunk-QZHKN3VN-B5wEbFHo.js → chunk-QZHKN3VN-BT8QI712.js} +1 -1
  128. package/ui-dist/assets/{chunk-WL4C6EOR-BanwYFa2.js → chunk-WL4C6EOR-CqH2or9g.js} +1 -1
  129. package/ui-dist/assets/classDiagram-VBA2DB6C-Bw6kzUsz.js +1 -0
  130. package/ui-dist/assets/classDiagram-v2-RAHNMMFH-Bw6kzUsz.js +1 -0
  131. package/ui-dist/assets/clone-Luak8Fsn.js +1 -0
  132. package/ui-dist/assets/{cose-bilkent-S5V4N54A-Cd4q2swD.js → cose-bilkent-S5V4N54A-CLH06Lnz.js} +1 -1
  133. package/ui-dist/assets/{dagre-KLK3FWXG-B_VyOhf3.js → dagre-KLK3FWXG-DxNQPDBj.js} +1 -1
  134. package/ui-dist/assets/{diagram-E7M64L7V-BRoG4Mz6.js → diagram-E7M64L7V-BOcSeWh0.js} +1 -1
  135. package/ui-dist/assets/{diagram-IFDJBPK2-CRU_A9p9.js → diagram-IFDJBPK2-DXyaFKVr.js} +1 -1
  136. package/ui-dist/assets/{diagram-P4PSJMXO-BYSQDbfb.js → diagram-P4PSJMXO-DhY_ls3C.js} +1 -1
  137. package/ui-dist/assets/{erDiagram-INFDFZHY-v5j1kyWr.js → erDiagram-INFDFZHY-QtL5Yt_b.js} +1 -1
  138. package/ui-dist/assets/{flowDiagram-PKNHOUZH-C06ZQgTj.js → flowDiagram-PKNHOUZH-BYqyaowc.js} +1 -1
  139. package/ui-dist/assets/{ganttDiagram-A5KZAMGK-Dw9p5nQ1.js → ganttDiagram-A5KZAMGK-D4xd7J_z.js} +1 -1
  140. package/ui-dist/assets/{gitGraphDiagram-K3NZZRJ6-CrpXRIaP.js → gitGraphDiagram-K3NZZRJ6-Co9xqKNH.js} +1 -1
  141. package/ui-dist/assets/{graph-ClTUmULf.js → graph-DEC7S98H.js} +1 -1
  142. package/ui-dist/assets/{index-sLGLHxIu.js → index-4_gJOU3u.js} +1 -1
  143. package/ui-dist/assets/{index-Cr7n11UG.js → index-B8QjK4Xd.js} +1 -1
  144. package/ui-dist/assets/index-BLDnKx7N.js +1478 -0
  145. package/ui-dist/assets/{index-DxzAgTWd.js → index-BX6QyxsL.js} +1 -1
  146. package/ui-dist/assets/{index-CIr7H9OI.js → index-BZGiyL9p.js} +1 -1
  147. package/ui-dist/assets/{index-DK13xhRv.js → index-BelfAyHh.js} +1 -1
  148. package/ui-dist/assets/index-BisI78wU.css +1 -0
  149. package/ui-dist/assets/{index-D-6z8wxx.js → index-Bm86s0IY.js} +1 -1
  150. package/ui-dist/assets/{index-T81awgzh.js → index-Bz0jEwWG.js} +1 -1
  151. package/ui-dist/assets/{index-CqYInp-c.js → index-CFANc8oH.js} +1 -1
  152. package/ui-dist/assets/{index-Btwy7Cp-.js → index-CIAMqUzr.js} +1 -1
  153. package/ui-dist/assets/{index-L6M3nVxh.js → index-ClrueuiI.js} +1 -1
  154. package/ui-dist/assets/{index-C_BTFRTX.js → index-CpxwEuIg.js} +1 -1
  155. package/ui-dist/assets/{index-CQWmziMF.js → index-D1ZkASZY.js} +1 -1
  156. package/ui-dist/assets/{index-DWFMs9uk.js → index-DUP0i_Iv.js} +1 -1
  157. package/ui-dist/assets/{index-BVfM9ax8.js → index-DawkXomB.js} +1 -1
  158. package/ui-dist/assets/{index-DNlWBtHa.js → index-DxchV0Z7.js} +1 -1
  159. package/ui-dist/assets/{index-DkDkjZ-D.js → index-Dzd88G_H.js} +1 -1
  160. package/ui-dist/assets/{index-BvGogi9q.js → index-SklGX83C.js} +1 -1
  161. package/ui-dist/assets/{index-Bpc2gRVo.js → index-_xX3B4n0.js} +1 -1
  162. package/ui-dist/assets/{index-_x9smX4T.js → index-bVqVfFu5.js} +1 -1
  163. package/ui-dist/assets/{index-DAhPD1Ss.js → index-eIjkqSkc.js} +1 -1
  164. package/ui-dist/assets/{index-4uxadHo5.js → index-mIrYeZR2.js} +1 -1
  165. package/ui-dist/assets/{index-BYC_xlrx.js → index-xg2FQeSA.js} +1 -1
  166. package/ui-dist/assets/{infoDiagram-LFFYTUFH-BA3VxOIU.js → infoDiagram-LFFYTUFH-BQ0qsBJ6.js} +1 -1
  167. package/ui-dist/assets/{ishikawaDiagram-PHBUUO56-DGrizi0S.js → ishikawaDiagram-PHBUUO56-B1u2RAnY.js} +1 -1
  168. package/ui-dist/assets/{journeyDiagram-4ABVD52K-6ey34a7e.js → journeyDiagram-4ABVD52K-Dv5wJGwT.js} +1 -1
  169. package/ui-dist/assets/{kanban-definition-K7BYSVSG-CwNnmsam.js → kanban-definition-K7BYSVSG-CJOykCsT.js} +1 -1
  170. package/ui-dist/assets/{layout-buNxvllr.js → layout-BDcM6t-f.js} +1 -1
  171. package/ui-dist/assets/{linear-BPWhxaRl.js → linear-B9Sm5Y96.js} +1 -1
  172. package/ui-dist/assets/{mermaid.core-Cajx0s-z.js → mermaid.core-lZPaf_Ix.js} +4 -4
  173. package/ui-dist/assets/{mindmap-definition-YRQLILUH-Bf5InEp-.js → mindmap-definition-YRQLILUH-Cu4HfP8K.js} +1 -1
  174. package/ui-dist/assets/{pieDiagram-SKSYHLDU-CZFz7NWC.js → pieDiagram-SKSYHLDU-B_v-Vluc.js} +1 -1
  175. package/ui-dist/assets/{quadrantDiagram-337W2JSQ-XBmKVoc9.js → quadrantDiagram-337W2JSQ-BU1ZwGcS.js} +1 -1
  176. package/ui-dist/assets/{requirementDiagram-Z7DCOOCP-BkgdDv0H.js → requirementDiagram-Z7DCOOCP-DBOqB50G.js} +1 -1
  177. package/ui-dist/assets/{sankeyDiagram-WA2Y5GQK-CASFR28i.js → sankeyDiagram-WA2Y5GQK-CsXDIOlq.js} +1 -1
  178. package/ui-dist/assets/{sequenceDiagram-2WXFIKYE-BzY7LMRv.js → sequenceDiagram-2WXFIKYE-Cmgr7vKy.js} +1 -1
  179. package/ui-dist/assets/{stateDiagram-RAJIS63D-C9UMSk36.js → stateDiagram-RAJIS63D-Bd0uRbWd.js} +1 -1
  180. package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-qGaY7iN1.js +1 -0
  181. package/ui-dist/assets/{timeline-definition-YZTLITO2-D6m4R4xe.js → timeline-definition-YZTLITO2-B9OfCgYQ.js} +1 -1
  182. package/ui-dist/assets/{treemap-KZPCXAKY-7V9mnT8T.js → treemap-KZPCXAKY-FWWMNo03.js} +1 -1
  183. package/ui-dist/assets/{vennDiagram-LZ73GAT5-Ci-sfAyq.js → vennDiagram-LZ73GAT5-CGs3T7cn.js} +1 -1
  184. package/ui-dist/assets/{xychartDiagram-JWTSCODW-BayXhRSu.js → xychartDiagram-JWTSCODW-BJ6DrP1k.js} +1 -1
  185. package/ui-dist/index.html +2 -2
  186. package/ui-dist/assets/channel-ClX7n84B.js +0 -1
  187. package/ui-dist/assets/classDiagram-VBA2DB6C-DvWbsnVz.js +0 -1
  188. package/ui-dist/assets/classDiagram-v2-RAHNMMFH-DvWbsnVz.js +0 -1
  189. package/ui-dist/assets/clone-Dla3A8ZA.js +0 -1
  190. package/ui-dist/assets/index-CSANx6ee.css +0 -1
  191. package/ui-dist/assets/index-DCa9-Sy-.js +0 -1439
  192. package/ui-dist/assets/stateDiagram-v2-FVOUBMTO-DWVhbAdj.js +0 -1
@@ -20,7 +20,7 @@ import { summarizeRuntimeSkillsForTrace } from "../runtime-trace-metadata.js";
20
20
  import { buildWorkspaceReadyComment, cleanupExecutionWorkspaceArtifacts, ensureRuntimeServicesForRun, persistAdapterManagedRuntimeServices, realizeExecutionWorkspace, releaseRuntimeServicesForRun, } from "../workspace-runtime.js";
21
21
  import { issueService } from "../issues.js";
22
22
  import { documentService } from "../documents.js";
23
- import { buildIssueConvergenceReviewWakeupOptions } from "../issue-review-wakeup.js";
23
+ import { buildIssueConvergenceReviewWakeupOptions, buildIssueReviewCloseoutWakeupOptions, } from "../issue-review-wakeup.js";
24
24
  import { executionWorkspaceService } from "../execution-workspaces.js";
25
25
  import { buildObservedRunLangfuseScores } from "../run-intelligence.js";
26
26
  import { workspaceOperationService } from "../workspace-operations.js";
@@ -48,6 +48,9 @@ const ISSUE_PASSIVE_FOLLOWUP_REASON = "issue_passive_followup";
48
48
  const ISSUE_PASSIVE_FOLLOWUP_WAKE_SOURCE = "passive_issue_followup";
49
49
  const ISSUE_PASSIVE_FOLLOWUP_FAILURE_REASON = "missing_closure";
50
50
  const ISSUE_PASSIVE_FOLLOWUP_MAX_ATTEMPTS = 2;
51
+ const ISSUE_REVIEW_CLOSEOUT_REASON = "issue_review_closeout_missing";
52
+ const ISSUE_REVIEW_CLOSEOUT_FAILURE_REASON = "missing_review_decision";
53
+ const ISSUE_REVIEW_CLOSEOUT_MAX_ATTEMPTS = 2;
51
54
  const ISSUE_PASSIVE_FOLLOWUP_COOLDOWN_MS_BY_ATTEMPT = new Map([
52
55
  [1, 2 * 60 * 1000],
53
56
  [2, 5 * 60 * 1000],
@@ -184,15 +187,42 @@ export function buildHeartbeatAdapterInvokePayload(input) {
184
187
  .map((entry) => normalizeLoadedSkill(entry))
185
188
  .filter((entry) => Boolean(entry))
186
189
  : [];
187
- const usedSkills = explicitUsedSkills.length > 0
188
- ? explicitUsedSkills
189
- : inferUsedSkillsFromPrompt(input.meta.prompt, input.runtimeSkills);
190
+ const promptRequestedSkills = inferUsedSkillsFromPrompt(input.meta.prompt, input.runtimeSkills);
191
+ const loadedSkills = Array.isArray(input.meta.loadedSkills) && input.meta.loadedSkills.length > 0
192
+ ? input.meta.loadedSkills
193
+ .map((entry) => normalizeLoadedSkillForPayload(entry))
194
+ .filter((entry) => Boolean(entry))
195
+ : input.runtimeSkills
196
+ .map((entry) => ({
197
+ key: entry.key,
198
+ runtimeName: entry.runtimeName ?? null,
199
+ name: entry.name ?? null,
200
+ description: entry.description ?? null,
201
+ }));
202
+ const loadedSkillEvidence = loadedSkills
203
+ .map((entry) => normalizeLoadedSkill(entry))
204
+ .filter((entry) => Boolean(entry));
205
+ const skillEvidence = resolveSkillEvidence({
206
+ usedSkills: explicitUsedSkills,
207
+ requestedSkills: promptRequestedSkills,
208
+ loadedSkills: loadedSkillEvidence,
209
+ });
190
210
  return {
191
211
  ...input.meta,
192
212
  ...summarizeRuntimeSkillsForTrace(input.runtimeSkills),
193
- usedSkillCount: usedSkills.length,
194
- usedSkillKeys: usedSkills.map((entry) => entry.key),
195
- usedSkills,
213
+ loadedSkillCount: loadedSkills.length,
214
+ loadedSkillKeys: loadedSkills.map((entry) => entry.key),
215
+ loadedSkills,
216
+ usedSkillCount: explicitUsedSkills.length,
217
+ usedSkillKeys: explicitUsedSkills.map((entry) => entry.key),
218
+ usedSkills: explicitUsedSkills,
219
+ promptRequestedSkillCount: promptRequestedSkills.length,
220
+ promptRequestedSkillKeys: promptRequestedSkills.map((entry) => entry.key),
221
+ promptRequestedSkills,
222
+ skillEvidenceType: skillEvidence.evidence,
223
+ skillEvidenceCount: skillEvidence.skills.length,
224
+ skillEvidenceKeys: skillEvidence.skills.map((entry) => entry.key),
225
+ skillEvidenceSkills: skillEvidence.skills,
196
226
  };
197
227
  }
198
228
  function buildRecentDateKeys(windowDays, now) {
@@ -238,6 +268,151 @@ function normalizeLoadedSkill(value) {
238
268
  const label = rawRuntimeName ?? rawName ?? fallbackSkillLabel(key);
239
269
  return { key, label };
240
270
  }
271
+ function normalizeLoadedSkillForPayload(value) {
272
+ const skill = parseObject(value);
273
+ const rawKey = readNonEmptyString(skill.key);
274
+ const rawRuntimeName = readNonEmptyString(skill.runtimeName);
275
+ const rawName = readNonEmptyString(skill.name);
276
+ const key = rawKey ?? rawRuntimeName ?? rawName;
277
+ if (!key)
278
+ return null;
279
+ return {
280
+ key,
281
+ runtimeName: rawRuntimeName ?? null,
282
+ name: rawName ?? null,
283
+ description: readNonEmptyString(skill.description) ?? null,
284
+ };
285
+ }
286
+ function emptySkillEvidenceCounts() {
287
+ return { used: 0, requested: 0, loaded: 0 };
288
+ }
289
+ function incrementSkillEvidenceCount(counts, evidence) {
290
+ counts[evidence] += 1;
291
+ }
292
+ function strongestSkillEvidence(left, right) {
293
+ const rank = {
294
+ used: 3,
295
+ requested: 2,
296
+ loaded: 1,
297
+ };
298
+ return rank[right] > rank[left] ? right : left;
299
+ }
300
+ function resolveSkillEvidence(input) {
301
+ if (input.usedSkills.length > 0)
302
+ return { evidence: "used", skills: input.usedSkills };
303
+ if (input.requestedSkills.length > 0)
304
+ return { evidence: "requested", skills: input.requestedSkills };
305
+ return { evidence: "loaded", skills: [] };
306
+ }
307
+ function readSkillEvidenceFromPayload(payload) {
308
+ const loadedSkills = Array.isArray(payload.loadedSkills)
309
+ ? payload.loadedSkills
310
+ .map((entry) => normalizeLoadedSkill(entry))
311
+ .filter((entry) => Boolean(entry))
312
+ : [];
313
+ const usedSkills = Array.isArray(payload.usedSkills)
314
+ ? payload.usedSkills
315
+ .map((entry) => normalizeLoadedSkill(entry))
316
+ .filter((entry) => Boolean(entry))
317
+ : [];
318
+ const requestedSkills = Array.isArray(payload.promptRequestedSkills)
319
+ ? payload.promptRequestedSkills
320
+ .map((entry) => normalizeLoadedSkill(entry))
321
+ .filter((entry) => Boolean(entry))
322
+ : inferUsedSkillsFromPrompt(payload.prompt, loadedSkills);
323
+ return resolveSkillEvidence({ usedSkills, requestedSkills, loadedSkills });
324
+ }
325
+ function extractSkillSlugFromPath(value) {
326
+ const normalized = value.trim().replace(/\\/g, "/").replace(/[?#].*$/u, "").replace(/\/+$/u, "");
327
+ if (!normalized.endsWith("/SKILL.md"))
328
+ return null;
329
+ const parts = normalized.split("/").filter(Boolean);
330
+ if (parts.length < 2)
331
+ return null;
332
+ const slug = parts.at(-2);
333
+ if (!slug || slug === "." || slug === "..")
334
+ return null;
335
+ return slug;
336
+ }
337
+ function collectSkillPathsFromText(value) {
338
+ const paths = [];
339
+ const pattern = /(?:^|[\s"'`(=])((?:\.{1,2}\/|~\/|\/)?(?:[^\s"'`()<>|;&/]+\/)+SKILL\.md(?:[?#][^\s"'`()<>|;&]*)?)(?=$|[\s"'`()<>|;&])/giu;
340
+ for (const match of value.matchAll(pattern)) {
341
+ const pathValue = match[1]?.trim();
342
+ if (pathValue)
343
+ paths.push(pathValue);
344
+ }
345
+ return paths;
346
+ }
347
+ function collectStringValues(value, depth = 0) {
348
+ if (depth > 4)
349
+ return [];
350
+ if (typeof value === "string")
351
+ return [value];
352
+ if (Array.isArray(value))
353
+ return value.flatMap((entry) => collectStringValues(entry, depth + 1));
354
+ const record = parseObject(value);
355
+ return Object.values(record).flatMap((entry) => collectStringValues(entry, depth + 1));
356
+ }
357
+ function normalizeSkillUseFromPath(value) {
358
+ const slug = extractSkillSlugFromPath(value);
359
+ if (!slug)
360
+ return null;
361
+ return { key: slug, label: slug };
362
+ }
363
+ function dedupeSkillUses(skills) {
364
+ const seen = new Set();
365
+ const result = [];
366
+ for (const skill of skills) {
367
+ const normalized = normalizeLoadedSkill(skill);
368
+ if (!normalized || seen.has(normalized.key))
369
+ continue;
370
+ seen.add(normalized.key);
371
+ result.push(normalized);
372
+ }
373
+ return result;
374
+ }
375
+ function collectSkillUsesFromText(value) {
376
+ return collectSkillPathsFromText(value)
377
+ .map((entry) => normalizeSkillUseFromPath(entry))
378
+ .filter((entry) => Boolean(entry));
379
+ }
380
+ function readToolCommandInput(input) {
381
+ if (typeof input === "string")
382
+ return input;
383
+ const record = parseObject(input);
384
+ return readNonEmptyString(record.command) ?? readNonEmptyString(record.cmd);
385
+ }
386
+ function isCommandTranscriptTool(name) {
387
+ const normalized = name.trim().toLowerCase();
388
+ return ["command_execution", "shell", "shelltoolcall", "bash"].includes(normalized);
389
+ }
390
+ function isReadTranscriptTool(name) {
391
+ const normalized = name.trim().toLowerCase();
392
+ if (isCommandTranscriptTool(normalized))
393
+ return true;
394
+ if (/(?:^|[_-])(read|fetch|open|cat)(?:$|[_-])/.test(normalized))
395
+ return true;
396
+ return false;
397
+ }
398
+ function inferUsedSkillsFromTranscript(transcript) {
399
+ const skills = [];
400
+ for (const entry of transcript) {
401
+ if (entry.kind !== "tool_call")
402
+ continue;
403
+ if (!isReadTranscriptTool(entry.name))
404
+ continue;
405
+ const command = readToolCommandInput(entry.input);
406
+ if (command) {
407
+ skills.push(...collectSkillUsesFromText(command));
408
+ continue;
409
+ }
410
+ for (const value of collectStringValues(entry.input)) {
411
+ skills.push(...collectSkillUsesFromText(value));
412
+ }
413
+ }
414
+ return dedupeSkillUses(skills);
415
+ }
241
416
  function normalizeSkillCandidate(value) {
242
417
  return value
243
418
  ?.trim()
@@ -787,6 +962,20 @@ function normalizePassiveFollowupContext(raw) {
787
962
  queuedAt: readNonEmptyString(parsed.queuedAt),
788
963
  };
789
964
  }
965
+ function normalizeReviewCloseoutContext(raw) {
966
+ const parsed = parseObject(raw);
967
+ const originRunId = readNonEmptyString(parsed.originRunId);
968
+ if (!originRunId)
969
+ return null;
970
+ const attempt = Math.max(0, Math.floor(asNumber(parsed.attempt, 0)));
971
+ return {
972
+ originRunId,
973
+ previousRunId: readNonEmptyString(parsed.previousRunId),
974
+ attempt,
975
+ maxAttempts: Math.max(1, Math.floor(asNumber(parsed.maxAttempts, ISSUE_REVIEW_CLOSEOUT_MAX_ATTEMPTS))),
976
+ reason: ISSUE_REVIEW_CLOSEOUT_FAILURE_REASON,
977
+ };
978
+ }
790
979
  function passiveFollowupCooldownMs(attempt) {
791
980
  return ISSUE_PASSIVE_FOLLOWUP_COOLDOWN_MS_BY_ATTEMPT.get(attempt) ?? 5 * 60 * 1000;
792
981
  }
@@ -1783,6 +1972,15 @@ export function heartbeatService(db) {
1783
1972
  .then((rows) => rows[0] ?? null);
1784
1973
  return Boolean(commentActivity);
1785
1974
  }
1975
+ async function runHasIssueReviewDecision(tx, run, issueId) {
1976
+ const decisionActivity = await tx
1977
+ .select({ id: activityLog.id })
1978
+ .from(activityLog)
1979
+ .where(and(eq(activityLog.orgId, run.orgId), eq(activityLog.action, "issue.review_decision_recorded"), eq(activityLog.entityType, "issue"), eq(activityLog.entityId, issueId), eq(activityLog.runId, run.id)))
1980
+ .limit(1)
1981
+ .then((rows) => rows[0] ?? null);
1982
+ return Boolean(decisionActivity);
1983
+ }
1786
1984
  async function issueHasDeferredWake(tx, orgId, issueId) {
1787
1985
  const deferred = await tx
1788
1986
  .select({ id: agentWakeupRequests.id })
@@ -1813,6 +2011,67 @@ export function heartbeatService(db) {
1813
2011
  .then((rows) => rows[0] ?? null);
1814
2012
  return Boolean(existingReview);
1815
2013
  }
2014
+ async function reviewerCloseoutAlreadyRecorded(tx, runId) {
2015
+ const existingWake = await tx
2016
+ .select({ id: agentWakeupRequests.id })
2017
+ .from(agentWakeupRequests)
2018
+ .where(eq(agentWakeupRequests.idempotencyKey, `${ISSUE_REVIEW_CLOSEOUT_REASON}:${runId}`))
2019
+ .limit(1)
2020
+ .then((rows) => rows[0] ?? null);
2021
+ if (existingWake)
2022
+ return true;
2023
+ const existingReview = await tx
2024
+ .select({ id: activityLog.id })
2025
+ .from(activityLog)
2026
+ .where(and(eq(activityLog.runId, runId), eq(activityLog.action, "issue.review_closure_needs_operator_review")))
2027
+ .limit(1)
2028
+ .then((rows) => rows[0] ?? null);
2029
+ return Boolean(existingReview);
2030
+ }
2031
+ async function issueHasConfirmedBlockedReviewerHandoff(tx, issue, reviewerAgentId) {
2032
+ if (issue.status !== "blocked")
2033
+ return false;
2034
+ const existingHandoff = await tx
2035
+ .select({ id: activityLog.id })
2036
+ .from(activityLog)
2037
+ .where(and(eq(activityLog.orgId, issue.orgId), eq(activityLog.action, "issue.review_decision_recorded"), eq(activityLog.entityType, "issue"), sql `${activityLog.entityId} = ${issue.id}::text`, eq(activityLog.actorType, "agent"), sql `${activityLog.actorId} = ${reviewerAgentId}::text`, sql `${activityLog.details} ->> 'decision' = 'blocked'`, sql `${activityLog.createdAt} >= COALESCE((
2038
+ SELECT MAX(material_activity.created_at)
2039
+ FROM activity_log material_activity
2040
+ WHERE material_activity.org_id = ${issue.orgId}
2041
+ AND material_activity.entity_type = 'issue'
2042
+ AND material_activity.entity_id = ${issue.id}::text
2043
+ AND (
2044
+ (
2045
+ material_activity.action = 'issue.updated'
2046
+ AND jsonb_typeof(material_activity.details) = 'object'
2047
+ AND EXISTS (
2048
+ SELECT 1
2049
+ FROM jsonb_object_keys(material_activity.details) AS detail_key(key)
2050
+ WHERE detail_key.key NOT IN (
2051
+ 'identifier',
2052
+ 'issueIdentifier',
2053
+ '_previous',
2054
+ 'source',
2055
+ 'reopened',
2056
+ 'reopenedFrom',
2057
+ 'normalizedFromStatus',
2058
+ 'normalizedReason'
2059
+ )
2060
+ )
2061
+ )
2062
+ OR (
2063
+ material_activity.action = 'issue.comment_added'
2064
+ AND NOT (
2065
+ material_activity.actor_type = 'agent'
2066
+ AND material_activity.actor_id = ${reviewerAgentId}::text
2067
+ )
2068
+ )
2069
+ )
2070
+ ), to_timestamp(0))`))
2071
+ .limit(1)
2072
+ .then((rows) => rows[0] ?? null);
2073
+ return Boolean(existingHandoff);
2074
+ }
1816
2075
  async function evaluatePassiveIssueClosureForLockedIssue(input) {
1817
2076
  const { tx, run, issue, now } = input;
1818
2077
  const context = parseObject(run.contextSnapshot);
@@ -1821,6 +2080,49 @@ export function heartbeatService(db) {
1821
2080
  return { kind: "none", reason: "run_not_issue_backed" };
1822
2081
  if (run.status !== "succeeded")
1823
2082
  return { kind: "none", reason: "run_not_successful" };
2083
+ const reviewerRun = (issue.status === "in_review" || issue.status === "blocked") &&
2084
+ issue.reviewerAgentId === run.agentId &&
2085
+ (run.invocationSource === "review" ||
2086
+ readNonEmptyString(context.role) === "reviewer" ||
2087
+ readNonEmptyString(context.wakeSource) === "review");
2088
+ if (reviewerRun) {
2089
+ if (await runHasIssueReviewDecision(tx, run, issue.id)) {
2090
+ return { kind: "none", reason: "review_decision_recorded" };
2091
+ }
2092
+ if (await issueHasConfirmedBlockedReviewerHandoff(tx, issue, run.agentId)) {
2093
+ return { kind: "none", reason: "blocked_reviewer_handoff_confirmed" };
2094
+ }
2095
+ if (await issueHasDeferredWake(tx, issue.orgId, issue.id)) {
2096
+ return { kind: "none", reason: "deferred_issue_wake_exists" };
2097
+ }
2098
+ if (await reviewerCloseoutAlreadyRecorded(tx, run.id)) {
2099
+ return { kind: "none", reason: "reviewer_closeout_already_recorded" };
2100
+ }
2101
+ const reviewCloseout = normalizeReviewCloseoutContext(context.reviewCloseout);
2102
+ const currentAttempt = reviewCloseout?.attempt ?? 0;
2103
+ const maxAttempts = reviewCloseout?.maxAttempts ?? ISSUE_REVIEW_CLOSEOUT_MAX_ATTEMPTS;
2104
+ const originRunId = reviewCloseout?.originRunId ?? run.id;
2105
+ if (currentAttempt >= maxAttempts) {
2106
+ return {
2107
+ kind: "reviewer_closeout_operator_review",
2108
+ issue,
2109
+ originRunId,
2110
+ previousRunId: run.id,
2111
+ attempts: currentAttempt,
2112
+ maxAttempts,
2113
+ reason: ISSUE_REVIEW_CLOSEOUT_FAILURE_REASON,
2114
+ };
2115
+ }
2116
+ return {
2117
+ kind: "reviewer_closeout",
2118
+ issue,
2119
+ originRunId,
2120
+ previousRunId: run.id,
2121
+ attempts: currentAttempt + 1,
2122
+ maxAttempts,
2123
+ reason: ISSUE_REVIEW_CLOSEOUT_FAILURE_REASON,
2124
+ };
2125
+ }
1824
2126
  if (issue.status !== "todo" && issue.status !== "in_progress") {
1825
2127
  return { kind: "none", reason: "issue_has_closure_status" };
1826
2128
  }
@@ -2329,6 +2631,7 @@ export function heartbeatService(db) {
2329
2631
  let finalObservationSessionId = heartbeatObservationContext.sessionKey ?? null;
2330
2632
  const runtime = await ensureRuntimeState(agent);
2331
2633
  const context = parseObject(run.contextSnapshot);
2634
+ delete context.rudderGitIdentity;
2332
2635
  const taskKey = deriveTaskKey(context, null);
2333
2636
  const sessionCodec = getAgentRuntimeSessionCodec(agent.agentRuntimeType);
2334
2637
  const issueId = readNonEmptyString(context.issueId);
@@ -3062,6 +3365,25 @@ export function heartbeatService(db) {
3062
3365
  });
3063
3366
  const finalizedRun = await getRun(run.id);
3064
3367
  if (finalizedRun) {
3368
+ const transcriptUsedSkills = inferUsedSkillsFromTranscript(executionTranscript);
3369
+ if (transcriptUsedSkills.length > 0) {
3370
+ await appendRunEvent(finalizedRun, seq++, {
3371
+ eventType: "adapter.skill_usage",
3372
+ stream: "system",
3373
+ level: "info",
3374
+ message: "skill usage inferred from transcript",
3375
+ payload: {
3376
+ source: "transcript.skill_file_read",
3377
+ usedSkillCount: transcriptUsedSkills.length,
3378
+ usedSkillKeys: transcriptUsedSkills.map((entry) => entry.key),
3379
+ usedSkills: transcriptUsedSkills,
3380
+ skillEvidenceType: "used",
3381
+ skillEvidenceCount: transcriptUsedSkills.length,
3382
+ skillEvidenceKeys: transcriptUsedSkills.map((entry) => entry.key),
3383
+ skillEvidenceSkills: transcriptUsedSkills,
3384
+ },
3385
+ });
3386
+ }
3065
3387
  await appendRunEvent(finalizedRun, seq++, {
3066
3388
  eventType: "lifecycle",
3067
3389
  stream: "system",
@@ -3528,6 +3850,96 @@ export function heartbeatService(db) {
3528
3850
  });
3529
3851
  }
3530
3852
  }
3853
+ else if (passiveClosure?.kind === "reviewer_closeout") {
3854
+ await appendRunEvent(run, await nextRunEventSeq(run.id), {
3855
+ eventType: "issue.review_closeout_missing",
3856
+ stream: "system",
3857
+ level: "warn",
3858
+ message: "Reviewer run finished without a structured review decision",
3859
+ payload: {
3860
+ issueId: passiveClosure.issue.id,
3861
+ originRunId: passiveClosure.originRunId,
3862
+ previousRunId: passiveClosure.previousRunId,
3863
+ attempts: passiveClosure.attempts,
3864
+ maxAttempts: passiveClosure.maxAttempts,
3865
+ reason: passiveClosure.reason,
3866
+ },
3867
+ });
3868
+ await logActivity(db, {
3869
+ orgId: passiveClosure.issue.orgId,
3870
+ actorType: "system",
3871
+ actorId: "issue_review_closeout_governance",
3872
+ action: "issue.review_closeout_missing",
3873
+ entityType: "issue",
3874
+ entityId: passiveClosure.issue.id,
3875
+ agentId: run.agentId,
3876
+ runId: run.id,
3877
+ details: {
3878
+ issueId: passiveClosure.issue.id,
3879
+ issueTitle: passiveClosure.issue.title,
3880
+ reviewerAgentId: passiveClosure.issue.reviewerAgentId,
3881
+ originRunId: passiveClosure.originRunId,
3882
+ previousRunId: passiveClosure.previousRunId,
3883
+ attempts: passiveClosure.attempts,
3884
+ maxAttempts: passiveClosure.maxAttempts,
3885
+ reason: passiveClosure.reason,
3886
+ },
3887
+ });
3888
+ if (passiveClosure.issue.reviewerAgentId) {
3889
+ await enqueueWakeup(passiveClosure.issue.reviewerAgentId, {
3890
+ ...buildIssueReviewCloseoutWakeupOptions({
3891
+ issue: passiveClosure.issue,
3892
+ contextSource: "issue.review_closeout_missing",
3893
+ originRunId: passiveClosure.originRunId,
3894
+ previousRunId: passiveClosure.previousRunId,
3895
+ attempts: passiveClosure.attempts,
3896
+ maxAttempts: passiveClosure.maxAttempts,
3897
+ requestedByActorType: "system",
3898
+ requestedByActorId: "issue_review_closeout_governance",
3899
+ }),
3900
+ idempotencyKey: `${ISSUE_REVIEW_CLOSEOUT_REASON}:${run.id}`,
3901
+ }).catch((err) => {
3902
+ logger.warn({ err, issueId: passiveClosure.issue.id }, "failed to wake reviewer after missing review close-out");
3903
+ return null;
3904
+ });
3905
+ }
3906
+ }
3907
+ else if (passiveClosure?.kind === "reviewer_closeout_operator_review") {
3908
+ await appendRunEvent(run, await nextRunEventSeq(run.id), {
3909
+ eventType: "issue.review_closure_needs_operator_review",
3910
+ stream: "system",
3911
+ level: "warn",
3912
+ message: "Reviewer close-out attempts stopped and need operator review",
3913
+ payload: {
3914
+ issueId: passiveClosure.issue.id,
3915
+ originRunId: passiveClosure.originRunId,
3916
+ previousRunId: passiveClosure.previousRunId,
3917
+ attempts: passiveClosure.attempts,
3918
+ maxAttempts: passiveClosure.maxAttempts,
3919
+ reason: passiveClosure.reason,
3920
+ },
3921
+ });
3922
+ await logActivity(db, {
3923
+ orgId: passiveClosure.issue.orgId,
3924
+ actorType: "system",
3925
+ actorId: "issue_review_closeout_governance",
3926
+ action: "issue.review_closure_needs_operator_review",
3927
+ entityType: "issue",
3928
+ entityId: passiveClosure.issue.id,
3929
+ agentId: run.agentId,
3930
+ runId: run.id,
3931
+ details: {
3932
+ issueId: passiveClosure.issue.id,
3933
+ issueTitle: passiveClosure.issue.title,
3934
+ reviewerAgentId: passiveClosure.issue.reviewerAgentId,
3935
+ originRunId: passiveClosure.originRunId,
3936
+ previousRunId: passiveClosure.previousRunId,
3937
+ attempts: passiveClosure.attempts,
3938
+ maxAttempts: passiveClosure.maxAttempts,
3939
+ reason: passiveClosure.reason,
3940
+ },
3941
+ });
3942
+ }
3531
3943
  const promotedRun = outcome.promotedRun;
3532
3944
  if (!promotedRun)
3533
3945
  return;
@@ -4333,60 +4745,184 @@ export function heartbeatService(db) {
4333
4745
  const windowEnd = new Date(`${endDate}T23:59:59.999Z`);
4334
4746
  const rows = await db
4335
4747
  .select({
4748
+ runId: heartbeatRunEvents.runId,
4336
4749
  createdAt: heartbeatRunEvents.createdAt,
4750
+ eventType: heartbeatRunEvents.eventType,
4337
4751
  payload: heartbeatRunEvents.payload,
4338
4752
  })
4339
4753
  .from(heartbeatRunEvents)
4340
- .where(and(eq(heartbeatRunEvents.orgId, scope.orgId), ...(scope.agentId ? [eq(heartbeatRunEvents.agentId, scope.agentId)] : []), eq(heartbeatRunEvents.eventType, "adapter.invoke"), gte(heartbeatRunEvents.createdAt, windowStart), lte(heartbeatRunEvents.createdAt, windowEnd)))
4754
+ .where(and(eq(heartbeatRunEvents.orgId, scope.orgId), ...(scope.agentId ? [eq(heartbeatRunEvents.agentId, scope.agentId)] : []), inArray(heartbeatRunEvents.eventType, ["adapter.invoke", "adapter.skill_usage"]), gte(heartbeatRunEvents.createdAt, windowStart), lte(heartbeatRunEvents.createdAt, windowEnd)))
4341
4755
  .orderBy(asc(heartbeatRunEvents.createdAt), asc(heartbeatRunEvents.id));
4342
4756
  const days = new Map();
4343
4757
  for (const date of dateKeys) {
4344
- days.set(date, { totalCount: 0, runCount: 0, skills: new Map() });
4758
+ days.set(date, { totalCount: 0, runCount: 0, evidenceCounts: emptySkillEvidenceCounts(), skills: new Map() });
4345
4759
  }
4346
4760
  const overallSkills = new Map();
4761
+ const runEvidence = new Map();
4347
4762
  let totalCount = 0;
4348
4763
  let totalRunsWithSkills = 0;
4349
- for (const row of rows) {
4350
- const date = new Date(row.createdAt).toISOString().slice(0, 10);
4351
- const bucket = days.get(date);
4352
- if (!bucket)
4353
- continue;
4354
- const payload = parseObject(row.payload);
4355
- const loadedSkills = Array.isArray(payload.loadedSkills) ? payload.loadedSkills : [];
4356
- const usedSkills = Array.isArray(payload.usedSkills)
4357
- ? payload.usedSkills
4358
- : inferUsedSkillsFromPrompt(payload.prompt, loadedSkills);
4359
- if (usedSkills.length === 0)
4360
- continue;
4361
- const eventSkills = new Map();
4362
- for (const entry of usedSkills) {
4764
+ const evidenceCounts = emptySkillEvidenceCounts();
4765
+ function addRunSkillEvidence(runId, date, evidence) {
4766
+ if (!days.has(date))
4767
+ return;
4768
+ if (evidence.skills.length === 0)
4769
+ return;
4770
+ const runBucket = runEvidence.get(runId) ?? { date, skills: new Map() };
4771
+ for (const entry of evidence.skills) {
4363
4772
  const normalized = normalizeLoadedSkill(entry);
4364
4773
  if (!normalized)
4365
4774
  continue;
4366
- if (!eventSkills.has(normalized.key)) {
4367
- eventSkills.set(normalized.key, normalized.label);
4775
+ const existing = runBucket.skills.get(normalized.key);
4776
+ if (existing) {
4777
+ existing.evidence = strongestSkillEvidence(existing.evidence, evidence.evidence);
4778
+ if (existing.label === fallbackSkillLabel(existing.key) && normalized.label !== fallbackSkillLabel(normalized.key)) {
4779
+ existing.label = normalized.label;
4780
+ }
4781
+ }
4782
+ else {
4783
+ runBucket.skills.set(normalized.key, {
4784
+ key: normalized.key,
4785
+ label: normalized.label,
4786
+ evidence: evidence.evidence,
4787
+ });
4788
+ }
4789
+ }
4790
+ if (runBucket.skills.size > 0)
4791
+ runEvidence.set(runId, runBucket);
4792
+ }
4793
+ async function inferUsedSkillsFromStoredRunLog(row) {
4794
+ if (row.logStore !== "local_file" || !row.logRef)
4795
+ return [];
4796
+ const adapter = (() => {
4797
+ try {
4798
+ return getServerAdapter(row.agentRuntimeType);
4799
+ }
4800
+ catch {
4801
+ return null;
4802
+ }
4803
+ })();
4804
+ if (!adapter)
4805
+ return [];
4806
+ const parser = adapter.parseStdoutLine ?? null;
4807
+ if (!parser)
4808
+ return [];
4809
+ const limitBytes = Math.min(Math.max(row.logBytes ?? 0, 256_000), 2_000_000);
4810
+ const read = await runLogStore
4811
+ .read({ store: "local_file", logRef: row.logRef }, { limitBytes })
4812
+ .catch(() => null);
4813
+ if (!read?.content)
4814
+ return [];
4815
+ const transcript = [];
4816
+ let stdoutBuffer = "";
4817
+ let stderrBuffer = "";
4818
+ for (const line of read.content.split("\n")) {
4819
+ if (!line.trim())
4820
+ continue;
4821
+ let raw;
4822
+ try {
4823
+ raw = JSON.parse(line);
4824
+ }
4825
+ catch {
4826
+ continue;
4827
+ }
4828
+ const parsed = parseObject(raw);
4829
+ const stream = parsed.stream === "stderr" ? "stderr" : parsed.stream === "stdout" ? "stdout" : null;
4830
+ const chunk = typeof parsed.chunk === "string" ? parsed.chunk : "";
4831
+ if (!stream || !chunk)
4832
+ continue;
4833
+ if (stream === "stdout") {
4834
+ stdoutBuffer = appendTranscriptEntriesFromChunk({
4835
+ buffer: stdoutBuffer,
4836
+ chunk,
4837
+ transcript,
4838
+ parser,
4839
+ kind: "stdout",
4840
+ });
4841
+ }
4842
+ else {
4843
+ stderrBuffer = appendTranscriptEntriesFromChunk({
4844
+ buffer: stderrBuffer,
4845
+ chunk,
4846
+ transcript,
4847
+ kind: "stderr",
4848
+ });
4368
4849
  }
4369
4850
  }
4370
- if (eventSkills.size === 0)
4851
+ appendTranscriptEntriesFromChunk({
4852
+ buffer: stdoutBuffer,
4853
+ chunk: "",
4854
+ transcript,
4855
+ parser,
4856
+ kind: "stdout",
4857
+ finalize: true,
4858
+ });
4859
+ appendTranscriptEntriesFromChunk({
4860
+ buffer: stderrBuffer,
4861
+ chunk: "",
4862
+ transcript,
4863
+ kind: "stderr",
4864
+ finalize: true,
4865
+ });
4866
+ return inferUsedSkillsFromTranscript(transcript);
4867
+ }
4868
+ for (const row of rows) {
4869
+ const date = new Date(row.createdAt).toISOString().slice(0, 10);
4870
+ const payload = parseObject(row.payload);
4871
+ addRunSkillEvidence(row.runId, date, readSkillEvidenceFromPayload(payload));
4872
+ }
4873
+ const runRows = await db
4874
+ .select({
4875
+ id: heartbeatRuns.id,
4876
+ agentRuntimeType: agents.agentRuntimeType,
4877
+ createdAt: heartbeatRuns.createdAt,
4878
+ logStore: heartbeatRuns.logStore,
4879
+ logRef: heartbeatRuns.logRef,
4880
+ logBytes: heartbeatRuns.logBytes,
4881
+ })
4882
+ .from(heartbeatRuns)
4883
+ .innerJoin(agents, eq(agents.id, heartbeatRuns.agentId))
4884
+ .where(and(eq(heartbeatRuns.orgId, scope.orgId), ...(scope.agentId ? [eq(heartbeatRuns.agentId, scope.agentId)] : []), gte(heartbeatRuns.createdAt, windowStart), lte(heartbeatRuns.createdAt, windowEnd)));
4885
+ for (const row of runRows) {
4886
+ const usedSkills = await inferUsedSkillsFromStoredRunLog(row);
4887
+ if (usedSkills.length === 0)
4888
+ continue;
4889
+ addRunSkillEvidence(row.id, new Date(row.createdAt).toISOString().slice(0, 10), {
4890
+ evidence: "used",
4891
+ skills: usedSkills,
4892
+ });
4893
+ }
4894
+ for (const runBucket of runEvidence.values()) {
4895
+ const bucket = days.get(runBucket.date);
4896
+ if (!bucket || runBucket.skills.size === 0)
4371
4897
  continue;
4372
4898
  bucket.runCount += 1;
4373
4899
  totalRunsWithSkills += 1;
4374
- for (const [key, label] of eventSkills) {
4900
+ for (const { key, label, evidence } of runBucket.skills.values()) {
4375
4901
  bucket.totalCount += 1;
4376
4902
  totalCount += 1;
4903
+ incrementSkillEvidenceCount(bucket.evidenceCounts, evidence);
4904
+ incrementSkillEvidenceCount(evidenceCounts, evidence);
4377
4905
  const existingDaySkill = bucket.skills.get(key);
4378
4906
  if (existingDaySkill) {
4379
4907
  existingDaySkill.count += 1;
4908
+ existingDaySkill.evidence = strongestSkillEvidence(existingDaySkill.evidence, evidence);
4909
+ incrementSkillEvidenceCount(existingDaySkill.evidenceCounts, evidence);
4380
4910
  }
4381
4911
  else {
4382
- bucket.skills.set(key, { key, label, count: 1 });
4912
+ const skillEvidenceCounts = emptySkillEvidenceCounts();
4913
+ incrementSkillEvidenceCount(skillEvidenceCounts, evidence);
4914
+ bucket.skills.set(key, { key, label, count: 1, evidence, evidenceCounts: skillEvidenceCounts });
4383
4915
  }
4384
4916
  const existingOverallSkill = overallSkills.get(key);
4385
4917
  if (existingOverallSkill) {
4386
4918
  existingOverallSkill.count += 1;
4919
+ existingOverallSkill.evidence = strongestSkillEvidence(existingOverallSkill.evidence, evidence);
4920
+ incrementSkillEvidenceCount(existingOverallSkill.evidenceCounts, evidence);
4387
4921
  }
4388
4922
  else {
4389
- overallSkills.set(key, { key, label, count: 1 });
4923
+ const skillEvidenceCounts = emptySkillEvidenceCounts();
4924
+ incrementSkillEvidenceCount(skillEvidenceCounts, evidence);
4925
+ overallSkills.set(key, { key, label, count: 1, evidence, evidenceCounts: skillEvidenceCounts });
4390
4926
  }
4391
4927
  }
4392
4928
  }
@@ -4398,6 +4934,7 @@ export function heartbeatService(db) {
4398
4934
  endDate,
4399
4935
  totalCount,
4400
4936
  totalRunsWithSkills,
4937
+ evidenceCounts,
4401
4938
  skills: Array.from(overallSkills.values()).sort((left, right) => (right.count - left.count
4402
4939
  || left.label.localeCompare(right.label)
4403
4940
  || left.key.localeCompare(right.key))),
@@ -4407,6 +4944,7 @@ export function heartbeatService(db) {
4407
4944
  date,
4408
4945
  totalCount: bucket.totalCount,
4409
4946
  runCount: bucket.runCount,
4947
+ evidenceCounts: bucket.evidenceCounts,
4410
4948
  skills: Array.from(bucket.skills.values()).sort((left, right) => (right.count - left.count
4411
4949
  || left.label.localeCompare(right.label)
4412
4950
  || left.key.localeCompare(right.key))),