crewly 1.20.193 → 1.20.195

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 (407) hide show
  1. package/config/roles/auditor/prompt.md +2 -2
  2. package/config/roles/orchestrator/fragments/communication.md +1 -1
  3. package/config/roles/orchestrator/prompt.md +15 -505
  4. package/config/skills/agent/computer-use/SKILL.md +6 -0
  5. package/config/skills/agent/core/experiment-card/SKILL.md +101 -0
  6. package/config/skills/agent/core/experiment-card/execute.sh +149 -0
  7. package/config/skills/agent/core/experiment-card/execute.test.sh +97 -0
  8. package/config/skills/agent/desktop-app-control/execute.sh +5 -2
  9. package/config/skills/orchestrator/send-message/execute.sh +19 -2
  10. package/config/skills/orchestrator/send-message/execute.test.sh +80 -0
  11. package/config/skills/registry.json +7 -5
  12. package/config/skills/team-leader/signal-digest/SKILL.md +110 -0
  13. package/config/skills/team-leader/signal-digest/execute.sh +173 -0
  14. package/config/skills/team-leader/signal-digest/execute.test.sh +123 -0
  15. package/config/skills/team-leader/signal-digest/signal-digest.config.example.json +23 -0
  16. package/config/skills/team-leader/signal-digest/signal_digest.py +572 -0
  17. package/config/skills/team-leader/signal-digest/signal_digest.test.py +319 -0
  18. package/dist/backend/backend/src/constants.d.ts +191 -1
  19. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  20. package/dist/backend/backend/src/constants.js +194 -1
  21. package/dist/backend/backend/src/constants.js.map +1 -1
  22. package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
  23. package/dist/backend/backend/src/controllers/chat/chat.controller.js +6 -0
  24. package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
  25. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.d.ts.map +1 -1
  26. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js +3 -1
  27. package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js.map +1 -1
  28. package/dist/backend/backend/src/controllers/experiments/experiments.routes.d.ts +27 -0
  29. package/dist/backend/backend/src/controllers/experiments/experiments.routes.d.ts.map +1 -0
  30. package/dist/backend/backend/src/controllers/experiments/experiments.routes.js +96 -0
  31. package/dist/backend/backend/src/controllers/experiments/experiments.routes.js.map +1 -0
  32. package/dist/backend/backend/src/controllers/marketplace/marketplace.routes.d.ts +1 -0
  33. package/dist/backend/backend/src/controllers/marketplace/marketplace.routes.d.ts.map +1 -1
  34. package/dist/backend/backend/src/controllers/marketplace/marketplace.routes.js +1 -0
  35. package/dist/backend/backend/src/controllers/marketplace/marketplace.routes.js.map +1 -1
  36. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts.map +1 -1
  37. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js +5 -2
  38. package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js.map +1 -1
  39. package/dist/backend/backend/src/controllers/request/request.controller.d.ts.map +1 -1
  40. package/dist/backend/backend/src/controllers/request/request.controller.js +5 -1
  41. package/dist/backend/backend/src/controllers/request/request.controller.js.map +1 -1
  42. package/dist/backend/backend/src/controllers/settings/settings.controller.d.ts.map +1 -1
  43. package/dist/backend/backend/src/controllers/settings/settings.controller.js +18 -2
  44. package/dist/backend/backend/src/controllers/settings/settings.controller.js.map +1 -1
  45. package/dist/backend/backend/src/controllers/signal-digest/signal-digest.controller.d.ts +26 -0
  46. package/dist/backend/backend/src/controllers/signal-digest/signal-digest.controller.d.ts.map +1 -0
  47. package/dist/backend/backend/src/controllers/signal-digest/signal-digest.controller.js +100 -0
  48. package/dist/backend/backend/src/controllers/signal-digest/signal-digest.controller.js.map +1 -0
  49. package/dist/backend/backend/src/controllers/trace/trace.controller.d.ts +47 -0
  50. package/dist/backend/backend/src/controllers/trace/trace.controller.d.ts.map +1 -0
  51. package/dist/backend/backend/src/controllers/trace/trace.controller.js +169 -0
  52. package/dist/backend/backend/src/controllers/trace/trace.controller.js.map +1 -0
  53. package/dist/backend/backend/src/index.d.ts.map +1 -1
  54. package/dist/backend/backend/src/index.js +66 -20
  55. package/dist/backend/backend/src/index.js.map +1 -1
  56. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  57. package/dist/backend/backend/src/routes/api.routes.js +14 -0
  58. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  59. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  60. package/dist/backend/backend/src/services/agent/agent-registration.service.js +6 -1
  61. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  62. package/dist/backend/backend/src/services/agent/auditor-scheduler.service.d.ts.map +1 -1
  63. package/dist/backend/backend/src/services/agent/auditor-scheduler.service.js +3 -0
  64. package/dist/backend/backend/src/services/agent/auditor-scheduler.service.js.map +1 -1
  65. package/dist/backend/backend/src/services/agent/oauth-relogin-monitor.service.d.ts +1 -1
  66. package/dist/backend/backend/src/services/agent/oauth-relogin-monitor.service.d.ts.map +1 -1
  67. package/dist/backend/backend/src/services/agent/runtime-session-recovery.d.ts +4 -2
  68. package/dist/backend/backend/src/services/agent/runtime-session-recovery.d.ts.map +1 -1
  69. package/dist/backend/backend/src/services/agent/runtime-session-recovery.js +22 -6
  70. package/dist/backend/backend/src/services/agent/runtime-session-recovery.js.map +1 -1
  71. package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.js +2 -2
  72. package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.js.map +1 -1
  73. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  74. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
  75. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  76. package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts +6 -0
  77. package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts.map +1 -1
  78. package/dist/backend/backend/src/services/backup/backup-archive.service.js +7 -1
  79. package/dist/backend/backend/src/services/backup/backup-archive.service.js.map +1 -1
  80. package/dist/backend/backend/src/services/boot/boot-announce.service.d.ts +1 -1
  81. package/dist/backend/backend/src/services/boot/boot-announce.service.d.ts.map +1 -1
  82. package/dist/backend/backend/src/services/canva/canva-token.service.d.ts.map +1 -1
  83. package/dist/backend/backend/src/services/canva/canva-token.service.js +5 -0
  84. package/dist/backend/backend/src/services/canva/canva-token.service.js.map +1 -1
  85. package/dist/backend/backend/src/services/chat/chat.service.d.ts +78 -2
  86. package/dist/backend/backend/src/services/chat/chat.service.d.ts.map +1 -1
  87. package/dist/backend/backend/src/services/chat/chat.service.js +210 -26
  88. package/dist/backend/backend/src/services/chat/chat.service.js.map +1 -1
  89. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +14 -3
  90. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
  91. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +28 -4
  92. package/dist/backend/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  93. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts +4 -3
  94. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
  95. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js +7 -4
  96. package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
  97. package/dist/backend/backend/src/services/decisions/decision-store.d.ts.map +1 -1
  98. package/dist/backend/backend/src/services/decisions/decision-store.js +5 -1
  99. package/dist/backend/backend/src/services/decisions/decision-store.js.map +1 -1
  100. package/dist/backend/backend/src/services/decisions/decision.wiring.d.ts +10 -1
  101. package/dist/backend/backend/src/services/decisions/decision.wiring.d.ts.map +1 -1
  102. package/dist/backend/backend/src/services/decisions/decision.wiring.js +20 -9
  103. package/dist/backend/backend/src/services/decisions/decision.wiring.js.map +1 -1
  104. package/dist/backend/backend/src/services/experiments/experiment-verdict.d.ts +88 -0
  105. package/dist/backend/backend/src/services/experiments/experiment-verdict.d.ts.map +1 -0
  106. package/dist/backend/backend/src/services/experiments/experiment-verdict.js +226 -0
  107. package/dist/backend/backend/src/services/experiments/experiment-verdict.js.map +1 -0
  108. package/dist/backend/backend/src/services/experiments/experiment.service.d.ts +350 -0
  109. package/dist/backend/backend/src/services/experiments/experiment.service.d.ts.map +1 -0
  110. package/dist/backend/backend/src/services/experiments/experiment.service.js +908 -0
  111. package/dist/backend/backend/src/services/experiments/experiment.service.js.map +1 -0
  112. package/dist/backend/backend/src/services/experiments/experiment.wiring.d.ts +88 -0
  113. package/dist/backend/backend/src/services/experiments/experiment.wiring.d.ts.map +1 -0
  114. package/dist/backend/backend/src/services/experiments/experiment.wiring.js +162 -0
  115. package/dist/backend/backend/src/services/experiments/experiment.wiring.js.map +1 -0
  116. package/dist/backend/backend/src/services/experiments/seo-ops-metric.fetcher.d.ts +67 -0
  117. package/dist/backend/backend/src/services/experiments/seo-ops-metric.fetcher.d.ts.map +1 -0
  118. package/dist/backend/backend/src/services/experiments/seo-ops-metric.fetcher.js +133 -0
  119. package/dist/backend/backend/src/services/experiments/seo-ops-metric.fetcher.js.map +1 -0
  120. package/dist/backend/backend/src/services/google/google-workspace-token.service.d.ts.map +1 -1
  121. package/dist/backend/backend/src/services/google/google-workspace-token.service.js +5 -0
  122. package/dist/backend/backend/src/services/google/google-workspace-token.service.js.map +1 -1
  123. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.d.ts.map +1 -1
  124. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.js +7 -0
  125. package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.js.map +1 -1
  126. package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts +11 -3
  127. package/dist/backend/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -1
  128. package/dist/backend/backend/src/services/messaging/queue-processor.service.js +24 -7
  129. package/dist/backend/backend/src/services/messaging/queue-processor.service.js.map +1 -1
  130. package/dist/backend/backend/src/services/microsoft/microsoft-token.service.d.ts.map +1 -1
  131. package/dist/backend/backend/src/services/microsoft/microsoft-token.service.js +5 -0
  132. package/dist/backend/backend/src/services/microsoft/microsoft-token.service.js.map +1 -1
  133. package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts +7 -0
  134. package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
  135. package/dist/backend/backend/src/services/monitoring/token-usage.service.js +4 -0
  136. package/dist/backend/backend/src/services/monitoring/token-usage.service.js.map +1 -1
  137. package/dist/backend/backend/src/services/orc/orc-status-router.service.d.ts.map +1 -1
  138. package/dist/backend/backend/src/services/orc/orc-status-router.service.js +12 -3
  139. package/dist/backend/backend/src/services/orc/orc-status-router.service.js.map +1 -1
  140. package/dist/backend/backend/src/services/orc/reply-destination.wiring.d.ts.map +1 -1
  141. package/dist/backend/backend/src/services/orc/reply-destination.wiring.js +16 -0
  142. package/dist/backend/backend/src/services/orc/reply-destination.wiring.js.map +1 -1
  143. package/dist/backend/backend/src/services/people/acting-for.service.d.ts +42 -5
  144. package/dist/backend/backend/src/services/people/acting-for.service.d.ts.map +1 -1
  145. package/dist/backend/backend/src/services/people/acting-for.service.js +80 -5
  146. package/dist/backend/backend/src/services/people/acting-for.service.js.map +1 -1
  147. package/dist/backend/backend/src/services/people/grant-sharing.d.ts +5 -4
  148. package/dist/backend/backend/src/services/people/grant-sharing.d.ts.map +1 -1
  149. package/dist/backend/backend/src/services/people/grant-sharing.js +12 -4
  150. package/dist/backend/backend/src/services/people/grant-sharing.js.map +1 -1
  151. package/dist/backend/backend/src/services/people/people-directory.service.d.ts +71 -13
  152. package/dist/backend/backend/src/services/people/people-directory.service.d.ts.map +1 -1
  153. package/dist/backend/backend/src/services/people/people-directory.service.js +164 -27
  154. package/dist/backend/backend/src/services/people/people-directory.service.js.map +1 -1
  155. package/dist/backend/backend/src/services/project-tickets/project-ticket.service.d.ts.map +1 -1
  156. package/dist/backend/backend/src/services/project-tickets/project-ticket.service.js +2 -0
  157. package/dist/backend/backend/src/services/project-tickets/project-ticket.service.js.map +1 -1
  158. package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
  159. package/dist/backend/backend/src/services/reconciler/reconciler.service.js +15 -0
  160. package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
  161. package/dist/backend/backend/src/services/session/session-handoff.service.d.ts.map +1 -1
  162. package/dist/backend/backend/src/services/session/session-handoff.service.js +8 -5
  163. package/dist/backend/backend/src/services/session/session-handoff.service.js.map +1 -1
  164. package/dist/backend/backend/src/services/signal-digest/signal-digest-card.d.ts +75 -0
  165. package/dist/backend/backend/src/services/signal-digest/signal-digest-card.d.ts.map +1 -0
  166. package/dist/backend/backend/src/services/signal-digest/signal-digest-card.js +187 -0
  167. package/dist/backend/backend/src/services/signal-digest/signal-digest-card.js.map +1 -0
  168. package/dist/backend/backend/src/services/signal-digest/signal-digest-contract.d.ts +86 -0
  169. package/dist/backend/backend/src/services/signal-digest/signal-digest-contract.d.ts.map +1 -0
  170. package/dist/backend/backend/src/services/signal-digest/signal-digest-contract.js +307 -0
  171. package/dist/backend/backend/src/services/signal-digest/signal-digest-contract.js.map +1 -0
  172. package/dist/backend/backend/src/services/signal-digest/signal-digest-store.d.ts +85 -0
  173. package/dist/backend/backend/src/services/signal-digest/signal-digest-store.d.ts.map +1 -0
  174. package/dist/backend/backend/src/services/signal-digest/signal-digest-store.js +185 -0
  175. package/dist/backend/backend/src/services/signal-digest/signal-digest-store.js.map +1 -0
  176. package/dist/backend/backend/src/services/signal-digest/signal-digest.service.d.ts +272 -0
  177. package/dist/backend/backend/src/services/signal-digest/signal-digest.service.d.ts.map +1 -0
  178. package/dist/backend/backend/src/services/signal-digest/signal-digest.service.js +570 -0
  179. package/dist/backend/backend/src/services/signal-digest/signal-digest.service.js.map +1 -0
  180. package/dist/backend/backend/src/services/signal-digest/signal-digest.wiring.d.ts +51 -0
  181. package/dist/backend/backend/src/services/signal-digest/signal-digest.wiring.d.ts.map +1 -0
  182. package/dist/backend/backend/src/services/signal-digest/signal-digest.wiring.js +116 -0
  183. package/dist/backend/backend/src/services/signal-digest/signal-digest.wiring.js.map +1 -0
  184. package/dist/backend/backend/src/services/slack/notify-reconciliation.service.d.ts +5 -1
  185. package/dist/backend/backend/src/services/slack/notify-reconciliation.service.d.ts.map +1 -1
  186. package/dist/backend/backend/src/services/slack/notify-reconciliation.service.js +6 -2
  187. package/dist/backend/backend/src/services/slack/notify-reconciliation.service.js.map +1 -1
  188. package/dist/backend/backend/src/services/slack/slack-bot-ids.d.ts +33 -0
  189. package/dist/backend/backend/src/services/slack/slack-bot-ids.d.ts.map +1 -0
  190. package/dist/backend/backend/src/services/slack/slack-bot-ids.js +56 -0
  191. package/dist/backend/backend/src/services/slack/slack-bot-ids.js.map +1 -0
  192. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +14 -1
  193. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  194. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +30 -10
  195. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  196. package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts +1 -1
  197. package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts.map +1 -1
  198. package/dist/backend/backend/src/services/slack/slack.service.d.ts +31 -1
  199. package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
  200. package/dist/backend/backend/src/services/slack/slack.service.js +52 -7
  201. package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
  202. package/dist/backend/backend/src/services/system/auto-update.service.d.ts +2 -2
  203. package/dist/backend/backend/src/services/system/auto-update.service.d.ts.map +1 -1
  204. package/dist/backend/backend/src/services/task-pool/pool-storage.d.ts.map +1 -1
  205. package/dist/backend/backend/src/services/task-pool/pool-storage.js +4 -0
  206. package/dist/backend/backend/src/services/task-pool/pool-storage.js.map +1 -1
  207. package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
  208. package/dist/backend/backend/src/services/task-pool/task-pool.service.js +2 -0
  209. package/dist/backend/backend/src/services/task-pool/task-pool.service.js.map +1 -1
  210. package/dist/backend/backend/src/services/trace/trace-context.service.d.ts +195 -0
  211. package/dist/backend/backend/src/services/trace/trace-context.service.d.ts.map +1 -0
  212. package/dist/backend/backend/src/services/trace/trace-context.service.js +432 -0
  213. package/dist/backend/backend/src/services/trace/trace-context.service.js.map +1 -0
  214. package/dist/backend/backend/src/services/trace/trace-http.middleware.d.ts +42 -0
  215. package/dist/backend/backend/src/services/trace/trace-http.middleware.d.ts.map +1 -0
  216. package/dist/backend/backend/src/services/trace/trace-http.middleware.js +146 -0
  217. package/dist/backend/backend/src/services/trace/trace-http.middleware.js.map +1 -0
  218. package/dist/backend/backend/src/services/trace/trace-markers.d.ts +93 -0
  219. package/dist/backend/backend/src/services/trace/trace-markers.d.ts.map +1 -0
  220. package/dist/backend/backend/src/services/trace/trace-markers.js +162 -0
  221. package/dist/backend/backend/src/services/trace/trace-markers.js.map +1 -0
  222. package/dist/backend/backend/src/services/trace/trace-recorder.d.ts +290 -0
  223. package/dist/backend/backend/src/services/trace/trace-recorder.d.ts.map +1 -0
  224. package/dist/backend/backend/src/services/trace/trace-recorder.js +669 -0
  225. package/dist/backend/backend/src/services/trace/trace-recorder.js.map +1 -0
  226. package/dist/backend/backend/src/services/trace/trace-store.d.ts +269 -0
  227. package/dist/backend/backend/src/services/trace/trace-store.d.ts.map +1 -0
  228. package/dist/backend/backend/src/services/trace/trace-store.js +602 -0
  229. package/dist/backend/backend/src/services/trace/trace-store.js.map +1 -0
  230. package/dist/backend/backend/src/services/trace/trace.types.d.ts +118 -0
  231. package/dist/backend/backend/src/services/trace/trace.types.d.ts.map +1 -0
  232. package/dist/backend/backend/src/services/trace/trace.types.js +85 -0
  233. package/dist/backend/backend/src/services/trace/trace.types.js.map +1 -0
  234. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.boot.d.ts +1 -1
  235. package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.boot.d.ts.map +1 -1
  236. package/dist/backend/backend/src/services/v3/request.service.d.ts.map +1 -1
  237. package/dist/backend/backend/src/services/v3/request.service.js +4 -0
  238. package/dist/backend/backend/src/services/v3/request.service.js.map +1 -1
  239. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts.map +1 -1
  240. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js +22 -2
  241. package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js.map +1 -1
  242. package/dist/backend/backend/src/services/wiki/wiki-ingest.service.d.ts +3 -1
  243. package/dist/backend/backend/src/services/wiki/wiki-ingest.service.d.ts.map +1 -1
  244. package/dist/backend/backend/src/services/wiki/wiki-ingest.service.js.map +1 -1
  245. package/dist/backend/backend/src/services/workflow/message-scheduler.service.d.ts.map +1 -1
  246. package/dist/backend/backend/src/services/workflow/message-scheduler.service.js +3 -0
  247. package/dist/backend/backend/src/services/workflow/message-scheduler.service.js.map +1 -1
  248. package/dist/backend/backend/src/services/workflow/scheduler.service.d.ts.map +1 -1
  249. package/dist/backend/backend/src/services/workflow/scheduler.service.js +3 -0
  250. package/dist/backend/backend/src/services/workflow/scheduler.service.js.map +1 -1
  251. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts +3 -0
  252. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts.map +1 -1
  253. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js +7 -0
  254. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js.map +1 -1
  255. package/dist/backend/backend/src/types/experiment.types.d.ts +155 -0
  256. package/dist/backend/backend/src/types/experiment.types.d.ts.map +1 -0
  257. package/dist/backend/backend/src/types/experiment.types.js +32 -0
  258. package/dist/backend/backend/src/types/experiment.types.js.map +1 -0
  259. package/dist/backend/backend/src/types/messaging.types.d.ts +6 -0
  260. package/dist/backend/backend/src/types/messaging.types.d.ts.map +1 -1
  261. package/dist/backend/backend/src/types/messaging.types.js.map +1 -1
  262. package/dist/backend/backend/src/types/signal-digest.types.d.ts +185 -0
  263. package/dist/backend/backend/src/types/signal-digest.types.d.ts.map +1 -0
  264. package/dist/backend/backend/src/types/signal-digest.types.js +21 -0
  265. package/dist/backend/backend/src/types/signal-digest.types.js.map +1 -0
  266. package/dist/backend/backend/src/types/v2/request.types.d.ts +4 -0
  267. package/dist/backend/backend/src/types/v2/request.types.d.ts.map +1 -1
  268. package/dist/backend/backend/src/types/v2/request.types.js.map +1 -1
  269. package/dist/backend/backend/src/types/v2/work-item.types.d.ts +4 -0
  270. package/dist/backend/backend/src/types/v2/work-item.types.d.ts.map +1 -1
  271. package/dist/backend/backend/src/types/v2/work-item.types.js +1 -0
  272. package/dist/backend/backend/src/types/v2/work-item.types.js.map +1 -1
  273. package/dist/backend/backend/src/utils/security.d.ts +2 -0
  274. package/dist/backend/backend/src/utils/security.d.ts.map +1 -1
  275. package/dist/backend/backend/src/utils/security.js +3 -1
  276. package/dist/backend/backend/src/utils/security.js.map +1 -1
  277. package/dist/backend/backend/src/websocket/chat.gateway.d.ts +9 -0
  278. package/dist/backend/backend/src/websocket/chat.gateway.d.ts.map +1 -1
  279. package/dist/backend/backend/src/websocket/chat.gateway.js +26 -0
  280. package/dist/backend/backend/src/websocket/chat.gateway.js.map +1 -1
  281. package/dist/backend/build-info.json +2 -2
  282. package/dist/cli/backend/src/constants.d.ts +191 -1
  283. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  284. package/dist/cli/backend/src/constants.js +194 -1
  285. package/dist/cli/backend/src/constants.js.map +1 -1
  286. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  287. package/dist/cli/backend/src/services/agent/agent-registration.service.js +6 -1
  288. package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
  289. package/dist/cli/backend/src/services/agent/auditor-scheduler.service.d.ts.map +1 -1
  290. package/dist/cli/backend/src/services/agent/auditor-scheduler.service.js +3 -0
  291. package/dist/cli/backend/src/services/agent/auditor-scheduler.service.js.map +1 -1
  292. package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.d.ts +1 -1
  293. package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.d.ts.map +1 -1
  294. package/dist/cli/backend/src/services/agent/runtime-session-recovery.d.ts +4 -2
  295. package/dist/cli/backend/src/services/agent/runtime-session-recovery.d.ts.map +1 -1
  296. package/dist/cli/backend/src/services/agent/runtime-session-recovery.js +22 -6
  297. package/dist/cli/backend/src/services/agent/runtime-session-recovery.js.map +1 -1
  298. package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.js +2 -2
  299. package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.js.map +1 -1
  300. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  301. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
  302. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  303. package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts +6 -0
  304. package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts.map +1 -1
  305. package/dist/cli/backend/src/services/backup/backup-archive.service.js +7 -1
  306. package/dist/cli/backend/src/services/backup/backup-archive.service.js.map +1 -1
  307. package/dist/cli/backend/src/services/chat/chat.service.d.ts +185 -0
  308. package/dist/cli/backend/src/services/chat/chat.service.d.ts.map +1 -0
  309. package/dist/cli/backend/src/services/chat/chat.service.js +558 -0
  310. package/dist/cli/backend/src/services/chat/chat.service.js.map +1 -0
  311. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts +14 -3
  312. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.d.ts.map +1 -1
  313. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js +28 -4
  314. package/dist/cli/backend/src/services/chat-v2/chat-v2.dispatcher.service.js.map +1 -1
  315. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts +4 -3
  316. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
  317. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js +7 -4
  318. package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
  319. package/dist/cli/backend/src/services/decisions/decision-store.d.ts.map +1 -1
  320. package/dist/cli/backend/src/services/decisions/decision-store.js +5 -1
  321. package/dist/cli/backend/src/services/decisions/decision-store.js.map +1 -1
  322. package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.d.ts.map +1 -1
  323. package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.js +7 -0
  324. package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.js.map +1 -1
  325. package/dist/cli/backend/src/services/messaging/queue-processor.service.d.ts +11 -3
  326. package/dist/cli/backend/src/services/messaging/queue-processor.service.d.ts.map +1 -1
  327. package/dist/cli/backend/src/services/messaging/queue-processor.service.js +24 -7
  328. package/dist/cli/backend/src/services/messaging/queue-processor.service.js.map +1 -1
  329. package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts +7 -0
  330. package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
  331. package/dist/cli/backend/src/services/monitoring/token-usage.service.js +4 -0
  332. package/dist/cli/backend/src/services/monitoring/token-usage.service.js.map +1 -1
  333. package/dist/cli/backend/src/services/people/acting-for.service.d.ts +42 -5
  334. package/dist/cli/backend/src/services/people/acting-for.service.d.ts.map +1 -1
  335. package/dist/cli/backend/src/services/people/acting-for.service.js +80 -5
  336. package/dist/cli/backend/src/services/people/acting-for.service.js.map +1 -1
  337. package/dist/cli/backend/src/services/people/people-directory.service.d.ts +71 -13
  338. package/dist/cli/backend/src/services/people/people-directory.service.d.ts.map +1 -1
  339. package/dist/cli/backend/src/services/people/people-directory.service.js +164 -27
  340. package/dist/cli/backend/src/services/people/people-directory.service.js.map +1 -1
  341. package/dist/cli/backend/src/services/project-tickets/project-ticket.service.d.ts.map +1 -1
  342. package/dist/cli/backend/src/services/project-tickets/project-ticket.service.js +2 -0
  343. package/dist/cli/backend/src/services/project-tickets/project-ticket.service.js.map +1 -1
  344. package/dist/cli/backend/src/services/session/session-handoff.service.d.ts.map +1 -1
  345. package/dist/cli/backend/src/services/session/session-handoff.service.js +8 -5
  346. package/dist/cli/backend/src/services/session/session-handoff.service.js.map +1 -1
  347. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts +14 -1
  348. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  349. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +30 -10
  350. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  351. package/dist/cli/backend/src/services/slack/slack.service.d.ts +31 -1
  352. package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
  353. package/dist/cli/backend/src/services/slack/slack.service.js +52 -7
  354. package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
  355. package/dist/cli/backend/src/services/task-pool/pool-storage.d.ts.map +1 -1
  356. package/dist/cli/backend/src/services/task-pool/pool-storage.js +4 -0
  357. package/dist/cli/backend/src/services/task-pool/pool-storage.js.map +1 -1
  358. package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
  359. package/dist/cli/backend/src/services/task-pool/task-pool.service.js +2 -0
  360. package/dist/cli/backend/src/services/task-pool/task-pool.service.js.map +1 -1
  361. package/dist/cli/backend/src/services/trace/trace-context.service.d.ts +195 -0
  362. package/dist/cli/backend/src/services/trace/trace-context.service.d.ts.map +1 -0
  363. package/dist/cli/backend/src/services/trace/trace-context.service.js +432 -0
  364. package/dist/cli/backend/src/services/trace/trace-context.service.js.map +1 -0
  365. package/dist/cli/backend/src/services/trace/trace-markers.d.ts +93 -0
  366. package/dist/cli/backend/src/services/trace/trace-markers.d.ts.map +1 -0
  367. package/dist/cli/backend/src/services/trace/trace-markers.js +162 -0
  368. package/dist/cli/backend/src/services/trace/trace-markers.js.map +1 -0
  369. package/dist/cli/backend/src/services/trace/trace-recorder.d.ts +290 -0
  370. package/dist/cli/backend/src/services/trace/trace-recorder.d.ts.map +1 -0
  371. package/dist/cli/backend/src/services/trace/trace-recorder.js +669 -0
  372. package/dist/cli/backend/src/services/trace/trace-recorder.js.map +1 -0
  373. package/dist/cli/backend/src/services/trace/trace-store.d.ts +269 -0
  374. package/dist/cli/backend/src/services/trace/trace-store.d.ts.map +1 -0
  375. package/dist/cli/backend/src/services/trace/trace-store.js +602 -0
  376. package/dist/cli/backend/src/services/trace/trace-store.js.map +1 -0
  377. package/dist/cli/backend/src/services/trace/trace.types.d.ts +118 -0
  378. package/dist/cli/backend/src/services/trace/trace.types.d.ts.map +1 -0
  379. package/dist/cli/backend/src/services/trace/trace.types.js +85 -0
  380. package/dist/cli/backend/src/services/trace/trace.types.js.map +1 -0
  381. package/dist/cli/backend/src/services/v3/request.service.d.ts.map +1 -1
  382. package/dist/cli/backend/src/services/v3/request.service.js +4 -0
  383. package/dist/cli/backend/src/services/v3/request.service.js.map +1 -1
  384. package/dist/cli/backend/src/services/wiki/wiki-ingest.service.d.ts +3 -1
  385. package/dist/cli/backend/src/services/wiki/wiki-ingest.service.d.ts.map +1 -1
  386. package/dist/cli/backend/src/services/wiki/wiki-ingest.service.js.map +1 -1
  387. package/dist/cli/backend/src/types/messaging.types.d.ts +6 -0
  388. package/dist/cli/backend/src/types/messaging.types.d.ts.map +1 -1
  389. package/dist/cli/backend/src/types/messaging.types.js.map +1 -1
  390. package/dist/cli/backend/src/types/v2/request.types.d.ts +4 -0
  391. package/dist/cli/backend/src/types/v2/request.types.d.ts.map +1 -1
  392. package/dist/cli/backend/src/types/v2/request.types.js.map +1 -1
  393. package/dist/cli/backend/src/types/v2/work-item.types.d.ts +4 -0
  394. package/dist/cli/backend/src/types/v2/work-item.types.d.ts.map +1 -1
  395. package/dist/cli/backend/src/types/v2/work-item.types.js +1 -0
  396. package/dist/cli/backend/src/types/v2/work-item.types.js.map +1 -1
  397. package/dist/cli/backend/src/utils/security.d.ts +196 -0
  398. package/dist/cli/backend/src/utils/security.d.ts.map +1 -0
  399. package/dist/cli/backend/src/utils/security.js +538 -0
  400. package/dist/cli/backend/src/utils/security.js.map +1 -0
  401. package/dist/cli/backend/src/websocket/chat.gateway.d.ts +9 -0
  402. package/dist/cli/backend/src/websocket/chat.gateway.d.ts.map +1 -1
  403. package/dist/cli/backend/src/websocket/chat.gateway.js +26 -0
  404. package/dist/cli/backend/src/websocket/chat.gateway.js.map +1 -1
  405. package/frontend/dist/assets/{index-465d1f8f.js → index-800118ca.js} +274 -274
  406. package/frontend/dist/index.html +1 -1
  407. package/package.json +1 -1
@@ -0,0 +1,572 @@
1
+ #!/usr/bin/env python3
2
+ """signal-digest: a site's daily signals, turned into ranked draft actions (#987).
3
+
4
+ Standard library only. Google access, the site config, Search Console windows and
5
+ analysis come from the seo-ops skill (seo_ops.py), so both skills share one config
6
+ file and one service account.
7
+
8
+ Subcommand
9
+ collect GA4 traffic + key events, Search Console opportunities, the site inbox
10
+ (passed in by execute.sh), broken sitemap pages and an optional JS-error
11
+ source; drops what the site's history or experiment log already covers;
12
+ prints JSON with the signals and up to 10 rule-ranked draft actions.
13
+
14
+ The team lead reads the output, keeps or rewrites 3-5 actions and proposes them
15
+ (execute.sh propose). Every source reports what it examined; a source that fails
16
+ (whatever the exception) says why and the others still run. execute.sh reports
17
+ the source statuses to Crewly, which tells the owner once when a source starts or
18
+ stops failing. Exit codes: 0 ok, 1 nothing could be examined, 2 setup problem.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import argparse
24
+ import concurrent.futures
25
+ import datetime as dt
26
+ import importlib.util
27
+ import json
28
+ import os
29
+ import re
30
+ import subprocess
31
+ import sys
32
+ import urllib.parse
33
+
34
+ EXIT_OK, EXIT_EMPTY, EXIT_SETUP = 0, 1, 2
35
+
36
+ MAX_CANDIDATES = 10
37
+ SITEMAP_CHECK_DEFAULT = 60
38
+ SITEMAP_CHECK_WORKERS = 8
39
+ ERRORS_COMMAND_TIMEOUT_S = 60
40
+ DEFAULT_DAYS = 7
41
+ LIST_ROWS = 5
42
+
43
+ # Typical organic CTR by position (rounded industry curves), used only to size
44
+ # the expected effect of a title rewrite or of reaching the top 3.
45
+ BENCHMARK_CTR = {1: 0.28, 2: 0.16, 3: 0.11, 4: 0.08, 5: 0.06}
46
+ NEAR_MISS_TARGET_POSITION = 3
47
+ # A rising query is proposed as a new topic only beyond this position (or when new).
48
+ RISING_MAX_RANKED_POSITION = 10
49
+ # A finished experiment keeps its query / page off the list this long (as a Do does).
50
+ EXPERIMENT_RECENT_DAYS = 90
51
+
52
+ DEFAULTS = {
53
+ "days": DEFAULT_DAYS,
54
+ "thresholds": {
55
+ "ga4DropPct": 20,
56
+ "ga4MinPrevSessions": 50,
57
+ "ga4MinPrevKeyEvents": 5,
58
+ },
59
+ "errors": {"checkSitemap": True, "maxUrls": SITEMAP_CHECK_DEFAULT},
60
+ }
61
+
62
+
63
+ class DigestError(Exception):
64
+ """A setup problem the operator can act on."""
65
+
66
+
67
+ # --------------------------------------------------------------------------- seo-ops
68
+
69
+ def seo_ops_candidates(script_dir=None, home=None):
70
+ """Where seo_ops.py may live: env override, the bundled skill, the marketplace install."""
71
+ script_dir = script_dir or os.path.dirname(os.path.abspath(__file__))
72
+ home = home or os.path.expanduser("~")
73
+ out = []
74
+ if os.environ.get("SIGNAL_DIGEST_SEO_OPS"):
75
+ out.append(os.environ["SIGNAL_DIGEST_SEO_OPS"])
76
+ out.append(os.path.join(script_dir, "..", "..", "agent", "marketplace", "seo-ops", "seo_ops.py"))
77
+ out.append(os.path.join(home, ".crewly", "marketplace", "skills", "seo-ops", "seo_ops.py"))
78
+ return out
79
+
80
+
81
+ def load_seo_ops(paths=None):
82
+ """Import seo_ops.py from the first path that exists."""
83
+ for path in paths or seo_ops_candidates():
84
+ if os.path.isfile(path):
85
+ spec = importlib.util.spec_from_file_location("seo_ops", path)
86
+ module = importlib.util.module_from_spec(spec)
87
+ spec.loader.exec_module(module)
88
+ return module
89
+ raise DigestError("seo-ops (seo_ops.py) not found. Install the seo-ops skill, or set "
90
+ "SIGNAL_DIGEST_SEO_OPS to its seo_ops.py.")
91
+
92
+
93
+ # --------------------------------------------------------------------------- helpers
94
+
95
+ def normalize_key(key):
96
+ """Same identity rule as the backend: case and spacing do not matter."""
97
+ return re.sub(r"\s+", " ", str(key)).strip().lower()
98
+
99
+
100
+ def change_pct(now, prev):
101
+ if not prev:
102
+ return None
103
+ return round(100.0 * (now - prev) / prev, 1)
104
+
105
+
106
+ def per_week(value, days):
107
+ return value * 7.0 / days if days else value
108
+
109
+
110
+ def short_path(url, site_url=""):
111
+ if site_url and url.startswith(site_url.rstrip("/")):
112
+ rest = url[len(site_url.rstrip("/")):]
113
+ return rest or "/"
114
+ p = urllib.parse.urlsplit(url)
115
+ return p.path or url
116
+
117
+
118
+ def digest_settings(cfg):
119
+ sd = cfg.get("signalDigest") or {}
120
+ merged = json.loads(json.dumps(DEFAULTS))
121
+ for k, v in sd.items():
122
+ if isinstance(v, dict) and isinstance(merged.get(k), dict):
123
+ merged[k].update(v)
124
+ else:
125
+ merged[k] = v
126
+ if not merged.get("site"):
127
+ host = urllib.parse.urlsplit(cfg.get("siteUrl", "")).netloc
128
+ if not host:
129
+ raise DigestError("Config needs signalDigest.site (or siteUrl) to name the site.")
130
+ merged["site"] = host
131
+ return merged
132
+
133
+
134
+ # --------------------------------------------------------------------------- GA4
135
+
136
+ def ga4_windows(days, today):
137
+ end = today - dt.timedelta(days=1)
138
+ start = end - dt.timedelta(days=days - 1)
139
+ p_end = start - dt.timedelta(days=1)
140
+ return start, end, p_end - dt.timedelta(days=days - 1), p_end
141
+
142
+
143
+ def _ga4_report(so, net, cfg, dims, metrics, ranges):
144
+ body = {"dateRanges": ranges, "dimensions": [{"name": d} for d in dims], "metrics": [{"name": m} for m in metrics]}
145
+ if cfg.get("ga4HostName"):
146
+ # One GA4 property can carry several sites (seo-ops ga4HostName, #885).
147
+ body["dimensionFilter"] = {"filter": {"fieldName": "hostName", "stringFilter": {"matchType": "EXACT", "value": cfg["ga4HostName"]}}}
148
+ data = net.post_json(so.GA4_API.format(prop=cfg["ga4PropertyId"]), body, so.GA4_SCOPE,
149
+ "GA4 property %s" % cfg["ga4PropertyId"], net.service_account_email())
150
+ out = []
151
+ for r in data.get("rows", []) or []:
152
+ values = [v.get("value", "") for v in r.get("dimensionValues", [])]
153
+ # With two date ranges GA4 appends the range name as an extra dimension.
154
+ rng = values[len(dims)] if len(values) > len(dims) else "now"
155
+ nums = [float(v.get("value", 0) or 0) for v in r.get("metricValues", [])]
156
+ out.append((values[:len(dims)], rng, nums))
157
+ return out
158
+
159
+
160
+ def ga4_summary(so, net, cfg, days, today, conversion_events=None):
161
+ """Sessions (all / organic) and key events, this window vs the previous one."""
162
+ start, end, p_start, p_end = ga4_windows(days, today)
163
+ ranges = [{"startDate": str(start), "endDate": str(end), "name": "now"},
164
+ {"startDate": str(p_start), "endDate": str(p_end), "name": "prev"}]
165
+ totals = {"now": {"sessions": 0.0, "organic": 0.0, "keyEvents": 0.0}, "prev": {"sessions": 0.0, "organic": 0.0, "keyEvents": 0.0}}
166
+ for (channel,), rng, (sessions, key_events) in _ga4_report(so, net, cfg, ["sessionDefaultChannelGroup"], ["sessions", "keyEvents"], ranges):
167
+ t = totals.setdefault(rng, {"sessions": 0.0, "organic": 0.0, "keyEvents": 0.0})
168
+ t["sessions"] += sessions
169
+ t["keyEvents"] += key_events
170
+ if channel == "Organic Search":
171
+ t["organic"] += sessions
172
+ events = {}
173
+ for (name,), rng, (count,) in _ga4_report(so, net, cfg, ["eventName"], ["keyEvents"], ranges):
174
+ if conversion_events and name not in conversion_events:
175
+ continue
176
+ events.setdefault(name, {"now": 0, "prev": 0})[rng] = int(count)
177
+ by_event = sorted(({"event": k, **v} for k, v in events.items() if v["now"] or v["prev"]), key=lambda e: -e["now"])
178
+ if conversion_events:
179
+ key_now = sum(e["now"] for e in by_event)
180
+ key_prev = sum(e["prev"] for e in by_event)
181
+ else:
182
+ key_now, key_prev = int(totals["now"]["keyEvents"]), int(totals["prev"]["keyEvents"])
183
+
184
+ def metric(now, prev):
185
+ return {"now": int(now), "prev": int(prev), "changePct": change_pct(now, prev)}
186
+ return {
187
+ "window": {"start": str(start), "end": str(end), "prevStart": str(p_start), "prevEnd": str(p_end)},
188
+ "sessions": metric(totals["now"]["sessions"], totals["prev"]["sessions"]),
189
+ "organicSessions": metric(totals["now"]["organic"], totals["prev"]["organic"]),
190
+ "keyEvents": metric(key_now, key_prev),
191
+ "byEvent": by_event[:LIST_ROWS * 2],
192
+ }
193
+
194
+
195
+ # --------------------------------------------------------------------------- Search Console
196
+
197
+ def top_pages(qp_rows):
198
+ """Best page (most impressions) per query."""
199
+ best = {}
200
+ for r in qp_rows:
201
+ q, page = r["keys"][0], r["keys"][1]
202
+ if q not in best or r["impressions"] > best[q][1]:
203
+ best[q] = (page, r["impressions"])
204
+ return {q: p for q, (p, _) in best.items()}
205
+
206
+
207
+ def gsc_opportunities(so, net, cfg, days, today):
208
+ start, end, p_start, p_end = so.windows(days, today)
209
+ q_now = so.gsc_rows(net, cfg, start, end, ["query"])
210
+ q_prev = so.gsc_rows(net, cfg, p_start, p_end, ["query"])
211
+ qp = so.gsc_rows(net, cfg, start, end, ["query", "page"])
212
+ res = so.analyse_queries(q_now, q_prev, qp, cfg)
213
+ pages = top_pages(qp)
214
+
215
+ def q(r):
216
+ query = r["keys"][0]
217
+ out = {"query": query, "impressions": int(r["impressions"]), "clicks": int(r["clicks"]),
218
+ "ctr": round(r["ctr"], 4), "position": round(r["position"], 1)}
219
+ if query in pages:
220
+ out["page"] = pages[query]
221
+ return out
222
+ return {
223
+ "window": {"start": str(start), "end": str(end), "days": days},
224
+ "examined": {"queries": res["queries_examined"], "queryPageRows": res["query_page_rows_examined"]},
225
+ "clicks": int(sum(r["clicks"] for r in q_now)),
226
+ "impressions": int(sum(r["impressions"] for r in q_now)),
227
+ "lowCtrTop3": [q(r) for r in res["low_ctr_top3"][:LIST_ROWS]],
228
+ "nearMiss": [q(r) for r in res["near_miss_4_20"][:LIST_ROWS]],
229
+ "rising": [dict(q(r), previous=int(r["previous"]), isNew=bool(r["is_new"])) for r in res["rising"][:LIST_ROWS]],
230
+ "cannibalization": [{"query": f["query"], "impressions": int(f["total_impressions"]),
231
+ "pages": [{"page": p["page"], "impressions": int(p["impressions"]), "position": p["position"]} for p in f["pages"][:3]]}
232
+ for f in res["cannibalization"][:LIST_ROWS]],
233
+ }
234
+
235
+
236
+ # --------------------------------------------------------------------------- errors
237
+
238
+ def check_urls(net, urls, workers=SITEMAP_CHECK_WORKERS):
239
+ """Status of each URL (0 = unreachable)."""
240
+ def one(u):
241
+ try:
242
+ return u, net.get(u, timeout=20)[0]
243
+ except Exception: # noqa: BLE001 - a URL that cannot be fetched is a finding, not a crash
244
+ return u, 0
245
+ with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as pool:
246
+ return dict(pool.map(one, urls))
247
+
248
+
249
+ def parse_error_list(raw):
250
+ """External JS-error source: a list, or {"errors": [...]}; each {message, count?, url?}."""
251
+ data = json.loads(raw)
252
+ items = data.get("errors", []) if isinstance(data, dict) else data
253
+ if not isinstance(items, list):
254
+ raise ValueError("expected a JSON list of errors")
255
+ out = []
256
+ for e in items:
257
+ if isinstance(e, dict) and e.get("message"):
258
+ out.append({"message": str(e["message"])[:200], "count": int(e.get("count") or 1),
259
+ **({"url": str(e["url"])} if e.get("url") else {})})
260
+ return sorted(out, key=lambda e: -e["count"])
261
+
262
+
263
+ def sitemap_slice(urls, n, today):
264
+ """Today's share of the sitemap: n URLs starting at a day-based offset, wrapping
265
+ around, so successive days check successive slices and the whole sitemap is
266
+ covered every ceil(len/n) days (not the same first n URLs every day).
267
+ Returns (urls, offset)."""
268
+ urls = sorted(urls)
269
+ if n <= 0 or len(urls) <= n:
270
+ return urls, 0
271
+ offset = (today.toordinal() * n) % len(urls)
272
+ return (urls + urls)[offset:offset + n], offset
273
+
274
+
275
+ def site_errors(so, net, cfg, settings, today=None):
276
+ out = {"brokenPages": [], "checked": 0, "jsErrors": [], "notes": []}
277
+ es = settings.get("errors") or {}
278
+ if es.get("checkSitemap") and cfg.get("sitemapUrl"):
279
+ all_urls = list(so.load_sitemap(net, cfg["sitemapUrl"]).keys())
280
+ urls, offset = sitemap_slice(all_urls, int(es.get("maxUrls") or SITEMAP_CHECK_DEFAULT), today or dt.date.today())
281
+ statuses = check_urls(net, urls)
282
+ out["checked"] = len(urls)
283
+ out["sitemapUrls"] = len(all_urls)
284
+ out["sitemapOffset"] = offset
285
+ out["brokenPages"] = [{"url": u, "status": s} for u, s in sorted(statuses.items()) if s != 200]
286
+ elif es.get("checkSitemap"):
287
+ out["notes"].append("no sitemapUrl in config: sitemap pages not checked")
288
+ if es.get("url"):
289
+ status, body = net.get(es["url"])
290
+ if status != 200:
291
+ out["notes"].append("errors.url answered HTTP %d" % status)
292
+ else:
293
+ out["jsErrors"] = parse_error_list(body)
294
+ elif es.get("command"):
295
+ proc = subprocess.run(es["command"], shell=True, capture_output=True, text=True, timeout=ERRORS_COMMAND_TIMEOUT_S)
296
+ if proc.returncode != 0:
297
+ out["notes"].append("errors.command exited %d: %s" % (proc.returncode, proc.stderr.strip()[:160]))
298
+ else:
299
+ out["jsErrors"] = parse_error_list(proc.stdout)
300
+ else:
301
+ out["notes"].append("no JS-error source configured (signalDigest.errors.url or .command)")
302
+ out["jsErrors"] = out["jsErrors"][:LIST_ROWS]
303
+ return out
304
+
305
+
306
+ # --------------------------------------------------------------------------- inbox / history / log
307
+
308
+ def read_json_file(path):
309
+ if not path:
310
+ return None
311
+ with open(path, encoding="utf-8") as f:
312
+ return json.load(f)
313
+
314
+
315
+ def inbox_summary(raw):
316
+ """gmail-search output (passed in by execute.sh) → count + messages."""
317
+ if raw is None:
318
+ return None
319
+ if raw.get("success") is False:
320
+ raise DigestError("inbox search failed: %s %s" % (raw.get("reason", ""), raw.get("message", "")))
321
+ messages = [{k: m.get(k) for k in ("from", "subject", "date", "snippet") if m.get(k)} for m in raw.get("messages", [])]
322
+ return {"query": raw.get("query"), "count": len(messages), "messages": messages}
323
+
324
+
325
+ def history_entries(raw):
326
+ if not raw:
327
+ return []
328
+ data = raw.get("data", raw)
329
+ return data.get("entries", []) if isinstance(data, dict) else []
330
+
331
+
332
+ def read_log(path):
333
+ if not path:
334
+ return ""
335
+ try:
336
+ with open(os.path.expanduser(path), encoding="utf-8") as f:
337
+ return f.read().lower()
338
+ except FileNotFoundError:
339
+ return ""
340
+
341
+
342
+ # --------------------------------------------------------------------------- candidates
343
+
344
+ def _cand(key, source, signal, proposal, effect, effort, score, subject, metric=None, experiment=None):
345
+ c = {"key": key, "source": source, "signal": signal, "proposal": proposal,
346
+ "expectedEffect": effect, "effort": effort, "score": round(score, 1), "subject": subject}
347
+ if metric:
348
+ c["metric"] = metric
349
+ if experiment:
350
+ # The experiment card a Do creates (#986): seo-ops metric source/measure + filters.
351
+ c["experiment"] = {k: v for k, v in experiment.items() if v}
352
+ return c
353
+
354
+
355
+ def build_candidates(signals, settings, site_url=""):
356
+ """Rule-based draft actions from the signals, best first (the lead decides)."""
357
+ out = []
358
+ t = settings["thresholds"]
359
+ ga4 = signals.get("ga4")
360
+ if isinstance(ga4, dict) and "sessions" in ga4:
361
+ ke, ss = ga4["keyEvents"], ga4["sessions"]
362
+ events = (settings.get("ga4") or {}).get("conversionEvents") or []
363
+ if ke["prev"] >= t["ga4MinPrevKeyEvents"] and ke["changePct"] is not None and ke["changePct"] <= -t["ga4DropPct"]:
364
+ out.append(_cand("ga4:key-events-drop", "ga4",
365
+ "Key events (form submissions) %d → %d (%s%%) vs the previous %d days" % (ke["prev"], ke["now"], ke["changePct"], settings["days"]),
366
+ "Find where the inquiry-form drop happens (form errors, landing pages that lost traffic) and fix the biggest cause",
367
+ "Back to ~%d key events per %d days" % (ke["prev"], settings["days"]), "M — half a day", 1000 + abs(ke["changePct"]),
368
+ "key-events-drop", "GA4 key events",
369
+ # seo-ops `conversions` = GA4 key events; one configured event narrows it.
370
+ {"source": "ga4", "measure": "conversions", "channel": "all", "event": events[0] if len(events) == 1 else None}))
371
+ if ss["prev"] >= t["ga4MinPrevSessions"] and ss["changePct"] is not None and ss["changePct"] <= -t["ga4DropPct"]:
372
+ out.append(_cand("ga4:sessions-drop", "ga4",
373
+ "Sessions %d → %d (%s%%) vs the previous %d days" % (ss["prev"], ss["now"], ss["changePct"], settings["days"]),
374
+ "Find which channel and landing pages lost the sessions and fix the top one",
375
+ "Recover ~%d sessions per %d days" % (ss["prev"] - ss["now"], settings["days"]), "M — half a day", 500 + abs(ss["changePct"]),
376
+ "sessions-drop", "GA4 sessions", {"source": "ga4", "measure": "sessions", "channel": "all"}))
377
+ gsc = signals.get("gsc")
378
+ if isinstance(gsc, dict) and "lowCtrTop3" in gsc:
379
+ days = gsc["window"]["days"]
380
+ for r in gsc["lowCtrTop3"]:
381
+ bench = BENCHMARK_CTR.get(max(1, min(5, int(round(r["position"])))), 0.06)
382
+ extra = per_week(max(0.0, (bench - r["ctr"]) * r["impressions"]), days)
383
+ page = short_path(r.get("page", ""), site_url) if r.get("page") else "the ranking page"
384
+ out.append(_cand("gsc:low-ctr:%s" % r["query"], "gsc",
385
+ "'%s' ranks #%d with %.1f%% CTR on %d impressions (%d days)" % (r["query"], round(r["position"]), 100 * r["ctr"], r["impressions"], days),
386
+ "Rewrite the title and description of %s to answer '%s'" % (page, r["query"]),
387
+ "CTR %.1f%% → ~%d%%: about +%d clicks a week" % (100 * r["ctr"], round(100 * bench), round(extra)),
388
+ "S — 1 h", extra, r["query"], "GSC CTR for '%s'" % r["query"],
389
+ {"source": "gsc", "measure": "ctr", "query": r["query"], "page": r.get("page")}))
390
+ for r in gsc["nearMiss"]:
391
+ target = BENCHMARK_CTR[NEAR_MISS_TARGET_POSITION]
392
+ extra = per_week(max(0.0, (target - r["ctr"]) * r["impressions"]), days)
393
+ page = short_path(r.get("page", ""), site_url) if r.get("page") else "the best page"
394
+ out.append(_cand("gsc:near-miss:%s" % r["query"], "gsc",
395
+ "'%s' averages position %s on %d impressions (%d days)" % (r["query"], r["position"], r["impressions"], days),
396
+ "Expand %s with a section that answers '%s' and add 2–3 internal links to it" % (page, r["query"]),
397
+ "Into the top 3: about +%d clicks a week" % round(extra),
398
+ "M — half a day", extra * 0.6, r["query"], "GSC position for '%s'" % r["query"],
399
+ {"source": "gsc", "measure": "position", "query": r["query"], "page": r.get("page")}))
400
+ covered = {r["query"] for r in gsc["lowCtrTop3"] + gsc["nearMiss"]} | {f["query"] for f in gsc["cannibalization"]}
401
+ for r in gsc["rising"]:
402
+ # Rising demand is a topic only when no page ranks for it yet: new,
403
+ # or beyond page 1, and not already an action above.
404
+ if r["query"] in covered or not (r["isNew"] or r["position"] > RISING_MAX_RANKED_POSITION):
405
+ continue
406
+ out.append(_cand("gsc:rising:%s" % r["query"], "gsc",
407
+ "'%s' impressions %d → %d%s (%d days)" % (r["query"], r["previous"], r["impressions"], " (new)" if r["isNew"] else "", days),
408
+ "Check whether a page answers '%s'; if none does, write one" % r["query"],
409
+ "Catch new demand: ~%d impressions a week" % round(per_week(r["impressions"], days)),
410
+ "M — half a day", per_week(r["impressions"], days) * 0.05, r["query"], "GSC clicks for '%s'" % r["query"],
411
+ {"source": "gsc", "measure": "clicks", "query": r["query"]}))
412
+ for f in gsc["cannibalization"]:
413
+ pages = [short_path(p["page"], site_url) for p in f["pages"]]
414
+ out.append(_cand("gsc:cannibal:%s" % f["query"], "gsc",
415
+ "'%s' is split across %d pages (%s), %d impressions" % (f["query"], len(pages), ", ".join(pages), f["impressions"]),
416
+ "Make %s the one page for '%s': differentiate the others or point them at it" % (pages[0], f["query"]),
417
+ "One stronger ranking for '%s'" % f["query"],
418
+ "M — half a day", per_week(f["impressions"], days) * 0.05, f["query"], "GSC position for '%s'" % f["query"],
419
+ {"source": "gsc", "measure": "position", "query": f["query"]}))
420
+ errors = signals.get("errors")
421
+ if isinstance(errors, dict):
422
+ for p in errors.get("brokenPages", []):
423
+ path = short_path(p["url"], site_url)
424
+ out.append(_cand("errors:broken:%s" % path, "errors",
425
+ "%s is in the sitemap but answers %s" % (path, "no response" if p["status"] == 0 else "HTTP %d" % p["status"]),
426
+ "Fix or redirect %s, and drop it from the sitemap if it is gone" % path,
427
+ "No dead page for visitors and crawlers", "S — 1 h", 300, path))
428
+ for e in errors.get("jsErrors", []):
429
+ msg = e["message"][:80]
430
+ out.append(_cand("errors:js:%s" % msg, "errors",
431
+ "JS error '%s' ×%d%s" % (msg, e["count"], " on %s" % short_path(e["url"], site_url) if e.get("url") else ""),
432
+ "Fix the JS error '%s'" % msg,
433
+ "Fewer broken sessions (×%d in the window)" % e["count"], "S — 1 h", 200 + e["count"], msg))
434
+ return sorted(out, key=lambda c: -c["score"])
435
+
436
+
437
+ def experiment_subjects(experiments, today):
438
+ """Queries / pages an experiment card already covers: planned or running, or
439
+ finished within EXPERIMENT_RECENT_DAYS. Maps subject → why."""
440
+ out = {}
441
+ cutoff = today - dt.timedelta(days=EXPERIMENT_RECENT_DAYS)
442
+ for e in experiments or []:
443
+ status = e.get("status")
444
+ if status == "cancelled":
445
+ continue
446
+ if status == "done":
447
+ try:
448
+ if dt.date.fromisoformat(str(e.get("updatedAt", ""))[:10]) < cutoff:
449
+ continue
450
+ except ValueError:
451
+ continue
452
+ metric = e.get("metric") or {}
453
+ why = "experiment %s (%s)" % (e.get("id", "?"), status)
454
+ if metric.get("query"):
455
+ out.setdefault(metric["query"].strip().lower(), why)
456
+ if metric.get("page"):
457
+ out.setdefault(short_path(metric["page"]).strip().lower(), why)
458
+ return out
459
+
460
+
461
+ def experiments_list(raw):
462
+ """GET /api/experiments output (envelope or bare list) → experiments."""
463
+ if not raw:
464
+ return []
465
+ data = raw.get("data", raw) if isinstance(raw, dict) else raw
466
+ return data if isinstance(data, list) else []
467
+
468
+
469
+ def filter_tried(candidates, history, log_text, experiments=None, today=None):
470
+ """Drop candidates whose key was Done/Skipped, whose query or page an experiment
471
+ card covers, or whose subject the experiment log mentions."""
472
+ blocked = {normalize_key(h["key"]): h for h in history if h.get("status") in ("do", "skip")}
473
+ covered = experiment_subjects(experiments, today or dt.date.today())
474
+ kept, tried = [], []
475
+ for c in candidates:
476
+ h = blocked.get(normalize_key(c["key"]))
477
+ page = (c.get("experiment") or {}).get("page")
478
+ hit = covered.get(c["subject"].strip().lower()) or (covered.get(short_path(page).strip().lower()) if page else None)
479
+ if h:
480
+ tried.append({"key": c["key"], "why": "owner chose %s on %s (%s)" % ("Do" if h["status"] == "do" else "Skip", str(h.get("at", ""))[:10], h.get("digestId", ""))})
481
+ elif hit:
482
+ tried.append({"key": c["key"], "why": hit})
483
+ elif log_text and len(c["subject"]) >= 4 and c["subject"].lower() in log_text:
484
+ tried.append({"key": c["key"], "why": "mentioned in the experiment log"})
485
+ else:
486
+ kept.append(c)
487
+ return kept, tried
488
+
489
+
490
+ # --------------------------------------------------------------------------- collect
491
+
492
+ def collect(so, cfg, net, today, history=None, inbox=None, days=None, experiments=None):
493
+ settings = digest_settings(cfg)
494
+ days = int(days or settings["days"])
495
+ settings["days"] = days
496
+ signals, sources = {}, {}
497
+
498
+ def run(name, configured, fn):
499
+ if not configured:
500
+ sources[name] = "not configured"
501
+ return
502
+ try:
503
+ signals[name] = fn()
504
+ sources[name] = "ok"
505
+ except Exception as e: # noqa: BLE001 - one bad source must not kill collect; it is reported
506
+ text = (str(e).splitlines() or [""])[0][:200] or type(e).__name__
507
+ sources[name] = "error: %s" % text
508
+
509
+ run("ga4", bool(cfg.get("ga4PropertyId")), lambda: ga4_summary(so, net, cfg, days, today, (settings.get("ga4") or {}).get("conversionEvents")))
510
+ run("gsc", bool(cfg.get("gscProperty")), lambda: gsc_opportunities(so, net, cfg, days, today))
511
+ run("inbox", inbox is not None, lambda: inbox_summary(inbox))
512
+ es = settings.get("errors") or {}
513
+ # Only a source that can examine something counts as configured.
514
+ run("errors", bool((es.get("checkSitemap") and cfg.get("sitemapUrl")) or es.get("url") or es.get("command")),
515
+ lambda: site_errors(so, net, cfg, settings, today))
516
+ entries = history_entries(history)
517
+ candidates, tried = filter_tried(build_candidates(signals, settings, cfg.get("siteUrl", "")), entries,
518
+ read_log(settings.get("experimentLog")), experiments_list(experiments), today)
519
+ for c in candidates:
520
+ c.pop("subject", None)
521
+ pending = [{"key": h["key"], "digestId": h.get("digestId")} for h in entries if h.get("status") == "open"]
522
+ return {
523
+ "site": settings["site"],
524
+ "project": settings.get("project"),
525
+ "date": str(today),
526
+ "days": days,
527
+ "sources": sources,
528
+ **signals,
529
+ "alreadyTried": tried,
530
+ "pendingOnOwner": pending,
531
+ "candidates": candidates[:MAX_CANDIDATES],
532
+ "next": "Pick 3-5 actions (the candidates, rewritten, or your own from the signals, e.g. repeated inbox questions), "
533
+ "best first, each with key/source/signal/proposal/expectedEffect/effort (+ metric, + experiment so that "
534
+ "Do creates an experiment card), and run: "
535
+ "execute.sh propose --config <config> --actions <file.json>",
536
+ }
537
+
538
+
539
+ def main(argv=None, net=None, today=None, so=None):
540
+ parser = argparse.ArgumentParser(prog="signal_digest.py", description="Daily signal digest for one site.")
541
+ sub = parser.add_subparsers(dest="command", required=True)
542
+ c = sub.add_parser("collect", help="gather the signals and draft actions (JSON)")
543
+ c.add_argument("--config", required=True)
544
+ c.add_argument("--days", type=int)
545
+ c.add_argument("--history", help="file with GET /api/signal-digests/history output")
546
+ c.add_argument("--inbox", help="file with gmail-search output")
547
+ c.add_argument("--experiments", help="file with GET /api/experiments output")
548
+ args = parser.parse_args(argv)
549
+ try:
550
+ so = so or load_seo_ops()
551
+ cfg = so.load_config(args.config)
552
+ net = net or so.Net(cfg)
553
+ result = collect(so, cfg, net, today or dt.date.today(), read_json_file(args.history), read_json_file(args.inbox), args.days,
554
+ read_json_file(args.experiments))
555
+ except DigestError as e:
556
+ print("signal-digest: %s" % e, file=sys.stderr)
557
+ return EXIT_SETUP
558
+ except Exception as e: # noqa: BLE001 - seo_ops errors carry their own message
559
+ if so is not None and isinstance(e, getattr(so, "SeoOpsError", ())):
560
+ print("signal-digest: %s" % e, file=sys.stderr)
561
+ return EXIT_SETUP
562
+ raise
563
+ print(json.dumps(result, ensure_ascii=False, indent=1))
564
+ if not any(v == "ok" for v in result["sources"].values()):
565
+ print("signal-digest: no source could be examined (%s). Nothing to propose from."
566
+ % ", ".join("%s: %s" % kv for kv in result["sources"].items()), file=sys.stderr)
567
+ return EXIT_EMPTY
568
+ return EXIT_OK
569
+
570
+
571
+ if __name__ == "__main__":
572
+ sys.exit(main())