reflectt-node 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +178 -0
- package/README.md +188 -0
- package/dist/activationEvents.d.ts +110 -0
- package/dist/activationEvents.d.ts.map +1 -0
- package/dist/activationEvents.js +378 -0
- package/dist/activationEvents.js.map +1 -0
- package/dist/activity-signal.d.ts +30 -0
- package/dist/activity-signal.d.ts.map +1 -0
- package/dist/activity-signal.js +93 -0
- package/dist/activity-signal.js.map +1 -0
- package/dist/alert-integrity.d.ts +100 -0
- package/dist/alert-integrity.d.ts.map +1 -0
- package/dist/alert-integrity.js +333 -0
- package/dist/alert-integrity.js.map +1 -0
- package/dist/alert-preflight.d.ts +40 -0
- package/dist/alert-preflight.d.ts.map +1 -0
- package/dist/alert-preflight.js +235 -0
- package/dist/alert-preflight.js.map +1 -0
- package/dist/analytics.d.ts +131 -0
- package/dist/analytics.d.ts.map +1 -0
- package/dist/analytics.js +371 -0
- package/dist/analytics.js.map +1 -0
- package/dist/artifact-mirror.d.ts +26 -0
- package/dist/artifact-mirror.d.ts.map +1 -0
- package/dist/artifact-mirror.js +170 -0
- package/dist/artifact-mirror.js.map +1 -0
- package/dist/artifact-resolver.d.ts +48 -0
- package/dist/artifact-resolver.d.ts.map +1 -0
- package/dist/artifact-resolver.js +164 -0
- package/dist/artifact-resolver.js.map +1 -0
- package/dist/assignment.d.ts +116 -0
- package/dist/assignment.d.ts.map +1 -0
- package/dist/assignment.js +475 -0
- package/dist/assignment.js.map +1 -0
- package/dist/auditLedger.d.ts +50 -0
- package/dist/auditLedger.d.ts.map +1 -0
- package/dist/auditLedger.js +136 -0
- package/dist/auditLedger.js.map +1 -0
- package/dist/boardHealthWorker.d.ts +134 -0
- package/dist/boardHealthWorker.d.ts.map +1 -0
- package/dist/boardHealthWorker.js +882 -0
- package/dist/boardHealthWorker.js.map +1 -0
- package/dist/bootstrap-team.d.ts +42 -0
- package/dist/bootstrap-team.d.ts.map +1 -0
- package/dist/bootstrap-team.js +111 -0
- package/dist/bootstrap-team.js.map +1 -0
- package/dist/buildInfo.d.ts +17 -0
- package/dist/buildInfo.d.ts.map +1 -0
- package/dist/buildInfo.js +56 -0
- package/dist/buildInfo.js.map +1 -0
- package/dist/calendar-events.d.ts +133 -0
- package/dist/calendar-events.d.ts.map +1 -0
- package/dist/calendar-events.js +615 -0
- package/dist/calendar-events.js.map +1 -0
- package/dist/calendar-ical.d.ts +41 -0
- package/dist/calendar-ical.d.ts.map +1 -0
- package/dist/calendar-ical.js +413 -0
- package/dist/calendar-ical.js.map +1 -0
- package/dist/calendar-reminder-engine.d.ts +10 -0
- package/dist/calendar-reminder-engine.d.ts.map +1 -0
- package/dist/calendar-reminder-engine.js +143 -0
- package/dist/calendar-reminder-engine.js.map +1 -0
- package/dist/calendar.d.ts +75 -0
- package/dist/calendar.d.ts.map +1 -0
- package/dist/calendar.js +391 -0
- package/dist/calendar.js.map +1 -0
- package/dist/canvas-multiplexer.d.ts +44 -0
- package/dist/canvas-multiplexer.d.ts.map +1 -0
- package/dist/canvas-multiplexer.js +150 -0
- package/dist/canvas-multiplexer.js.map +1 -0
- package/dist/canvas-slots.d.ts +83 -0
- package/dist/canvas-slots.d.ts.map +1 -0
- package/dist/canvas-slots.js +144 -0
- package/dist/canvas-slots.js.map +1 -0
- package/dist/canvas-types.d.ts +56 -0
- package/dist/canvas-types.d.ts.map +1 -0
- package/dist/canvas-types.js +54 -0
- package/dist/canvas-types.js.map +1 -0
- package/dist/cf-keepalive.d.ts +40 -0
- package/dist/cf-keepalive.d.ts.map +1 -0
- package/dist/cf-keepalive.js +153 -0
- package/dist/cf-keepalive.js.map +1 -0
- package/dist/changeFeed.d.ts +38 -0
- package/dist/changeFeed.d.ts.map +1 -0
- package/dist/changeFeed.js +324 -0
- package/dist/changeFeed.js.map +1 -0
- package/dist/channels.d.ts +28 -0
- package/dist/channels.d.ts.map +1 -0
- package/dist/channels.js +23 -0
- package/dist/channels.js.map +1 -0
- package/dist/chat-approval-detector.d.ts +47 -0
- package/dist/chat-approval-detector.d.ts.map +1 -0
- package/dist/chat-approval-detector.js +224 -0
- package/dist/chat-approval-detector.js.map +1 -0
- package/dist/chat.d.ts +119 -0
- package/dist/chat.d.ts.map +1 -0
- package/dist/chat.js +666 -0
- package/dist/chat.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +1142 -0
- package/dist/cli.js.map +1 -0
- package/dist/cloud.d.ts +45 -0
- package/dist/cloud.d.ts.map +1 -0
- package/dist/cloud.js +962 -0
- package/dist/cloud.js.map +1 -0
- package/dist/config.d.ts +17 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +33 -0
- package/dist/config.js.map +1 -0
- package/dist/connectivity.d.ts +59 -0
- package/dist/connectivity.d.ts.map +1 -0
- package/dist/connectivity.js +173 -0
- package/dist/connectivity.js.map +1 -0
- package/dist/contacts.d.ts +59 -0
- package/dist/contacts.d.ts.map +1 -0
- package/dist/contacts.js +183 -0
- package/dist/contacts.js.map +1 -0
- package/dist/content.d.ts +130 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +186 -0
- package/dist/content.js.map +1 -0
- package/dist/context-budget.d.ts +87 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +459 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/continuity-loop.d.ts +55 -0
- package/dist/continuity-loop.d.ts.map +1 -0
- package/dist/continuity-loop.js +267 -0
- package/dist/continuity-loop.js.map +1 -0
- package/dist/dashboard.d.ts +6 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +2348 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/db.d.ts +44 -0
- package/dist/db.d.ts.map +1 -0
- package/dist/db.js +648 -0
- package/dist/db.js.map +1 -0
- package/dist/doctor.d.ts +30 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +159 -0
- package/dist/doctor.js.map +1 -0
- package/dist/duplicateClosureGuard.d.ts +31 -0
- package/dist/duplicateClosureGuard.d.ts.map +1 -0
- package/dist/duplicateClosureGuard.js +83 -0
- package/dist/duplicateClosureGuard.js.map +1 -0
- package/dist/embeddings.d.ts +13 -0
- package/dist/embeddings.d.ts.map +1 -0
- package/dist/embeddings.js +78 -0
- package/dist/embeddings.js.map +1 -0
- package/dist/escalation.d.ts +80 -0
- package/dist/escalation.d.ts.map +1 -0
- package/dist/escalation.js +213 -0
- package/dist/escalation.js.map +1 -0
- package/dist/events.d.ts +130 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +382 -0
- package/dist/events.js.map +1 -0
- package/dist/executionSweeper.d.ts +97 -0
- package/dist/executionSweeper.d.ts.map +1 -0
- package/dist/executionSweeper.js +875 -0
- package/dist/executionSweeper.js.map +1 -0
- package/dist/experiments.d.ts +47 -0
- package/dist/experiments.d.ts.map +1 -0
- package/dist/experiments.js +133 -0
- package/dist/experiments.js.map +1 -0
- package/dist/feedback.d.ts +179 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +397 -0
- package/dist/feedback.js.map +1 -0
- package/dist/files.d.ts +52 -0
- package/dist/files.d.ts.map +1 -0
- package/dist/files.js +172 -0
- package/dist/files.js.map +1 -0
- package/dist/format-duration.d.ts +19 -0
- package/dist/format-duration.d.ts.map +1 -0
- package/dist/format-duration.js +33 -0
- package/dist/format-duration.js.map +1 -0
- package/dist/github-actor-auth.d.ts +20 -0
- package/dist/github-actor-auth.d.ts.map +1 -0
- package/dist/github-actor-auth.js +54 -0
- package/dist/github-actor-auth.js.map +1 -0
- package/dist/github-ci.d.ts +16 -0
- package/dist/github-ci.d.ts.map +1 -0
- package/dist/github-ci.js +37 -0
- package/dist/github-ci.js.map +1 -0
- package/dist/github-identity.d.ts +30 -0
- package/dist/github-identity.d.ts.map +1 -0
- package/dist/github-identity.js +96 -0
- package/dist/github-identity.js.map +1 -0
- package/dist/github-reviews.d.ts +24 -0
- package/dist/github-reviews.d.ts.map +1 -0
- package/dist/github-reviews.js +56 -0
- package/dist/github-reviews.js.map +1 -0
- package/dist/health.d.ts +391 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +1841 -0
- package/dist/health.js.map +1 -0
- package/dist/host-keepalive.d.ts +22 -0
- package/dist/host-keepalive.d.ts.map +1 -0
- package/dist/host-keepalive.js +126 -0
- package/dist/host-keepalive.js.map +1 -0
- package/dist/host-registry.d.ts +43 -0
- package/dist/host-registry.d.ts.map +1 -0
- package/dist/host-registry.js +93 -0
- package/dist/host-registry.js.map +1 -0
- package/dist/inbox.d.ts +87 -0
- package/dist/inbox.d.ts.map +1 -0
- package/dist/inbox.js +410 -0
- package/dist/inbox.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +306 -0
- package/dist/index.js.map +1 -0
- package/dist/insight-mutation.d.ts +32 -0
- package/dist/insight-mutation.d.ts.map +1 -0
- package/dist/insight-mutation.js +160 -0
- package/dist/insight-mutation.js.map +1 -0
- package/dist/insight-promotion.d.ts +89 -0
- package/dist/insight-promotion.d.ts.map +1 -0
- package/dist/insight-promotion.js +278 -0
- package/dist/insight-promotion.js.map +1 -0
- package/dist/insight-task-bridge.d.ts +77 -0
- package/dist/insight-task-bridge.d.ts.map +1 -0
- package/dist/insight-task-bridge.js +556 -0
- package/dist/insight-task-bridge.js.map +1 -0
- package/dist/insights.d.ts +222 -0
- package/dist/insights.d.ts.map +1 -0
- package/dist/insights.js +871 -0
- package/dist/insights.js.map +1 -0
- package/dist/intake-pipeline.d.ts +74 -0
- package/dist/intake-pipeline.d.ts.map +1 -0
- package/dist/intake-pipeline.js +199 -0
- package/dist/intake-pipeline.js.map +1 -0
- package/dist/intensity.d.ts +31 -0
- package/dist/intensity.d.ts.map +1 -0
- package/dist/intensity.js +94 -0
- package/dist/intensity.js.map +1 -0
- package/dist/knowledge-auto-index.d.ts +37 -0
- package/dist/knowledge-auto-index.d.ts.map +1 -0
- package/dist/knowledge-auto-index.js +149 -0
- package/dist/knowledge-auto-index.js.map +1 -0
- package/dist/knowledge-docs.d.ts +45 -0
- package/dist/knowledge-docs.d.ts.map +1 -0
- package/dist/knowledge-docs.js +188 -0
- package/dist/knowledge-docs.js.map +1 -0
- package/dist/lane-config.d.ts +25 -0
- package/dist/lane-config.d.ts.map +1 -0
- package/dist/lane-config.js +105 -0
- package/dist/lane-config.js.map +1 -0
- package/dist/lineage.d.ts +86 -0
- package/dist/lineage.d.ts.map +1 -0
- package/dist/lineage.js +303 -0
- package/dist/lineage.js.map +1 -0
- package/dist/logStore.d.ts +25 -0
- package/dist/logStore.d.ts.map +1 -0
- package/dist/logStore.js +83 -0
- package/dist/logStore.js.map +1 -0
- package/dist/manage.d.ts +12 -0
- package/dist/manage.d.ts.map +1 -0
- package/dist/manage.js +253 -0
- package/dist/manage.js.map +1 -0
- package/dist/mcp.d.ts +5 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +604 -0
- package/dist/mcp.js.map +1 -0
- package/dist/memory.d.ts +47 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +149 -0
- package/dist/memory.js.map +1 -0
- package/dist/mention-ack.d.ts +80 -0
- package/dist/mention-ack.d.ts.map +1 -0
- package/dist/mention-ack.js +175 -0
- package/dist/mention-ack.js.map +1 -0
- package/dist/messageRouter.d.ts +60 -0
- package/dist/messageRouter.d.ts.map +1 -0
- package/dist/messageRouter.js +309 -0
- package/dist/messageRouter.js.map +1 -0
- package/dist/mutationAlert.d.ts +44 -0
- package/dist/mutationAlert.d.ts.map +1 -0
- package/dist/mutationAlert.js +174 -0
- package/dist/mutationAlert.js.map +1 -0
- package/dist/noise-budget.d.ts +136 -0
- package/dist/noise-budget.d.ts.map +1 -0
- package/dist/noise-budget.js +340 -0
- package/dist/noise-budget.js.map +1 -0
- package/dist/notifications.d.ts +67 -0
- package/dist/notifications.d.ts.map +1 -0
- package/dist/notifications.js +253 -0
- package/dist/notifications.js.map +1 -0
- package/dist/openclaw.d.ts +34 -0
- package/dist/openclaw.d.ts.map +1 -0
- package/dist/openclaw.js +208 -0
- package/dist/openclaw.js.map +1 -0
- package/dist/pause-controls.d.ts +31 -0
- package/dist/pause-controls.d.ts.map +1 -0
- package/dist/pause-controls.js +130 -0
- package/dist/pause-controls.js.map +1 -0
- package/dist/pidlock.d.ts +25 -0
- package/dist/pidlock.d.ts.map +1 -0
- package/dist/pidlock.js +179 -0
- package/dist/pidlock.js.map +1 -0
- package/dist/policy.d.ts +139 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +264 -0
- package/dist/policy.js.map +1 -0
- package/dist/polls.d.ts +47 -0
- package/dist/polls.d.ts.map +1 -0
- package/dist/polls.js +162 -0
- package/dist/polls.js.map +1 -0
- package/dist/portability.d.ts +55 -0
- package/dist/portability.d.ts.map +1 -0
- package/dist/portability.js +292 -0
- package/dist/portability.js.map +1 -0
- package/dist/pr-integrity.d.ts +45 -0
- package/dist/pr-integrity.d.ts.map +1 -0
- package/dist/pr-integrity.js +124 -0
- package/dist/pr-integrity.js.map +1 -0
- package/dist/prAutoMerge.d.ts +62 -0
- package/dist/prAutoMerge.d.ts.map +1 -0
- package/dist/prAutoMerge.js +493 -0
- package/dist/prAutoMerge.js.map +1 -0
- package/dist/preflight.d.ts +66 -0
- package/dist/preflight.d.ts.map +1 -0
- package/dist/preflight.js +864 -0
- package/dist/preflight.js.map +1 -0
- package/dist/presence.d.ts +98 -0
- package/dist/presence.d.ts.map +1 -0
- package/dist/presence.js +347 -0
- package/dist/presence.js.map +1 -0
- package/dist/provisioning.d.ts +101 -0
- package/dist/provisioning.d.ts.map +1 -0
- package/dist/provisioning.js +430 -0
- package/dist/provisioning.js.map +1 -0
- package/dist/reflection-automation.d.ts +59 -0
- package/dist/reflection-automation.d.ts.map +1 -0
- package/dist/reflection-automation.js +350 -0
- package/dist/reflection-automation.js.map +1 -0
- package/dist/reflections.d.ts +65 -0
- package/dist/reflections.d.ts.map +1 -0
- package/dist/reflections.js +306 -0
- package/dist/reflections.js.map +1 -0
- package/dist/release.d.ts +67 -0
- package/dist/release.d.ts.map +1 -0
- package/dist/release.js +275 -0
- package/dist/release.js.map +1 -0
- package/dist/request-tracker.d.ts +36 -0
- package/dist/request-tracker.d.ts.map +1 -0
- package/dist/request-tracker.js +109 -0
- package/dist/request-tracker.js.map +1 -0
- package/dist/research.d.ts +75 -0
- package/dist/research.d.ts.map +1 -0
- package/dist/research.js +171 -0
- package/dist/research.js.map +1 -0
- package/dist/routing-approvals.d.ts +73 -0
- package/dist/routing-approvals.d.ts.map +1 -0
- package/dist/routing-approvals.js +88 -0
- package/dist/routing-approvals.js.map +1 -0
- package/dist/routing-override.d.ts +94 -0
- package/dist/routing-override.d.ts.map +1 -0
- package/dist/routing-override.js +290 -0
- package/dist/routing-override.js.map +1 -0
- package/dist/scope-routing.d.ts +18 -0
- package/dist/scope-routing.d.ts.map +1 -0
- package/dist/scope-routing.js +29 -0
- package/dist/scope-routing.js.map +1 -0
- package/dist/secrets.d.ts +77 -0
- package/dist/secrets.d.ts.map +1 -0
- package/dist/secrets.js +287 -0
- package/dist/secrets.js.map +1 -0
- package/dist/server.d.ts +3 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +10887 -0
- package/dist/server.js.map +1 -0
- package/dist/service-probe.d.ts +53 -0
- package/dist/service-probe.d.ts.map +1 -0
- package/dist/service-probe.js +225 -0
- package/dist/service-probe.js.map +1 -0
- package/dist/shared-workspace-api.d.ts +73 -0
- package/dist/shared-workspace-api.d.ts.map +1 -0
- package/dist/shared-workspace-api.js +281 -0
- package/dist/shared-workspace-api.js.map +1 -0
- package/dist/shipped-heartbeat.d.ts +91 -0
- package/dist/shipped-heartbeat.d.ts.map +1 -0
- package/dist/shipped-heartbeat.js +272 -0
- package/dist/shipped-heartbeat.js.map +1 -0
- package/dist/starter-team.d.ts +23 -0
- package/dist/starter-team.d.ts.map +1 -0
- package/dist/starter-team.js +88 -0
- package/dist/starter-team.js.map +1 -0
- package/dist/suppression-ledger.d.ts +73 -0
- package/dist/suppression-ledger.d.ts.map +1 -0
- package/dist/suppression-ledger.js +125 -0
- package/dist/suppression-ledger.js.map +1 -0
- package/dist/system-loop-state.d.ts +4 -0
- package/dist/system-loop-state.d.ts.map +1 -0
- package/dist/system-loop-state.js +40 -0
- package/dist/system-loop-state.js.map +1 -0
- package/dist/taskCommentIngest.d.ts +43 -0
- package/dist/taskCommentIngest.d.ts.map +1 -0
- package/dist/taskCommentIngest.js +59 -0
- package/dist/taskCommentIngest.js.map +1 -0
- package/dist/taskPrecheck.d.ts +20 -0
- package/dist/taskPrecheck.d.ts.map +1 -0
- package/dist/taskPrecheck.js +329 -0
- package/dist/taskPrecheck.js.map +1 -0
- package/dist/taskStateSync.d.ts +8 -0
- package/dist/taskStateSync.d.ts.map +1 -0
- package/dist/taskStateSync.js +79 -0
- package/dist/taskStateSync.js.map +1 -0
- package/dist/tasks.d.ts +140 -0
- package/dist/tasks.d.ts.map +1 -0
- package/dist/tasks.js +1281 -0
- package/dist/tasks.js.map +1 -0
- package/dist/team-config.d.ts +24 -0
- package/dist/team-config.d.ts.map +1 -0
- package/dist/team-config.js +221 -0
- package/dist/team-config.js.map +1 -0
- package/dist/team-doctor.d.ts +22 -0
- package/dist/team-doctor.d.ts.map +1 -0
- package/dist/team-doctor.js +270 -0
- package/dist/team-doctor.js.map +1 -0
- package/dist/team-pulse.d.ts +52 -0
- package/dist/team-pulse.d.ts.map +1 -0
- package/dist/team-pulse.js +176 -0
- package/dist/team-pulse.js.map +1 -0
- package/dist/telemetry.d.ts +74 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +256 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/test-task-filter.d.ts +21 -0
- package/dist/test-task-filter.d.ts.map +1 -0
- package/dist/test-task-filter.js +48 -0
- package/dist/test-task-filter.js.map +1 -0
- package/dist/types.d.ts +126 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -0
- package/dist/usage-tracking.d.ts +101 -0
- package/dist/usage-tracking.d.ts.map +1 -0
- package/dist/usage-tracking.js +325 -0
- package/dist/usage-tracking.js.map +1 -0
- package/dist/vector-store.d.ts +87 -0
- package/dist/vector-store.d.ts.map +1 -0
- package/dist/vector-store.js +247 -0
- package/dist/vector-store.js.map +1 -0
- package/dist/watchdog/idleNudgeLane.d.ts +22 -0
- package/dist/watchdog/idleNudgeLane.d.ts.map +1 -0
- package/dist/watchdog/idleNudgeLane.js +98 -0
- package/dist/watchdog/idleNudgeLane.js.map +1 -0
- package/dist/webhooks.d.ts +103 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +398 -0
- package/dist/webhooks.js.map +1 -0
- package/dist/working-contract.d.ts +42 -0
- package/dist/working-contract.d.ts.map +1 -0
- package/dist/working-contract.js +228 -0
- package/dist/working-contract.js.map +1 -0
- package/dist/ws-heartbeat.d.ts +66 -0
- package/dist/ws-heartbeat.d.ts.map +1 -0
- package/dist/ws-heartbeat.js +174 -0
- package/dist/ws-heartbeat.js.map +1 -0
- package/package.json +87 -0
- package/plugins/reflectt-channel/README.md +96 -0
- package/plugins/reflectt-channel/index.ts +789 -0
- package/plugins/reflectt-channel/openclaw.plugin.json +23 -0
- package/plugins/reflectt-channel/package.json +23 -0
- package/plugins/reflectt-channel/src/channel.ts +433 -0
- package/plugins/reflectt-channel/src/types.ts +29 -0
- package/public/avatars/echo.png +0 -0
- package/public/avatars/harmony.png +0 -0
- package/public/avatars/kai.png +0 -0
- package/public/avatars/link.png +0 -0
- package/public/avatars/pixel.png +0 -0
- package/public/avatars/rhythm.png +0 -0
- package/public/avatars/ryan.png +0 -0
- package/public/avatars/sage.png +0 -0
- package/public/avatars/scout.png +0 -0
- package/public/avatars/spark.png +0 -0
- package/public/dashboard-animations.css +381 -0
- package/public/dashboard.js +3479 -0
- package/public/docs.md +1062 -0
- package/public/file-upload-mock.html +1097 -0
- package/public/og-card.png +0 -0
- package/public/ui-kit.html +318 -0
- package/public/widget/feedback.js +194 -0
|
@@ -0,0 +1,875 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) Reflectt AI
|
|
3
|
+
/**
|
|
4
|
+
* Execution Sweeper — Zero-Leak Enforcement
|
|
5
|
+
*
|
|
6
|
+
* Periodically scans for:
|
|
7
|
+
* 1. Stale validating tasks (no reviewer activity within SLA)
|
|
8
|
+
* 2. Open PRs not linked to active tasks (orphan PRs)
|
|
9
|
+
* 3. Task/PR state drift (merged PR but task still validating)
|
|
10
|
+
*
|
|
11
|
+
* Escalates via chat messages when thresholds are breached.
|
|
12
|
+
* Provides drift report endpoint for full visibility.
|
|
13
|
+
*/
|
|
14
|
+
import { taskManager } from './tasks.js';
|
|
15
|
+
import { chatManager } from './chat.js';
|
|
16
|
+
import { execSync } from 'child_process';
|
|
17
|
+
import { processAutoMerge, generateRemediation } from './prAutoMerge.js';
|
|
18
|
+
import { preflightCheck } from './alert-preflight.js';
|
|
19
|
+
/**
|
|
20
|
+
* Send an alert message through chatManager with preflight guard.
|
|
21
|
+
* If preflight suppresses the alert (in enforce mode), the message is not sent.
|
|
22
|
+
* In canary mode, it logs but still sends.
|
|
23
|
+
*/
|
|
24
|
+
async function sendAlertWithPreflight(msg, preflight) {
|
|
25
|
+
const result = preflightCheck({
|
|
26
|
+
...preflight,
|
|
27
|
+
content: msg.content,
|
|
28
|
+
channel: msg.channel,
|
|
29
|
+
});
|
|
30
|
+
if (!result.proceed) {
|
|
31
|
+
console.log(`[Sweeper] Alert suppressed by preflight: ${result.reason} (key: ${result.idempotentKey})`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
await chatManager.sendMessage(msg);
|
|
35
|
+
}
|
|
36
|
+
import { msToMinutes, formatDuration } from './format-duration.js';
|
|
37
|
+
import { suggestReviewer } from './assignment.js';
|
|
38
|
+
import { getDuplicateClosureCanonicalRefError } from './duplicateClosureGuard.js';
|
|
39
|
+
/**
|
|
40
|
+
* Check live PR state via `gh` CLI. Returns 'unknown' if gh is unavailable.
|
|
41
|
+
* Results are cached for the duration of one sweep cycle to avoid rate limits.
|
|
42
|
+
*/
|
|
43
|
+
const prStateCache = new Map();
|
|
44
|
+
const PR_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
45
|
+
export function checkLivePrState(prUrl) {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const cached = prStateCache.get(prUrl);
|
|
48
|
+
if (cached && (now - cached.cachedAt) < PR_CACHE_TTL_MS) {
|
|
49
|
+
return cached.state;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
// Extract owner/repo and PR number from URL
|
|
53
|
+
const match = prUrl.match(/github\.com\/([^/]+\/[^/]+)\/pull\/(\d+)/);
|
|
54
|
+
if (!match)
|
|
55
|
+
return { state: 'unknown', error: 'Invalid PR URL format' };
|
|
56
|
+
const [, repo, prNumber] = match;
|
|
57
|
+
const raw = execSync(`gh pr view ${prNumber} --repo ${repo} --json state --jq .state`, { timeout: 10_000, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim().toUpperCase();
|
|
58
|
+
let state = 'unknown';
|
|
59
|
+
if (raw === 'OPEN')
|
|
60
|
+
state = 'open';
|
|
61
|
+
else if (raw === 'MERGED')
|
|
62
|
+
state = 'merged';
|
|
63
|
+
else if (raw === 'CLOSED')
|
|
64
|
+
state = 'closed';
|
|
65
|
+
const result = { state };
|
|
66
|
+
prStateCache.set(prUrl, { state: result, cachedAt: now });
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
const result = { state: 'unknown', error: String(err) };
|
|
71
|
+
prStateCache.set(prUrl, { state: result, cachedAt: now });
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Clear PR state cache (for testing) */
|
|
76
|
+
export function _clearPrStateCache() {
|
|
77
|
+
prStateCache.clear();
|
|
78
|
+
}
|
|
79
|
+
// ── Configuration ──────────────────────────────────────────────────────────
|
|
80
|
+
/** How often the sweeper runs (ms) */
|
|
81
|
+
const SWEEP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
|
|
82
|
+
/** Validating SLA: escalate after this many ms without reviewer activity */
|
|
83
|
+
const VALIDATING_SLA_MS = 2 * 60 * 60 * 1000; // 2 hours (was 30m — too aggressive for async AI review)
|
|
84
|
+
/** Critical SLA: second escalation tier */
|
|
85
|
+
const VALIDATING_CRITICAL_MS = 8 * 60 * 60 * 1000; // 8 hours (was 60m — reviewers aren't real-time)
|
|
86
|
+
/** Auto-reassign reviewer after this much time without reviewer activity */
|
|
87
|
+
const VALIDATING_REASSIGN_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
88
|
+
/** PR age threshold: flag PRs linked to non-active tasks older than this */
|
|
89
|
+
const ORPHAN_PR_THRESHOLD_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
90
|
+
/** Re-escalation cooldown: don't re-alert the same task within this window */
|
|
91
|
+
const ESCALATION_COOLDOWN_MS = 4 * 60 * 60 * 1000; // 4 hours
|
|
92
|
+
/** Artifact grace period: validating tasks without artifacts after this are auto-rejected */
|
|
93
|
+
const ARTIFACT_GRACE_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
94
|
+
/**
|
|
95
|
+
* @deprecated Use formatDuration(ms) from format-duration.ts instead.
|
|
96
|
+
* Kept temporarily for reference; all call sites now use formatDuration().
|
|
97
|
+
*/
|
|
98
|
+
/** Max escalation count per task before silencing */
|
|
99
|
+
const MAX_ESCALATION_COUNT = 3;
|
|
100
|
+
/** Track which tasks we've already escalated (avoid spam) — in-memory cache */
|
|
101
|
+
const escalated = new Map();
|
|
102
|
+
/** Track which orphan PRs we've already flagged */
|
|
103
|
+
const flaggedOrphanPRs = new Set();
|
|
104
|
+
/** Track sweep stats for the /execution-health endpoint */
|
|
105
|
+
let lastSweepAt = 0;
|
|
106
|
+
let lastSweepResults = null;
|
|
107
|
+
/** Dry-run log for 24h evidence capture */
|
|
108
|
+
const dryRunLog = [];
|
|
109
|
+
const DRY_RUN_LOG_MAX = 500;
|
|
110
|
+
function logDryRun(event, detail) {
|
|
111
|
+
dryRunLog.push({ timestamp: Date.now(), event, detail });
|
|
112
|
+
if (dryRunLog.length > DRY_RUN_LOG_MAX) {
|
|
113
|
+
dryRunLog.splice(0, dryRunLog.length - DRY_RUN_LOG_MAX);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ── Auto-close eligibility ─────────────────────────────────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* Determines if a validating task can be auto-closed (no manual reviewer action needed).
|
|
119
|
+
*
|
|
120
|
+
* Conditions (ALL must be true):
|
|
121
|
+
* 1. metadata.reconciled === true (task was created from insight reconciliation)
|
|
122
|
+
* 2. Review is approved (reviewer_approved=true OR review_state='approved')
|
|
123
|
+
* 3. No code delta is required (no pr_url, or PR is already merged)
|
|
124
|
+
*/
|
|
125
|
+
export function isAutoClosable(task, meta) {
|
|
126
|
+
// Must be reconciled
|
|
127
|
+
if (!meta.reconciled)
|
|
128
|
+
return false;
|
|
129
|
+
// Must have reviewer approval or approved review state
|
|
130
|
+
const reviewApproved = meta.reviewer_approved === true || meta.review_state === 'approved';
|
|
131
|
+
if (!reviewApproved)
|
|
132
|
+
return false;
|
|
133
|
+
// If there's a PR URL, it must already be merged to auto-close
|
|
134
|
+
const prUrl = extractPrUrl(meta);
|
|
135
|
+
if (prUrl) {
|
|
136
|
+
const prMerged = meta.pr_merged === true || meta.merge_commit;
|
|
137
|
+
if (!prMerged)
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
// ── Core Sweep Logic ───────────────────────────────────────────────────────
|
|
143
|
+
export async function sweepValidatingQueue() {
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
// Snapshot tasks for load-balanced reviewer reassignment decisions
|
|
146
|
+
const tasksForScoring = taskManager.listTasks({}).map(t => ({
|
|
147
|
+
id: t.id,
|
|
148
|
+
title: t.title,
|
|
149
|
+
status: t.status,
|
|
150
|
+
assignee: t.assignee,
|
|
151
|
+
reviewer: t.reviewer,
|
|
152
|
+
tags: t.metadata?.tags,
|
|
153
|
+
metadata: t.metadata,
|
|
154
|
+
}));
|
|
155
|
+
const validating = taskManager.listTasks({ status: 'validating' });
|
|
156
|
+
const doneTasks = taskManager.listTasks({ status: 'done' });
|
|
157
|
+
const doingTasks = taskManager.listTasks({ status: 'doing' });
|
|
158
|
+
const todoTasks = taskManager.listTasks({ status: 'todo' });
|
|
159
|
+
const totalScanned = validating.length + doneTasks.length + doingTasks.length + todoTasks.length;
|
|
160
|
+
const violations = [];
|
|
161
|
+
// ── Auto-close reconciled validating tasks ────────────────────────────
|
|
162
|
+
// Reconciled tasks (metadata.reconciled=true) with an approved review
|
|
163
|
+
// or evidence packet and no code delta required can be auto-closed
|
|
164
|
+
// to prevent SLA noise.
|
|
165
|
+
const autoClosedIds = new Set();
|
|
166
|
+
for (const task of validating) {
|
|
167
|
+
const meta = (task.metadata || {});
|
|
168
|
+
if (isAutoClosable(task, meta)) {
|
|
169
|
+
const dupeErr = getDuplicateClosureCanonicalRefError(meta);
|
|
170
|
+
if (dupeErr) {
|
|
171
|
+
// Don't auto-close into a churny N/A duplicate packet — requeue for canonical refs.
|
|
172
|
+
try {
|
|
173
|
+
await taskManager.updateTask(task.id, {
|
|
174
|
+
status: 'todo',
|
|
175
|
+
metadata: {
|
|
176
|
+
...meta,
|
|
177
|
+
auto_close_blocked: true,
|
|
178
|
+
auto_close_blocked_at: now,
|
|
179
|
+
auto_close_blocked_reason: dupeErr,
|
|
180
|
+
review_state: 'needs_author',
|
|
181
|
+
reviewer_approved: undefined,
|
|
182
|
+
reviewer_decision: undefined,
|
|
183
|
+
reviewer_notes: undefined,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
chatManager.sendMessage({
|
|
187
|
+
from: 'system',
|
|
188
|
+
channel: 'task-notifications',
|
|
189
|
+
content: `⚠️ Auto-close blocked for duplicate closure without canonical refs: ${task.id}. Requeued to todo. @${task.assignee || 'unassigned'} please set duplicate_of + canonical_pr + canonical_commit.`,
|
|
190
|
+
}).catch(() => { });
|
|
191
|
+
}
|
|
192
|
+
catch { }
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
await taskManager.updateTask(task.id, {
|
|
197
|
+
status: 'done',
|
|
198
|
+
metadata: {
|
|
199
|
+
...meta,
|
|
200
|
+
auto_closed: true,
|
|
201
|
+
auto_closed_at: now,
|
|
202
|
+
auto_close_reason: 'reconciled_no_code_delta',
|
|
203
|
+
source_insight: meta.source_insight || meta.insight_id || null,
|
|
204
|
+
source_reflection: meta.source_reflection || null,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
autoClosedIds.add(task.id);
|
|
208
|
+
escalated.delete(task.id);
|
|
209
|
+
logDryRun('auto_closed_reconciled', `${task.id} — reconciled + approved, no code delta required`);
|
|
210
|
+
// Notify in chat
|
|
211
|
+
chatManager.sendMessage({
|
|
212
|
+
from: 'system',
|
|
213
|
+
channel: 'task-notifications',
|
|
214
|
+
content: `✅ Auto-closed reconciled task "${task.title}" (${task.id}) — no code delta required. Insight: ${meta.source_insight || meta.insight_id || 'N/A'}`,
|
|
215
|
+
}).catch(() => { });
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
logDryRun('auto_close_failed', `${task.id} — ${String(err)}`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Filter out auto-closed tasks from further checks
|
|
224
|
+
const remainingValidating = validating.filter(t => !autoClosedIds.has(t.id));
|
|
225
|
+
// ── Artifact grace period: auto-reject tasks missing artifacts after 24h ──
|
|
226
|
+
const artifactRejectedIds = new Set();
|
|
227
|
+
for (const task of remainingValidating) {
|
|
228
|
+
const meta = (task.metadata || {});
|
|
229
|
+
const enteredAt = meta.entered_validating_at || task.updatedAt;
|
|
230
|
+
const ageInValidating = now - enteredAt;
|
|
231
|
+
if (ageInValidating >= ARTIFACT_GRACE_MS && !hasRequiredArtifacts(meta)) {
|
|
232
|
+
try {
|
|
233
|
+
taskManager.updateTask(task.id, {
|
|
234
|
+
status: 'todo',
|
|
235
|
+
metadata: {
|
|
236
|
+
...meta,
|
|
237
|
+
artifact_rejected: true,
|
|
238
|
+
artifact_rejected_at: now,
|
|
239
|
+
artifact_reject_reason: 'Missing required artifacts (PR or qa_bundle) after 24h grace period',
|
|
240
|
+
review_state: undefined,
|
|
241
|
+
reviewer_approved: undefined,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
artifactRejectedIds.add(task.id);
|
|
245
|
+
escalated.delete(task.id);
|
|
246
|
+
logDryRun('artifact_rejected', `${task.id} — no artifacts after ${msToMinutes(ageInValidating)}m in validating`);
|
|
247
|
+
chatManager.sendMessage({
|
|
248
|
+
from: 'system',
|
|
249
|
+
channel: 'task-notifications',
|
|
250
|
+
content: `⚠️ Auto-rejected "${task.title}" (${task.id}) back to todo — missing required artifacts (PR or qa_bundle) after 24h in validating. @${task.assignee || 'unassigned'} please add artifacts and resubmit.`,
|
|
251
|
+
}).catch(() => { });
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
logDryRun('artifact_reject_failed', `${task.id} — ${String(err)}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Filter out artifact-rejected tasks from SLA checks
|
|
259
|
+
const slaValidating = remainingValidating.filter(t => !artifactRejectedIds.has(t.id));
|
|
260
|
+
for (const task of slaValidating) {
|
|
261
|
+
const meta = (task.metadata || {});
|
|
262
|
+
// Auto-close approved tasks still stuck in validating (drift repair)
|
|
263
|
+
// This catches chat approvals or any path that set reviewer_approved
|
|
264
|
+
// without transitioning status to done.
|
|
265
|
+
const reviewState = meta.review_state;
|
|
266
|
+
const reviewerApproved = meta.reviewer_approved === true;
|
|
267
|
+
if (reviewState === 'approved' || reviewerApproved) {
|
|
268
|
+
const dupeErr = getDuplicateClosureCanonicalRefError(meta);
|
|
269
|
+
if (dupeErr) {
|
|
270
|
+
try {
|
|
271
|
+
await taskManager.updateTask(task.id, {
|
|
272
|
+
status: 'todo',
|
|
273
|
+
metadata: {
|
|
274
|
+
...meta,
|
|
275
|
+
auto_close_blocked: true,
|
|
276
|
+
auto_close_blocked_at: now,
|
|
277
|
+
auto_close_blocked_reason: dupeErr,
|
|
278
|
+
review_state: 'needs_author',
|
|
279
|
+
reviewer_approved: undefined,
|
|
280
|
+
reviewer_decision: undefined,
|
|
281
|
+
reviewer_notes: undefined,
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
chatManager.sendMessage({
|
|
285
|
+
from: 'system',
|
|
286
|
+
channel: 'task-notifications',
|
|
287
|
+
content: `⚠️ Drift-repair auto-close blocked for duplicate closure without canonical refs: ${task.id}. Requeued to todo.`,
|
|
288
|
+
}).catch(() => { });
|
|
289
|
+
}
|
|
290
|
+
catch { }
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
await taskManager.updateTask(task.id, {
|
|
295
|
+
status: 'done',
|
|
296
|
+
metadata: {
|
|
297
|
+
...meta,
|
|
298
|
+
auto_closed: true,
|
|
299
|
+
auto_closed_at: now,
|
|
300
|
+
auto_close_reason: 'sweeper_drift_repair_approved',
|
|
301
|
+
completed_at: now,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
autoClosedIds.add(task.id);
|
|
305
|
+
escalated.delete(task.id);
|
|
306
|
+
logDryRun('drift_repair_auto_closed', `${task.id} — approved but stuck in validating, auto-closed`);
|
|
307
|
+
chatManager.sendMessage({
|
|
308
|
+
from: 'system',
|
|
309
|
+
channel: 'task-notifications',
|
|
310
|
+
content: `✅ Drift repair: auto-closed "${task.title}" (${task.id}) — was approved but stuck in validating. reviewer: @${task.reviewer || 'unknown'}`,
|
|
311
|
+
}).catch(() => { });
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
logDryRun('drift_repair_auto_close_failed', `${task.id} — ${String(err)}`);
|
|
315
|
+
}
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const enteredAt = meta.entered_validating_at || task.updatedAt;
|
|
319
|
+
const lastActivity = meta.review_last_activity_at || enteredAt;
|
|
320
|
+
const ageSinceActivity = now - lastActivity;
|
|
321
|
+
const ageMinutes = msToMinutes(ageSinceActivity);
|
|
322
|
+
// ── Persistent escalation state (survives restarts) ──────────────
|
|
323
|
+
// Read escalation history from task metadata, not just in-memory map
|
|
324
|
+
const persistedLevel = meta.sweeper_escalation_level;
|
|
325
|
+
const persistedAt = meta.sweeper_escalated_at;
|
|
326
|
+
const persistedCount = meta.sweeper_escalation_count || 0;
|
|
327
|
+
// Rehydrate in-memory map from metadata on first encounter
|
|
328
|
+
const prev = escalated.get(task.id);
|
|
329
|
+
if (!prev && persistedLevel && persistedAt) {
|
|
330
|
+
escalated.set(task.id, {
|
|
331
|
+
level: persistedLevel,
|
|
332
|
+
at: persistedAt,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
const effective = escalated.get(task.id);
|
|
336
|
+
// Auto-reassign reviewer when a task sits in validating too long without activity.
|
|
337
|
+
// This mitigates "stuck in validating" states when the original reviewer is offline.
|
|
338
|
+
if (ageSinceActivity >= VALIDATING_REASSIGN_MS && meta.reviewer_auto_reassigned !== true) {
|
|
339
|
+
try {
|
|
340
|
+
const suggestion = suggestReviewer({
|
|
341
|
+
title: task.title,
|
|
342
|
+
assignee: task.assignee,
|
|
343
|
+
tags: meta.tags,
|
|
344
|
+
done_criteria: task.done_criteria,
|
|
345
|
+
}, tasksForScoring);
|
|
346
|
+
const currentReviewer = (task.reviewer || '').trim();
|
|
347
|
+
const nextReviewer = (suggestion.suggested || '').trim();
|
|
348
|
+
if (currentReviewer && nextReviewer && nextReviewer.toLowerCase() !== currentReviewer.toLowerCase()) {
|
|
349
|
+
await taskManager.updateTask(task.id, {
|
|
350
|
+
reviewer: nextReviewer,
|
|
351
|
+
metadata: {
|
|
352
|
+
...meta,
|
|
353
|
+
review_state: 'queued',
|
|
354
|
+
review_last_activity_at: now,
|
|
355
|
+
reviewer_previous: currentReviewer,
|
|
356
|
+
reviewer_auto_reassigned: true,
|
|
357
|
+
reviewer_auto_reassigned_at: now,
|
|
358
|
+
reviewer_reassign_reason: 'validating_no_reviewer_activity',
|
|
359
|
+
reviewer_scores: suggestion.scores.slice(0, 3),
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
// Mutate local copy so subsequent alerts in this sweep use the new reviewer.
|
|
363
|
+
task.reviewer = nextReviewer;
|
|
364
|
+
logDryRun('reviewer_auto_reassigned', `${task.id} — ${currentReviewer} -> ${nextReviewer} after ${ageMinutes}m without reviewer activity`);
|
|
365
|
+
chatManager.sendMessage({
|
|
366
|
+
from: 'system',
|
|
367
|
+
channel: 'task-notifications',
|
|
368
|
+
content: `🔁 Auto-reassigned reviewer for "${task.title}" (${task.id}) after ${ageMinutes}m without reviewer activity: @${currentReviewer} → @${nextReviewer}.`,
|
|
369
|
+
}).catch(() => { });
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
logDryRun('reviewer_auto_reassign_failed', `${task.id} — ${String(err)}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
// Skip if max escalations reached (silenced)
|
|
377
|
+
if (persistedCount >= MAX_ESCALATION_COUNT) {
|
|
378
|
+
logDryRun('escalation_silenced', `${task.id} — ${persistedCount} escalations, max reached`);
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
// Skip if within cooldown window
|
|
382
|
+
const lastEscalatedAt = effective?.at || persistedAt || 0;
|
|
383
|
+
if (lastEscalatedAt && (now - lastEscalatedAt) < ESCALATION_COOLDOWN_MS) {
|
|
384
|
+
continue; // Still in cooldown
|
|
385
|
+
}
|
|
386
|
+
if (ageSinceActivity >= VALIDATING_CRITICAL_MS && effective?.level !== 'critical') {
|
|
387
|
+
const prUrl = extractPrUrl(meta);
|
|
388
|
+
const newCount = persistedCount + 1;
|
|
389
|
+
violations.push({
|
|
390
|
+
taskId: task.id,
|
|
391
|
+
title: task.title,
|
|
392
|
+
assignee: task.assignee,
|
|
393
|
+
reviewer: task.reviewer,
|
|
394
|
+
type: 'validating_critical',
|
|
395
|
+
age_minutes: ageMinutes,
|
|
396
|
+
message: `🚨 CRITICAL: "${task.title}" (${task.id}) stuck in validating for ${formatDuration(ageSinceActivity)}. @${task.reviewer || 'unassigned'} please review. @${task.assignee || 'unassigned'} — your PR is blocked.`,
|
|
397
|
+
remediation: generateRemediation({ taskId: task.id, issue: 'stale_validating', prUrl: prUrl || undefined, meta }),
|
|
398
|
+
});
|
|
399
|
+
escalated.set(task.id, { level: 'critical', at: now });
|
|
400
|
+
// Persist to task metadata so it survives restarts (lightweight, bypasses lifecycle gates)
|
|
401
|
+
taskManager.patchTaskMetadata(task.id, {
|
|
402
|
+
sweeper_escalation_level: 'critical',
|
|
403
|
+
sweeper_escalated_at: now,
|
|
404
|
+
sweeper_escalation_count: newCount,
|
|
405
|
+
});
|
|
406
|
+
logDryRun('validating_critical', `${task.id} — ${ageMinutes}m — reviewer:${task.reviewer} assignee:${task.assignee} count:${newCount}`);
|
|
407
|
+
}
|
|
408
|
+
else if (ageSinceActivity >= VALIDATING_SLA_MS && !effective) {
|
|
409
|
+
const prUrl = extractPrUrl(meta);
|
|
410
|
+
const newCount = persistedCount + 1;
|
|
411
|
+
violations.push({
|
|
412
|
+
taskId: task.id,
|
|
413
|
+
title: task.title,
|
|
414
|
+
assignee: task.assignee,
|
|
415
|
+
reviewer: task.reviewer,
|
|
416
|
+
type: 'validating_sla',
|
|
417
|
+
age_minutes: ageMinutes,
|
|
418
|
+
message: `⚠️ SLA breach: "${task.title}" (${task.id}) in validating ${formatDuration(ageSinceActivity)}. @${task.reviewer || 'unassigned'} — review needed. @${task.assignee || 'unassigned'} — ping if blocked.`,
|
|
419
|
+
remediation: generateRemediation({ taskId: task.id, issue: 'stale_validating', prUrl: prUrl || undefined, meta }),
|
|
420
|
+
});
|
|
421
|
+
escalated.set(task.id, { level: 'warning', at: now });
|
|
422
|
+
// Persist to task metadata so it survives restarts (lightweight, bypasses lifecycle gates)
|
|
423
|
+
taskManager.patchTaskMetadata(task.id, {
|
|
424
|
+
sweeper_escalation_level: 'warning',
|
|
425
|
+
sweeper_escalated_at: now,
|
|
426
|
+
sweeper_escalation_count: newCount,
|
|
427
|
+
});
|
|
428
|
+
logDryRun('validating_sla', `${task.id} — ${ageMinutes}m — reviewer:${task.reviewer} assignee:${task.assignee} count:${newCount}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
// Clean up escalation tracking for tasks no longer validating
|
|
432
|
+
for (const [key] of escalated) {
|
|
433
|
+
// drift: prefix holds the real task ID — strip before resolving
|
|
434
|
+
const realTaskId = key.startsWith('drift:') ? key.slice(6) : key;
|
|
435
|
+
const lookup = taskManager.resolveTaskId(realTaskId);
|
|
436
|
+
if (!lookup.task || lookup.task.status !== 'validating') {
|
|
437
|
+
escalated.delete(key);
|
|
438
|
+
// Also clear persisted sweeper metadata when task leaves validating
|
|
439
|
+
if (lookup.task) {
|
|
440
|
+
const meta = (lookup.task.metadata || {});
|
|
441
|
+
if (meta.sweeper_escalation_level) {
|
|
442
|
+
taskManager.patchTaskMetadata(realTaskId, {
|
|
443
|
+
sweeper_escalation_level: undefined,
|
|
444
|
+
sweeper_escalated_at: undefined,
|
|
445
|
+
sweeper_escalation_count: undefined,
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
logDryRun('escalation_cleared', `${key} — no longer validating`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
// ── Orphan PR detection ──────────────────────────────────────────────
|
|
453
|
+
// Scan all tasks with PR URLs where the task is done/cancelled but the PR
|
|
454
|
+
// was linked — these represent potential orphan open PRs
|
|
455
|
+
const cancelledTasks = taskManager.listTasks({ status: 'cancelled' });
|
|
456
|
+
const doneAndCancelled = [...doneTasks, ...cancelledTasks];
|
|
457
|
+
for (const task of doneAndCancelled) {
|
|
458
|
+
const meta = (task.metadata || {});
|
|
459
|
+
const prUrl = extractPrUrl(meta);
|
|
460
|
+
if (!prUrl || flaggedOrphanPRs.has(prUrl))
|
|
461
|
+
continue;
|
|
462
|
+
// Check if this PR is also referenced by an active task — if so, it's not orphan
|
|
463
|
+
const activeTasks = [...doingTasks, ...validating, ...todoTasks];
|
|
464
|
+
const activeRef = activeTasks.find((t) => t.id !== task.id &&
|
|
465
|
+
extractPrUrl((t.metadata || {})) === prUrl);
|
|
466
|
+
if (activeRef)
|
|
467
|
+
continue;
|
|
468
|
+
// PR on a done task with no active task referencing it
|
|
469
|
+
// Check metadata flags that indicate the PR was merged/resolved
|
|
470
|
+
const prMerged = !!(meta.pr_merged);
|
|
471
|
+
const reviewerApproved = !!(meta.reviewer_approved);
|
|
472
|
+
const taskDone = task.status === 'done';
|
|
473
|
+
// If metadata says merged, skip
|
|
474
|
+
if (prMerged)
|
|
475
|
+
continue;
|
|
476
|
+
// Skip live PR checks during periodic sweep — execSync blocks the event loop.
|
|
477
|
+
// Orphan PR detection relies on metadata flags only; live checks available via /drift-report.
|
|
478
|
+
if (prMerged || reviewerApproved)
|
|
479
|
+
continue;
|
|
480
|
+
const completedAge = now - task.updatedAt;
|
|
481
|
+
if (completedAge >= ORPHAN_PR_THRESHOLD_MS) {
|
|
482
|
+
const assigneeMention = task.assignee ? `@${task.assignee}` : '@unassigned';
|
|
483
|
+
const reviewerMention = task.reviewer ? `@${task.reviewer}` : '@unassigned';
|
|
484
|
+
violations.push({
|
|
485
|
+
taskId: task.id,
|
|
486
|
+
title: task.title,
|
|
487
|
+
assignee: task.assignee,
|
|
488
|
+
reviewer: task.reviewer,
|
|
489
|
+
type: 'orphan_pr',
|
|
490
|
+
age_minutes: msToMinutes(completedAge),
|
|
491
|
+
message: `🔍 Orphan PR detected: ${prUrl} linked to done task "${task.title}" (${task.id}). PR may still be open — ${assigneeMention} close or merge it. ${reviewerMention} — confirm status.`,
|
|
492
|
+
remediation: generateRemediation({ taskId: task.id, issue: 'orphan_pr', prUrl }),
|
|
493
|
+
});
|
|
494
|
+
flaggedOrphanPRs.add(prUrl);
|
|
495
|
+
logDryRun('orphan_pr', `${prUrl} on ${task.id} — task done ${msToMinutes(completedAge)}m ago`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
// Also check validating tasks for PR drift (merged but not advanced)
|
|
499
|
+
for (const task of validating) {
|
|
500
|
+
const meta = (task.metadata || {});
|
|
501
|
+
if (meta.pr_merged && task.status === 'validating') {
|
|
502
|
+
const mergedAt = meta.pr_merged_at || task.updatedAt;
|
|
503
|
+
const driftAge = now - mergedAt;
|
|
504
|
+
if (driftAge >= ORPHAN_PR_THRESHOLD_MS && !escalated.has(`drift:${task.id}`)) {
|
|
505
|
+
violations.push({
|
|
506
|
+
taskId: task.id,
|
|
507
|
+
title: task.title,
|
|
508
|
+
assignee: task.assignee,
|
|
509
|
+
reviewer: task.reviewer,
|
|
510
|
+
type: 'pr_drift',
|
|
511
|
+
age_minutes: msToMinutes(driftAge),
|
|
512
|
+
message: `📦 PR merged ${msToMinutes(driftAge)}m ago but "${task.title}" (${task.id}) still in validating. @${task.reviewer || 'unassigned'} — approve or close. @${task.assignee || 'unassigned'} — ping if needed.`,
|
|
513
|
+
remediation: generateRemediation({ taskId: task.id, issue: 'pr_merged_not_closed', prUrl: extractPrUrl(meta) || undefined, meta }),
|
|
514
|
+
});
|
|
515
|
+
escalated.set(`drift:${task.id}`, { level: 'warning', at: now });
|
|
516
|
+
logDryRun('pr_drift', `${task.id} — PR merged ${msToMinutes(driftAge)}m ago, still validating`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// ── Auto-merge processing ─────────────────────────────────────────────
|
|
521
|
+
// Attempt to auto-merge green+approved PRs and auto-close tasks
|
|
522
|
+
try {
|
|
523
|
+
const autoMergeResult = processAutoMerge([...validating, ...doingTasks, ...todoTasks]);
|
|
524
|
+
if (autoMergeResult.mergeAttempts > 0 || autoMergeResult.autoCloses > 0) {
|
|
525
|
+
logDryRun('auto_merge', `attempts=${autoMergeResult.mergeAttempts} successes=${autoMergeResult.mergeSuccesses} autoCloses=${autoMergeResult.autoCloses} skipped=${autoMergeResult.skipped}`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
catch (err) {
|
|
529
|
+
console.error('[Sweeper] Auto-merge processing failed:', err);
|
|
530
|
+
logDryRun('auto_merge_error', String(err));
|
|
531
|
+
}
|
|
532
|
+
const result = {
|
|
533
|
+
timestamp: now,
|
|
534
|
+
violations,
|
|
535
|
+
tasksScanned: totalScanned,
|
|
536
|
+
validatingCount: validating.length,
|
|
537
|
+
autoClosedCount: autoClosedIds.size,
|
|
538
|
+
artifactRejectedCount: artifactRejectedIds.size,
|
|
539
|
+
};
|
|
540
|
+
lastSweepAt = now;
|
|
541
|
+
lastSweepResults = result;
|
|
542
|
+
logDryRun('sweep_complete', `scanned=${totalScanned} validating=${validating.length} violations=${violations.length}`);
|
|
543
|
+
return result;
|
|
544
|
+
}
|
|
545
|
+
// ── Helper: Extract PR URL from task metadata ──────────────────────────────
|
|
546
|
+
function isValidPrUrl(url) {
|
|
547
|
+
// Filter out placeholder/invalid PR URLs (e.g. /pull/0, /pull/00)
|
|
548
|
+
const match = url.match(/\/pull\/(\d+)/);
|
|
549
|
+
if (!match)
|
|
550
|
+
return false;
|
|
551
|
+
const prNumber = parseInt(match[1], 10);
|
|
552
|
+
return prNumber > 0;
|
|
553
|
+
}
|
|
554
|
+
function extractPrUrl(meta) {
|
|
555
|
+
// Skip extraction entirely for doc-only or config-only tasks
|
|
556
|
+
const reviewHandoff = meta.review_handoff;
|
|
557
|
+
if (reviewHandoff?.doc_only || reviewHandoff?.config_only)
|
|
558
|
+
return null;
|
|
559
|
+
// Check multiple locations where PR URLs are stored
|
|
560
|
+
const candidates = [];
|
|
561
|
+
if (meta.pr_url && typeof meta.pr_url === 'string')
|
|
562
|
+
candidates.push(meta.pr_url);
|
|
563
|
+
const qaBundle = meta.qa_bundle;
|
|
564
|
+
if (qaBundle?.pr_link && typeof qaBundle.pr_link === 'string')
|
|
565
|
+
candidates.push(qaBundle.pr_link);
|
|
566
|
+
if (reviewHandoff?.pr_url && typeof reviewHandoff.pr_url === 'string')
|
|
567
|
+
candidates.push(reviewHandoff.pr_url);
|
|
568
|
+
const artifacts = meta.artifacts;
|
|
569
|
+
if (artifacts?.length) {
|
|
570
|
+
const ghPr = artifacts.find(a => typeof a === 'string' && a.includes('github.com') && a.includes('/pull/'));
|
|
571
|
+
if (ghPr)
|
|
572
|
+
candidates.push(ghPr);
|
|
573
|
+
}
|
|
574
|
+
// Return first valid PR URL, filtering out placeholders like /pull/0
|
|
575
|
+
for (const url of candidates) {
|
|
576
|
+
if (isValidPrUrl(url))
|
|
577
|
+
return url;
|
|
578
|
+
}
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
// ── Artifact Check ─────────────────────────────────────────────────────────
|
|
582
|
+
/**
|
|
583
|
+
* Check if a validating task has required artifacts (PR URL or qa_bundle).
|
|
584
|
+
* Doc-only and config-only tasks are exempt (they use review_handoff).
|
|
585
|
+
*/
|
|
586
|
+
export function hasRequiredArtifacts(meta) {
|
|
587
|
+
// Doc-only / config-only tasks don't need code artifacts
|
|
588
|
+
const reviewHandoff = meta.review_handoff;
|
|
589
|
+
if (reviewHandoff?.doc_only || reviewHandoff?.config_only)
|
|
590
|
+
return true;
|
|
591
|
+
// Reconciled tasks (no code delta) are exempt
|
|
592
|
+
if (meta.reconciled === true)
|
|
593
|
+
return true;
|
|
594
|
+
// Check for PR URL in any known location
|
|
595
|
+
if (extractPrUrl(meta))
|
|
596
|
+
return true;
|
|
597
|
+
// Check for qa_bundle with meaningful evidence (not just review_packet structure)
|
|
598
|
+
const qaBundle = meta.qa_bundle;
|
|
599
|
+
if (qaBundle) {
|
|
600
|
+
// pr_link in qa_bundle counts as evidence
|
|
601
|
+
if (qaBundle.pr_link && typeof qaBundle.pr_link === 'string' && isValidPrUrl(qaBundle.pr_link))
|
|
602
|
+
return true;
|
|
603
|
+
// test results or deployment evidence count
|
|
604
|
+
if (qaBundle.test_results || qaBundle.deployment_url)
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
// Check artifacts array for any meaningful entry
|
|
608
|
+
const artifacts = meta.artifacts;
|
|
609
|
+
if (artifacts?.some(a => typeof a === 'string' && a.length > 0 && !a.startsWith('duplicate:')))
|
|
610
|
+
return true;
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
// ── Escalation ─────────────────────────────────────────────────────────────
|
|
614
|
+
async function escalateViolations(violations) {
|
|
615
|
+
if (violations.length === 0)
|
|
616
|
+
return;
|
|
617
|
+
// ── Batch violations into a single summary message ─────────────────
|
|
618
|
+
// Instead of spamming one message per violation, group them by type
|
|
619
|
+
// and post a single digest. Reduces noise from N messages to 1.
|
|
620
|
+
const critical = violations.filter(v => v.type === 'validating_critical');
|
|
621
|
+
const warnings = violations.filter(v => v.type === 'validating_sla');
|
|
622
|
+
const prIssues = violations.filter(v => v.type === 'orphan_pr' || v.type === 'pr_drift');
|
|
623
|
+
const lines = [`🔍 **Sweeper Digest** — ${violations.length} issue(s) found`];
|
|
624
|
+
if (critical.length > 0) {
|
|
625
|
+
lines.push('');
|
|
626
|
+
lines.push(`🚨 **Critical** (${critical.length}):`);
|
|
627
|
+
for (const v of critical) {
|
|
628
|
+
lines.push(` • ${v.title} (${v.taskId}) — ${v.age_minutes}m, reviewer: @${v.reviewer || 'unassigned'}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
if (warnings.length > 0) {
|
|
632
|
+
lines.push('');
|
|
633
|
+
lines.push(`⚠️ **SLA Warning** (${warnings.length}):`);
|
|
634
|
+
for (const v of warnings) {
|
|
635
|
+
lines.push(` • ${v.title} (${v.taskId}) — ${v.age_minutes}m, reviewer: @${v.reviewer || 'unassigned'}`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
if (prIssues.length > 0) {
|
|
639
|
+
lines.push('');
|
|
640
|
+
lines.push(`📦 **PR Issues** (${prIssues.length}):`);
|
|
641
|
+
for (const v of prIssues) {
|
|
642
|
+
lines.push(` • ${v.title} (${v.taskId}) — ${v.age_minutes}m`);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
try {
|
|
646
|
+
// Use first violation's taskId for preflight; digest is a summary alert
|
|
647
|
+
const firstTaskId = violations[0]?.taskId || 'unknown';
|
|
648
|
+
await sendAlertWithPreflight({
|
|
649
|
+
channel: 'general',
|
|
650
|
+
from: 'sweeper',
|
|
651
|
+
content: lines.join('\n'),
|
|
652
|
+
}, {
|
|
653
|
+
taskId: firstTaskId,
|
|
654
|
+
alertType: 'sweeper_digest',
|
|
655
|
+
agentId: violations[0]?.reviewer || violations[0]?.assignee,
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
catch {
|
|
659
|
+
console.warn(`[Sweeper] Could not post escalation digest`);
|
|
660
|
+
}
|
|
661
|
+
console.log(`[Sweeper] Escalated ${violations.length} violation(s) (batched):`, violations.map(v => `${v.type}:${v.taskId}`).join(', '));
|
|
662
|
+
}
|
|
663
|
+
// ── PR-State Drift Detection (webhook-triggered) ──────────────────────────
|
|
664
|
+
/**
|
|
665
|
+
* Check if a task's linked PR has been merged but the task is still in validating.
|
|
666
|
+
* Called externally when PR state changes are detected.
|
|
667
|
+
*/
|
|
668
|
+
export function flagPrDrift(taskId, prState) {
|
|
669
|
+
const lookup = taskManager.resolveTaskId(taskId);
|
|
670
|
+
if (!lookup.task)
|
|
671
|
+
return null;
|
|
672
|
+
const task = lookup.task;
|
|
673
|
+
if (task.status === 'done')
|
|
674
|
+
return null; // Already done, no drift
|
|
675
|
+
if (prState === 'merged' && task.status === 'validating') {
|
|
676
|
+
logDryRun('pr_drift_webhook', `${taskId} — PR merged while task validating`);
|
|
677
|
+
return {
|
|
678
|
+
taskId: task.id,
|
|
679
|
+
title: task.title,
|
|
680
|
+
assignee: task.assignee,
|
|
681
|
+
reviewer: task.reviewer,
|
|
682
|
+
type: 'pr_drift',
|
|
683
|
+
age_minutes: 0,
|
|
684
|
+
message: `📦 PR merged but task "${task.title}" (${task.id}) still in validating. @${task.reviewer || 'unassigned'} — review or auto-advance. @${task.assignee || 'unassigned'} — confirm status.`,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
if (prState === 'closed' && task.status !== 'blocked') {
|
|
688
|
+
logDryRun('pr_closed_webhook', `${taskId} — PR closed unmerged`);
|
|
689
|
+
return {
|
|
690
|
+
taskId: task.id,
|
|
691
|
+
title: task.title,
|
|
692
|
+
assignee: task.assignee,
|
|
693
|
+
reviewer: task.reviewer,
|
|
694
|
+
type: 'pr_drift',
|
|
695
|
+
age_minutes: 0,
|
|
696
|
+
message: `🔴 PR closed (not merged) for task "${task.title}" (${task.id}). @${task.assignee || 'unassigned'} — task should be blocked or have replacement PR. @${task.reviewer || 'unassigned'} — confirm action.`,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
return null;
|
|
700
|
+
}
|
|
701
|
+
// ── Drift Report ───────────────────────────────────────────────────────────
|
|
702
|
+
/**
|
|
703
|
+
* Generate a comprehensive drift report showing all validating tasks,
|
|
704
|
+
* their PR status, and any orphan PRs or state drift.
|
|
705
|
+
*/
|
|
706
|
+
export function generateDriftReport() {
|
|
707
|
+
const now = Date.now();
|
|
708
|
+
const validating = taskManager.listTasks({ status: 'validating' });
|
|
709
|
+
const validatingEntries = [];
|
|
710
|
+
const orphanEntries = [];
|
|
711
|
+
let staleCount = 0;
|
|
712
|
+
let driftCount = 0;
|
|
713
|
+
let cleanCount = 0;
|
|
714
|
+
// Analyze validating tasks
|
|
715
|
+
for (const task of validating) {
|
|
716
|
+
const meta = (task.metadata || {});
|
|
717
|
+
const enteredAt = meta.entered_validating_at || task.updatedAt;
|
|
718
|
+
const lastActivity = meta.review_last_activity_at || enteredAt;
|
|
719
|
+
const ageSinceActivity = now - lastActivity;
|
|
720
|
+
const ageMinutes = msToMinutes(ageSinceActivity);
|
|
721
|
+
const prUrl = extractPrUrl(meta);
|
|
722
|
+
const prMerged = !!(meta.pr_merged);
|
|
723
|
+
let issue = 'clean';
|
|
724
|
+
let detail = 'On track';
|
|
725
|
+
if (prMerged) {
|
|
726
|
+
issue = 'pr_merged_not_closed';
|
|
727
|
+
detail = `PR merged but task still validating (${ageMinutes}m since last activity)`;
|
|
728
|
+
driftCount++;
|
|
729
|
+
}
|
|
730
|
+
else if (!prUrl) {
|
|
731
|
+
issue = 'no_pr_linked';
|
|
732
|
+
detail = `No PR URL found in task metadata — cannot verify PR state`;
|
|
733
|
+
staleCount++;
|
|
734
|
+
}
|
|
735
|
+
else if (ageSinceActivity >= VALIDATING_CRITICAL_MS) {
|
|
736
|
+
issue = 'stale_validating';
|
|
737
|
+
detail = `${ageMinutes}m without reviewer activity (CRITICAL threshold: ${VALIDATING_CRITICAL_MS / 60_000}m)`;
|
|
738
|
+
staleCount++;
|
|
739
|
+
}
|
|
740
|
+
else if (ageSinceActivity >= VALIDATING_SLA_MS) {
|
|
741
|
+
issue = 'stale_validating';
|
|
742
|
+
detail = `${ageMinutes}m without reviewer activity (SLA threshold: ${VALIDATING_SLA_MS / 60_000}m)`;
|
|
743
|
+
staleCount++;
|
|
744
|
+
}
|
|
745
|
+
else {
|
|
746
|
+
cleanCount++;
|
|
747
|
+
}
|
|
748
|
+
validatingEntries.push({
|
|
749
|
+
taskId: task.id,
|
|
750
|
+
title: task.title,
|
|
751
|
+
status: task.status,
|
|
752
|
+
assignee: task.assignee,
|
|
753
|
+
reviewer: task.reviewer,
|
|
754
|
+
age_minutes: ageMinutes,
|
|
755
|
+
prUrl: prUrl || undefined,
|
|
756
|
+
prMerged,
|
|
757
|
+
issue,
|
|
758
|
+
detail,
|
|
759
|
+
remediation: issue !== 'clean' ? generateRemediation({ taskId: task.id, issue, prUrl: prUrl || undefined, meta }) : undefined,
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
// Collect all PR URLs from all tasks to find orphans
|
|
763
|
+
// Query only statuses that matter for orphan detection
|
|
764
|
+
const driftDone = taskManager.listTasks({ status: 'done' });
|
|
765
|
+
const driftDoing = taskManager.listTasks({ status: 'doing' });
|
|
766
|
+
const driftTodo = taskManager.listTasks({ status: 'todo' });
|
|
767
|
+
const driftBlocked = taskManager.listTasks({ status: 'blocked' });
|
|
768
|
+
const driftAll = [...validating, ...driftDone, ...driftDoing, ...driftTodo, ...driftBlocked];
|
|
769
|
+
const prToTasks = new Map();
|
|
770
|
+
for (const task of driftAll) {
|
|
771
|
+
const meta = (task.metadata || {});
|
|
772
|
+
const prUrl = extractPrUrl(meta);
|
|
773
|
+
if (!prUrl || prUrl.includes('workspace://'))
|
|
774
|
+
continue;
|
|
775
|
+
if (!prToTasks.has(prUrl))
|
|
776
|
+
prToTasks.set(prUrl, []);
|
|
777
|
+
prToTasks.get(prUrl).push({ taskId: task.id, status: task.status });
|
|
778
|
+
}
|
|
779
|
+
// Find orphan PRs: PR URLs only linked to done/cancelled tasks (not merged)
|
|
780
|
+
for (const [prUrl, tasks] of prToTasks) {
|
|
781
|
+
const hasActiveTask = tasks.some(t => ['doing', 'validating', 'todo', 'blocked'].includes(t.status));
|
|
782
|
+
if (hasActiveTask)
|
|
783
|
+
continue;
|
|
784
|
+
const doneTasks = tasks.filter(t => t.status === 'done' || t.status === 'cancelled');
|
|
785
|
+
if (doneTasks.length === 0)
|
|
786
|
+
continue;
|
|
787
|
+
// Check if any of the done tasks have pr_merged or reviewer_approved — if so, verify live
|
|
788
|
+
const anyMergedMeta = doneTasks.some(t => {
|
|
789
|
+
const task = driftAll.find(at => at.id === t.taskId);
|
|
790
|
+
if (!task)
|
|
791
|
+
return false;
|
|
792
|
+
const m = (task.metadata || {});
|
|
793
|
+
return !!(m.pr_merged);
|
|
794
|
+
});
|
|
795
|
+
if (anyMergedMeta)
|
|
796
|
+
continue;
|
|
797
|
+
// NOTE: Live PR checks removed — execSync blocks event loop.
|
|
798
|
+
// Rely on metadata flags; checkLivePrState() kept for on-demand use only.
|
|
799
|
+
const oldestDone = driftAll.find(t => t.id === doneTasks[0].taskId);
|
|
800
|
+
orphanEntries.push({
|
|
801
|
+
taskId: doneTasks[0].taskId,
|
|
802
|
+
title: oldestDone?.title || 'Unknown',
|
|
803
|
+
status: 'done',
|
|
804
|
+
assignee: oldestDone?.assignee,
|
|
805
|
+
reviewer: oldestDone?.reviewer,
|
|
806
|
+
age_minutes: oldestDone ? msToMinutes(now - oldestDone.updatedAt) : 0,
|
|
807
|
+
prUrl,
|
|
808
|
+
issue: 'orphan_pr',
|
|
809
|
+
detail: `PR linked to ${doneTasks.length} done task(s) but not marked as merged. May still be open.`,
|
|
810
|
+
remediation: generateRemediation({ taskId: doneTasks[0].taskId, issue: 'orphan_pr', prUrl }),
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
timestamp: now,
|
|
815
|
+
validating: validatingEntries,
|
|
816
|
+
orphanPRs: orphanEntries,
|
|
817
|
+
summary: {
|
|
818
|
+
totalValidating: validating.length,
|
|
819
|
+
staleValidating: staleCount,
|
|
820
|
+
orphanPRCount: orphanEntries.length,
|
|
821
|
+
prDriftCount: driftCount,
|
|
822
|
+
cleanCount,
|
|
823
|
+
},
|
|
824
|
+
};
|
|
825
|
+
}
|
|
826
|
+
// ── Periodic Runner ────────────────────────────────────────────────────────
|
|
827
|
+
let sweepTimer = null;
|
|
828
|
+
export function startSweeper() {
|
|
829
|
+
if (sweepTimer)
|
|
830
|
+
return;
|
|
831
|
+
console.log(`[Sweeper] Starting execution sweeper (interval: ${SWEEP_INTERVAL_MS / 1000}s, SLA: ${VALIDATING_SLA_MS / 60_000}m, critical: ${VALIDATING_CRITICAL_MS / 60_000}m)`);
|
|
832
|
+
// Defer initial sweep to avoid blocking server startup
|
|
833
|
+
setTimeout(() => {
|
|
834
|
+
;
|
|
835
|
+
(async () => {
|
|
836
|
+
const initial = await sweepValidatingQueue();
|
|
837
|
+
escalateViolations(initial.violations);
|
|
838
|
+
})().catch(err => {
|
|
839
|
+
console.error('[Sweeper] Initial sweep failed:', err);
|
|
840
|
+
});
|
|
841
|
+
}, 5000);
|
|
842
|
+
logDryRun('sweeper_started', `interval=${SWEEP_INTERVAL_MS / 1000}s SLA=${VALIDATING_SLA_MS / 60_000}m critical=${VALIDATING_CRITICAL_MS / 60_000}m`);
|
|
843
|
+
sweepTimer = setInterval(() => {
|
|
844
|
+
;
|
|
845
|
+
(async () => {
|
|
846
|
+
const result = await sweepValidatingQueue();
|
|
847
|
+
escalateViolations(result.violations);
|
|
848
|
+
})().catch(err => {
|
|
849
|
+
console.error('[Sweeper] Sweep failed:', err);
|
|
850
|
+
logDryRun('sweep_error', String(err));
|
|
851
|
+
});
|
|
852
|
+
}, SWEEP_INTERVAL_MS);
|
|
853
|
+
sweepTimer.unref();
|
|
854
|
+
}
|
|
855
|
+
export function stopSweeper() {
|
|
856
|
+
if (sweepTimer) {
|
|
857
|
+
clearInterval(sweepTimer);
|
|
858
|
+
sweepTimer = null;
|
|
859
|
+
logDryRun('sweeper_stopped', 'Manual stop');
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
export function getSweeperStatus() {
|
|
863
|
+
return {
|
|
864
|
+
running: sweepTimer !== null,
|
|
865
|
+
lastSweepAt,
|
|
866
|
+
lastResults: lastSweepResults,
|
|
867
|
+
escalationTracking: Array.from(escalated.entries()).map(([taskId, e]) => ({
|
|
868
|
+
taskId,
|
|
869
|
+
level: e.level,
|
|
870
|
+
at: e.at,
|
|
871
|
+
})),
|
|
872
|
+
dryRunLog,
|
|
873
|
+
};
|
|
874
|
+
}
|
|
875
|
+
//# sourceMappingURL=executionSweeper.js.map
|