crewly 1.20.194 → 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.
- package/config/roles/auditor/prompt.md +2 -2
- package/config/roles/orchestrator/fragments/communication.md +1 -1
- package/config/roles/orchestrator/prompt.md +15 -505
- package/config/skills/agent/computer-use/SKILL.md +6 -0
- package/config/skills/agent/core/experiment-card/SKILL.md +10 -5
- package/config/skills/agent/core/experiment-card/execute.sh +5 -3
- package/config/skills/agent/core/experiment-card/execute.test.sh +6 -0
- package/config/skills/agent/desktop-app-control/execute.sh +5 -2
- package/config/skills/orchestrator/send-message/execute.sh +19 -2
- package/config/skills/orchestrator/send-message/execute.test.sh +80 -0
- package/config/skills/registry.json +6 -5
- package/config/skills/team-leader/signal-digest/SKILL.md +110 -0
- package/config/skills/team-leader/signal-digest/execute.sh +173 -0
- package/config/skills/team-leader/signal-digest/execute.test.sh +123 -0
- package/config/skills/team-leader/signal-digest/signal-digest.config.example.json +23 -0
- package/config/skills/team-leader/signal-digest/signal_digest.py +572 -0
- package/config/skills/team-leader/signal-digest/signal_digest.test.py +319 -0
- package/dist/backend/backend/src/constants.d.ts +133 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +135 -2
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat/chat.controller.js +6 -0
- package/dist/backend/backend/src/controllers/chat/chat.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js +3 -1
- package/dist/backend/backend/src/controllers/chat-v2/chat-v2.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/marketplace/marketplace.routes.d.ts +1 -0
- package/dist/backend/backend/src/controllers/marketplace/marketplace.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/marketplace/marketplace.routes.js +1 -0
- package/dist/backend/backend/src/controllers/marketplace/marketplace.routes.js.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js +5 -2
- package/dist/backend/backend/src/controllers/monitoring/terminal.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/request/request.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/request/request.controller.js +5 -1
- package/dist/backend/backend/src/controllers/request/request.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/settings/settings.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/settings/settings.controller.js +18 -2
- package/dist/backend/backend/src/controllers/settings/settings.controller.js.map +1 -1
- package/dist/backend/backend/src/controllers/signal-digest/signal-digest.controller.d.ts +26 -0
- package/dist/backend/backend/src/controllers/signal-digest/signal-digest.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/signal-digest/signal-digest.controller.js +100 -0
- package/dist/backend/backend/src/controllers/signal-digest/signal-digest.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/trace/trace.controller.d.ts +47 -0
- package/dist/backend/backend/src/controllers/trace/trace.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/trace/trace.controller.js +169 -0
- package/dist/backend/backend/src/controllers/trace/trace.controller.js.map +1 -0
- package/dist/backend/backend/src/index.d.ts.map +1 -1
- package/dist/backend/backend/src/index.js +29 -19
- package/dist/backend/backend/src/index.js.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/api.routes.js +11 -0
- package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js +6 -1
- package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/backend/backend/src/services/agent/oauth-relogin-monitor.service.d.ts +1 -1
- package/dist/backend/backend/src/services/agent/oauth-relogin-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-session-recovery.d.ts +4 -2
- package/dist/backend/backend/src/services/agent/runtime-session-recovery.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/runtime-session-recovery.js +22 -6
- package/dist/backend/backend/src/services/agent/runtime-session-recovery.js.map +1 -1
- package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.js +2 -2
- package/dist/backend/backend/src/services/ai/prompt-modules/communication.module.js.map +1 -1
- package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts +6 -0
- package/dist/backend/backend/src/services/backup/backup-archive.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/backup/backup-archive.service.js +7 -1
- package/dist/backend/backend/src/services/backup/backup-archive.service.js.map +1 -1
- package/dist/backend/backend/src/services/boot/boot-announce.service.d.ts +1 -1
- package/dist/backend/backend/src/services/boot/boot-announce.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat/chat.service.d.ts +78 -2
- package/dist/backend/backend/src/services/chat/chat.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat/chat.service.js +210 -26
- package/dist/backend/backend/src/services/chat/chat.service.js.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts +4 -3
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js +7 -4
- package/dist/backend/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision-store.d.ts.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision-store.js +5 -1
- package/dist/backend/backend/src/services/decisions/decision-store.js.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.wiring.d.ts +10 -1
- package/dist/backend/backend/src/services/decisions/decision.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.wiring.js +20 -9
- package/dist/backend/backend/src/services/decisions/decision.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/experiments/experiment-verdict.d.ts +1 -1
- package/dist/backend/backend/src/services/experiments/experiment-verdict.js +2 -2
- package/dist/backend/backend/src/services/experiments/experiment-verdict.js.map +1 -1
- package/dist/backend/backend/src/services/experiments/experiment.service.d.ts +52 -3
- package/dist/backend/backend/src/services/experiments/experiment.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/experiments/experiment.service.js +178 -24
- package/dist/backend/backend/src/services/experiments/experiment.service.js.map +1 -1
- package/dist/backend/backend/src/services/experiments/experiment.wiring.d.ts +20 -5
- package/dist/backend/backend/src/services/experiments/experiment.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/experiments/experiment.wiring.js +27 -7
- package/dist/backend/backend/src/services/experiments/experiment.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/experiments/seo-ops-metric.fetcher.d.ts +7 -0
- package/dist/backend/backend/src/services/experiments/seo-ops-metric.fetcher.d.ts.map +1 -1
- package/dist/backend/backend/src/services/experiments/seo-ops-metric.fetcher.js +21 -1
- package/dist/backend/backend/src/services/experiments/seo-ops-metric.fetcher.js.map +1 -1
- package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.js +7 -0
- package/dist/backend/backend/src/services/messaging/owner-message-watchdog.service.js.map +1 -1
- package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts +7 -0
- package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/monitoring/token-usage.service.js +4 -0
- package/dist/backend/backend/src/services/monitoring/token-usage.service.js.map +1 -1
- package/dist/backend/backend/src/services/orc/orc-status-router.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orc/orc-status-router.service.js +10 -2
- package/dist/backend/backend/src/services/orc/orc-status-router.service.js.map +1 -1
- package/dist/backend/backend/src/services/orc/reply-destination.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/orc/reply-destination.wiring.js +16 -0
- package/dist/backend/backend/src/services/orc/reply-destination.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/project-ticket.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/project-tickets/project-ticket.service.js +2 -0
- package/dist/backend/backend/src/services/project-tickets/project-ticket.service.js.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js +15 -0
- package/dist/backend/backend/src/services/reconciler/reconciler.service.js.map +1 -1
- package/dist/backend/backend/src/services/session/session-handoff.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/session/session-handoff.service.js +8 -5
- package/dist/backend/backend/src/services/session/session-handoff.service.js.map +1 -1
- package/dist/backend/backend/src/services/signal-digest/signal-digest-card.d.ts +75 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest-card.d.ts.map +1 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest-card.js +187 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest-card.js.map +1 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest-contract.d.ts +86 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest-contract.d.ts.map +1 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest-contract.js +307 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest-contract.js.map +1 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest-store.d.ts +85 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest-store.d.ts.map +1 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest-store.js +185 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest-store.js.map +1 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest.service.d.ts +272 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest.service.js +570 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest.service.js.map +1 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest.wiring.d.ts +51 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest.wiring.d.ts.map +1 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest.wiring.js +116 -0
- package/dist/backend/backend/src/services/signal-digest/signal-digest.wiring.js.map +1 -0
- package/dist/backend/backend/src/services/slack/notify-reconciliation.service.d.ts +5 -1
- package/dist/backend/backend/src/services/slack/notify-reconciliation.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/notify-reconciliation.service.js +6 -2
- package/dist/backend/backend/src/services/slack/notify-reconciliation.service.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts +2 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +3 -2
- package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts +1 -1
- package/dist/backend/backend/src/services/slack/slack-relogin-dm.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.d.ts +25 -1
- package/dist/backend/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack.service.js +44 -7
- package/dist/backend/backend/src/services/slack/slack.service.js.map +1 -1
- package/dist/backend/backend/src/services/system/auto-update.service.d.ts +2 -2
- package/dist/backend/backend/src/services/system/auto-update.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/task-pool/pool-storage.d.ts.map +1 -1
- package/dist/backend/backend/src/services/task-pool/pool-storage.js +4 -0
- package/dist/backend/backend/src/services/task-pool/pool-storage.js.map +1 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/task-pool/task-pool.service.js +2 -0
- package/dist/backend/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/dist/backend/backend/src/services/trace/trace-context.service.d.ts +195 -0
- package/dist/backend/backend/src/services/trace/trace-context.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/trace/trace-context.service.js +432 -0
- package/dist/backend/backend/src/services/trace/trace-context.service.js.map +1 -0
- package/dist/backend/backend/src/services/trace/trace-http.middleware.d.ts +42 -0
- package/dist/backend/backend/src/services/trace/trace-http.middleware.d.ts.map +1 -0
- package/dist/backend/backend/src/services/trace/trace-http.middleware.js +146 -0
- package/dist/backend/backend/src/services/trace/trace-http.middleware.js.map +1 -0
- package/dist/backend/backend/src/services/trace/trace-markers.d.ts +93 -0
- package/dist/backend/backend/src/services/trace/trace-markers.d.ts.map +1 -0
- package/dist/backend/backend/src/services/trace/trace-markers.js +162 -0
- package/dist/backend/backend/src/services/trace/trace-markers.js.map +1 -0
- package/dist/backend/backend/src/services/trace/trace-recorder.d.ts +290 -0
- package/dist/backend/backend/src/services/trace/trace-recorder.d.ts.map +1 -0
- package/dist/backend/backend/src/services/trace/trace-recorder.js +669 -0
- package/dist/backend/backend/src/services/trace/trace-recorder.js.map +1 -0
- package/dist/backend/backend/src/services/trace/trace-store.d.ts +269 -0
- package/dist/backend/backend/src/services/trace/trace-store.d.ts.map +1 -0
- package/dist/backend/backend/src/services/trace/trace-store.js +602 -0
- package/dist/backend/backend/src/services/trace/trace-store.js.map +1 -0
- package/dist/backend/backend/src/services/trace/trace.types.d.ts +118 -0
- package/dist/backend/backend/src/services/trace/trace.types.d.ts.map +1 -0
- package/dist/backend/backend/src/services/trace/trace.types.js +85 -0
- package/dist/backend/backend/src/services/trace/trace.types.js.map +1 -0
- package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.boot.d.ts +1 -1
- package/dist/backend/backend/src/services/v3/owner-receipt/owner-receipt.boot.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/request.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/request.service.js +4 -0
- package/dist/backend/backend/src/services/v3/request.service.js.map +1 -1
- package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.d.ts.map +1 -1
- package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js +22 -2
- package/dist/backend/backend/src/services/v3/workitem-dispatch.subscriber.js.map +1 -1
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts +3 -0
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js +7 -0
- package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js.map +1 -1
- package/dist/backend/backend/src/types/experiment.types.d.ts +8 -6
- package/dist/backend/backend/src/types/experiment.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/experiment.types.js.map +1 -1
- package/dist/backend/backend/src/types/signal-digest.types.d.ts +185 -0
- package/dist/backend/backend/src/types/signal-digest.types.d.ts.map +1 -0
- package/dist/backend/backend/src/types/signal-digest.types.js +21 -0
- package/dist/backend/backend/src/types/signal-digest.types.js.map +1 -0
- package/dist/backend/backend/src/types/v2/request.types.d.ts +4 -0
- package/dist/backend/backend/src/types/v2/request.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/request.types.js.map +1 -1
- package/dist/backend/backend/src/types/v2/work-item.types.d.ts +4 -0
- package/dist/backend/backend/src/types/v2/work-item.types.d.ts.map +1 -1
- package/dist/backend/backend/src/types/v2/work-item.types.js +1 -0
- package/dist/backend/backend/src/types/v2/work-item.types.js.map +1 -1
- package/dist/backend/backend/src/utils/security.d.ts +2 -0
- package/dist/backend/backend/src/utils/security.d.ts.map +1 -1
- package/dist/backend/backend/src/utils/security.js +3 -1
- package/dist/backend/backend/src/utils/security.js.map +1 -1
- package/dist/backend/backend/src/websocket/chat.gateway.d.ts +9 -0
- package/dist/backend/backend/src/websocket/chat.gateway.d.ts.map +1 -1
- package/dist/backend/backend/src/websocket/chat.gateway.js +26 -0
- package/dist/backend/backend/src/websocket/chat.gateway.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +133 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +135 -2
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js +6 -1
- package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
- package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.d.ts +1 -1
- package/dist/cli/backend/src/services/agent/oauth-relogin-monitor.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/runtime-session-recovery.d.ts +4 -2
- package/dist/cli/backend/src/services/agent/runtime-session-recovery.d.ts.map +1 -1
- package/dist/cli/backend/src/services/agent/runtime-session-recovery.js +22 -6
- package/dist/cli/backend/src/services/agent/runtime-session-recovery.js.map +1 -1
- package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.js +2 -2
- package/dist/cli/backend/src/services/ai/prompt-modules/communication.module.js.map +1 -1
- package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts +6 -0
- package/dist/cli/backend/src/services/backup/backup-archive.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/backup/backup-archive.service.js +7 -1
- package/dist/cli/backend/src/services/backup/backup-archive.service.js.map +1 -1
- package/dist/cli/backend/src/services/chat/chat.service.d.ts +185 -0
- package/dist/cli/backend/src/services/chat/chat.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/chat/chat.service.js +558 -0
- package/dist/cli/backend/src/services/chat/chat.service.js.map +1 -0
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts +4 -3
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js +7 -4
- package/dist/cli/backend/src/services/chat-v2/chat-v2.service.js.map +1 -1
- package/dist/cli/backend/src/services/decisions/decision-store.d.ts.map +1 -1
- package/dist/cli/backend/src/services/decisions/decision-store.js +5 -1
- package/dist/cli/backend/src/services/decisions/decision-store.js.map +1 -1
- package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.js +7 -0
- package/dist/cli/backend/src/services/messaging/owner-message-watchdog.service.js.map +1 -1
- package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts +7 -0
- package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/monitoring/token-usage.service.js +4 -0
- package/dist/cli/backend/src/services/monitoring/token-usage.service.js.map +1 -1
- package/dist/cli/backend/src/services/project-tickets/project-ticket.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/project-tickets/project-ticket.service.js +2 -0
- package/dist/cli/backend/src/services/project-tickets/project-ticket.service.js.map +1 -1
- package/dist/cli/backend/src/services/session/session-handoff.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/session/session-handoff.service.js +8 -5
- package/dist/cli/backend/src/services/session/session-handoff.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts +2 -1
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +3 -2
- package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack.service.d.ts +25 -1
- package/dist/cli/backend/src/services/slack/slack.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack.service.js +44 -7
- package/dist/cli/backend/src/services/slack/slack.service.js.map +1 -1
- package/dist/cli/backend/src/services/task-pool/pool-storage.d.ts.map +1 -1
- package/dist/cli/backend/src/services/task-pool/pool-storage.js +4 -0
- package/dist/cli/backend/src/services/task-pool/pool-storage.js.map +1 -1
- package/dist/cli/backend/src/services/task-pool/task-pool.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/task-pool/task-pool.service.js +2 -0
- package/dist/cli/backend/src/services/task-pool/task-pool.service.js.map +1 -1
- package/dist/cli/backend/src/services/trace/trace-context.service.d.ts +195 -0
- package/dist/cli/backend/src/services/trace/trace-context.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/trace/trace-context.service.js +432 -0
- package/dist/cli/backend/src/services/trace/trace-context.service.js.map +1 -0
- package/dist/cli/backend/src/services/trace/trace-markers.d.ts +93 -0
- package/dist/cli/backend/src/services/trace/trace-markers.d.ts.map +1 -0
- package/dist/cli/backend/src/services/trace/trace-markers.js +162 -0
- package/dist/cli/backend/src/services/trace/trace-markers.js.map +1 -0
- package/dist/cli/backend/src/services/trace/trace-recorder.d.ts +290 -0
- package/dist/cli/backend/src/services/trace/trace-recorder.d.ts.map +1 -0
- package/dist/cli/backend/src/services/trace/trace-recorder.js +669 -0
- package/dist/cli/backend/src/services/trace/trace-recorder.js.map +1 -0
- package/dist/cli/backend/src/services/trace/trace-store.d.ts +269 -0
- package/dist/cli/backend/src/services/trace/trace-store.d.ts.map +1 -0
- package/dist/cli/backend/src/services/trace/trace-store.js +602 -0
- package/dist/cli/backend/src/services/trace/trace-store.js.map +1 -0
- package/dist/cli/backend/src/services/trace/trace.types.d.ts +118 -0
- package/dist/cli/backend/src/services/trace/trace.types.d.ts.map +1 -0
- package/dist/cli/backend/src/services/trace/trace.types.js +85 -0
- package/dist/cli/backend/src/services/trace/trace.types.js.map +1 -0
- package/dist/cli/backend/src/services/v3/request.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/v3/request.service.js +4 -0
- package/dist/cli/backend/src/services/v3/request.service.js.map +1 -1
- package/dist/cli/backend/src/types/v2/request.types.d.ts +4 -0
- package/dist/cli/backend/src/types/v2/request.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/v2/request.types.js.map +1 -1
- package/dist/cli/backend/src/types/v2/work-item.types.d.ts +4 -0
- package/dist/cli/backend/src/types/v2/work-item.types.d.ts.map +1 -1
- package/dist/cli/backend/src/types/v2/work-item.types.js +1 -0
- package/dist/cli/backend/src/types/v2/work-item.types.js.map +1 -1
- package/dist/cli/backend/src/utils/security.d.ts +196 -0
- package/dist/cli/backend/src/utils/security.d.ts.map +1 -0
- package/dist/cli/backend/src/utils/security.js +538 -0
- package/dist/cli/backend/src/utils/security.js.map +1 -0
- package/dist/cli/backend/src/websocket/chat.gateway.d.ts +9 -0
- package/dist/cli/backend/src/websocket/chat.gateway.d.ts.map +1 -1
- package/dist/cli/backend/src/websocket/chat.gateway.js +26 -0
- package/dist/cli/backend/src/websocket/chat.gateway.js.map +1 -1
- 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())
|