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,882 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) Reflectt AI
|
|
3
|
+
/**
|
|
4
|
+
* Board-Health Execution Worker
|
|
5
|
+
*
|
|
6
|
+
* Automated board hygiene with full audit trail and rollback:
|
|
7
|
+
* - Auto-block stale doing tasks (configurable threshold)
|
|
8
|
+
* - Suggest close for abandoned tasks
|
|
9
|
+
* - Emit periodic digest to chat
|
|
10
|
+
* - Audit log for every automated action
|
|
11
|
+
* - Rollback window for reversing decisions
|
|
12
|
+
*/
|
|
13
|
+
import { taskManager } from './tasks.js';
|
|
14
|
+
import { routeMessage } from './messageRouter.js';
|
|
15
|
+
import { validateTaskTimestamp, verifyTaskExists } from './health.js';
|
|
16
|
+
import { policyManager } from './policy.js';
|
|
17
|
+
import { getEffectiveActivity } from './activity-signal.js';
|
|
18
|
+
import { presenceManager } from './presence.js';
|
|
19
|
+
import { suggestReviewer } from './assignment.js';
|
|
20
|
+
import { isTestHarnessTask } from './test-task-filter.js';
|
|
21
|
+
import { recordSystemLoopTick } from './system-loop-state.js';
|
|
22
|
+
const DEFAULT_CONFIG = {
|
|
23
|
+
enabled: true,
|
|
24
|
+
intervalMs: 5 * 60 * 1000, // 5 minutes
|
|
25
|
+
staleDoingThresholdMin: 240, // 4 hours
|
|
26
|
+
suggestCloseThresholdMin: 1440, // 24 hours
|
|
27
|
+
rollbackWindowMs: 60 * 60 * 1000, // 1 hour
|
|
28
|
+
digestIntervalMs: 4 * 60 * 60 * 1000, // 4 hours
|
|
29
|
+
digestChannel: 'ops',
|
|
30
|
+
quietHoursStart: 0,
|
|
31
|
+
quietHoursEnd: 6,
|
|
32
|
+
dryRun: false,
|
|
33
|
+
maxActionsPerTick: 5,
|
|
34
|
+
inactiveAgentThresholdMin: 1440, // 24 hours
|
|
35
|
+
reviewSlaThresholdMin: 480, // 8 hours
|
|
36
|
+
reviewEscalationTarget: 'ryan',
|
|
37
|
+
};
|
|
38
|
+
// ── Worker ─────────────────────────────────────────────────────────────────
|
|
39
|
+
export class BoardHealthWorker {
|
|
40
|
+
config;
|
|
41
|
+
auditLog = [];
|
|
42
|
+
lastDigestAt = 0;
|
|
43
|
+
lastTickAt = 0;
|
|
44
|
+
tickCount = 0;
|
|
45
|
+
timer = null;
|
|
46
|
+
constructor(config) {
|
|
47
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
48
|
+
}
|
|
49
|
+
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
50
|
+
start() {
|
|
51
|
+
if (this.timer)
|
|
52
|
+
return;
|
|
53
|
+
if (!this.config.enabled)
|
|
54
|
+
return;
|
|
55
|
+
this.timer = setInterval(() => {
|
|
56
|
+
this.tick().catch(() => { });
|
|
57
|
+
}, this.config.intervalMs);
|
|
58
|
+
this.timer.unref();
|
|
59
|
+
}
|
|
60
|
+
stop() {
|
|
61
|
+
if (this.timer) {
|
|
62
|
+
clearInterval(this.timer);
|
|
63
|
+
this.timer = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
updateConfig(patch) {
|
|
67
|
+
const wasEnabled = this.config.enabled;
|
|
68
|
+
this.config = { ...this.config, ...patch };
|
|
69
|
+
// Restart timer if interval changed or enable toggled
|
|
70
|
+
if (this.timer) {
|
|
71
|
+
this.stop();
|
|
72
|
+
}
|
|
73
|
+
if (this.config.enabled) {
|
|
74
|
+
this.start();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
getConfig() {
|
|
78
|
+
return { ...this.config };
|
|
79
|
+
}
|
|
80
|
+
// ── Core tick ──────────────────────────────────────────────────────────
|
|
81
|
+
async tick(options) {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
const dryRun = options?.dryRun ?? this.config.dryRun;
|
|
84
|
+
const force = options?.force ?? false;
|
|
85
|
+
// Quiet hours check
|
|
86
|
+
if (!force && this.isQuietHours(now)) {
|
|
87
|
+
return { actions: [], digest: null, skipped: true, reason: 'quiet-hours' };
|
|
88
|
+
}
|
|
89
|
+
this.tickCount++;
|
|
90
|
+
this.lastTickAt = now;
|
|
91
|
+
// Persist tick time so /health/system can prove this worker is actually running.
|
|
92
|
+
recordSystemLoopTick('board_health', now);
|
|
93
|
+
const actions = [];
|
|
94
|
+
// 1. Detect stale doing tasks
|
|
95
|
+
const staleDoing = this.findStaleDoingTasks(now);
|
|
96
|
+
let actionCount = 0;
|
|
97
|
+
for (const task of staleDoing) {
|
|
98
|
+
if (actionCount >= this.config.maxActionsPerTick)
|
|
99
|
+
break;
|
|
100
|
+
// Don't re-block already blocked tasks or test-harness tasks
|
|
101
|
+
if (isTestHarnessTask(task))
|
|
102
|
+
continue;
|
|
103
|
+
const action = await this.applyAutoBlockStale(task, now, dryRun);
|
|
104
|
+
if (action) {
|
|
105
|
+
actions.push(action);
|
|
106
|
+
actionCount++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// 2. Detect tasks that should be suggested for close
|
|
110
|
+
const abandonedTasks = this.findAbandonedTasks(now);
|
|
111
|
+
for (const task of abandonedTasks) {
|
|
112
|
+
if (actionCount >= this.config.maxActionsPerTick)
|
|
113
|
+
break;
|
|
114
|
+
if (isTestHarnessTask(task))
|
|
115
|
+
continue;
|
|
116
|
+
const action = await this.applySuggestClose(task, now, dryRun);
|
|
117
|
+
if (action) {
|
|
118
|
+
actions.push(action);
|
|
119
|
+
actionCount++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// 3. Ready-queue floor check
|
|
123
|
+
const rqfActions = await this.checkReadyQueueFloor(now, dryRun);
|
|
124
|
+
actions.push(...rqfActions);
|
|
125
|
+
// 3a. Review SLA auto-reassignment
|
|
126
|
+
const reviewActions = await this.checkReviewSla(now, dryRun);
|
|
127
|
+
actions.push(...reviewActions);
|
|
128
|
+
// 3b. Reflection automation nudges
|
|
129
|
+
if (!dryRun) {
|
|
130
|
+
try {
|
|
131
|
+
const { tickReflectionNudges } = await import('./reflection-automation.js');
|
|
132
|
+
await tickReflectionNudges();
|
|
133
|
+
}
|
|
134
|
+
catch { /* reflection automation may not be loaded */ }
|
|
135
|
+
}
|
|
136
|
+
// 3c. Working contract enforcement (auto-requeue stale doing tasks)
|
|
137
|
+
if (!dryRun) {
|
|
138
|
+
try {
|
|
139
|
+
const { tickWorkingContract } = await import('./working-contract.js');
|
|
140
|
+
const wcResult = await tickWorkingContract();
|
|
141
|
+
if (wcResult.requeued > 0 || wcResult.warnings > 0) {
|
|
142
|
+
for (const action of wcResult.actions) {
|
|
143
|
+
actions.push({
|
|
144
|
+
id: `wc-${action.timestamp}-${Math.random().toString(36).slice(2, 7)}`,
|
|
145
|
+
kind: (action.type === 'auto_requeue' ? 'auto-requeue' : 'working-contract-warning'),
|
|
146
|
+
taskId: action.taskId,
|
|
147
|
+
agent: action.agent,
|
|
148
|
+
description: action.reason,
|
|
149
|
+
previousState: { type: action.type },
|
|
150
|
+
appliedAt: action.timestamp,
|
|
151
|
+
rolledBack: false,
|
|
152
|
+
rolledBackAt: null,
|
|
153
|
+
rollbackBy: null,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch { /* working-contract module may not be loaded */ }
|
|
159
|
+
}
|
|
160
|
+
// 3d. Ready-queue sweeper: auto-create placeholder tasks for empty lanes
|
|
161
|
+
const sweeperActions = await this.sweepReadyQueue(now, dryRun);
|
|
162
|
+
actions.push(...sweeperActions);
|
|
163
|
+
// 3e. Continuity loop: auto-replenish queues from promoted insights
|
|
164
|
+
if (!dryRun) {
|
|
165
|
+
try {
|
|
166
|
+
const { tickContinuityLoop } = await import('./continuity-loop.js');
|
|
167
|
+
const clResult = await tickContinuityLoop();
|
|
168
|
+
if (clResult.replenished > 0) {
|
|
169
|
+
actions.push(...clResult.actions.map(a => ({
|
|
170
|
+
id: a.id,
|
|
171
|
+
kind: 'continuity-replenish',
|
|
172
|
+
taskId: a.taskId ?? null,
|
|
173
|
+
agent: a.agent,
|
|
174
|
+
description: a.detail,
|
|
175
|
+
previousState: { insightId: a.insightId },
|
|
176
|
+
appliedAt: a.timestamp,
|
|
177
|
+
rolledBack: false,
|
|
178
|
+
rolledBackAt: null,
|
|
179
|
+
rollbackBy: null,
|
|
180
|
+
})));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
catch { /* continuity loop may not be loaded */ }
|
|
184
|
+
}
|
|
185
|
+
// 4. Emit digest if interval elapsed
|
|
186
|
+
let digest = null;
|
|
187
|
+
if (force || now - this.lastDigestAt >= this.config.digestIntervalMs) {
|
|
188
|
+
digest = await this.emitDigest(now, actions, dryRun);
|
|
189
|
+
if (!dryRun) {
|
|
190
|
+
this.lastDigestAt = now;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return { actions, digest, skipped: false };
|
|
194
|
+
}
|
|
195
|
+
// ── Policy: Auto-block stale doing ────────────────────────────────────
|
|
196
|
+
findStaleDoingTasks(now) {
|
|
197
|
+
const thresholdMs = this.config.staleDoingThresholdMin * 60_000;
|
|
198
|
+
const doingTasks = taskManager.listTasks({ status: 'doing' });
|
|
199
|
+
return doingTasks.filter(task => {
|
|
200
|
+
// Verify task still exists (guards against stale cache entries)
|
|
201
|
+
if (!verifyTaskExists(task.id))
|
|
202
|
+
return false;
|
|
203
|
+
const lastActivity = this.getTaskLastActivityAt(task);
|
|
204
|
+
if (!lastActivity)
|
|
205
|
+
return false;
|
|
206
|
+
// Validate timestamp is within reasonable bounds
|
|
207
|
+
const validatedTs = validateTaskTimestamp(lastActivity, now);
|
|
208
|
+
if (!validatedTs)
|
|
209
|
+
return false;
|
|
210
|
+
return now - validatedTs > thresholdMs;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
async applyAutoBlockStale(task, now, dryRun) {
|
|
214
|
+
// Check if we already acted on this task recently
|
|
215
|
+
const recentAction = this.auditLog.find(a => a.taskId === task.id && a.kind === 'auto-block-stale' && !a.rolledBack
|
|
216
|
+
&& now - a.appliedAt < this.config.rollbackWindowMs);
|
|
217
|
+
if (recentAction)
|
|
218
|
+
return null;
|
|
219
|
+
const staleMinutes = Math.floor((now - this.getTaskLastActivityAt(task)) / 60_000);
|
|
220
|
+
const action = {
|
|
221
|
+
id: `bh-${now}-${Math.random().toString(36).slice(2, 8)}`,
|
|
222
|
+
kind: 'auto-block-stale',
|
|
223
|
+
taskId: task.id,
|
|
224
|
+
agent: task.assignee || null,
|
|
225
|
+
description: `Auto-blocked stale doing task (${staleMinutes}m inactive, threshold: ${this.config.staleDoingThresholdMin}m)`,
|
|
226
|
+
previousState: {
|
|
227
|
+
status: task.status,
|
|
228
|
+
metadata: task.metadata ? { ...task.metadata } : null,
|
|
229
|
+
},
|
|
230
|
+
appliedAt: now,
|
|
231
|
+
rolledBack: false,
|
|
232
|
+
rolledBackAt: null,
|
|
233
|
+
rollbackBy: null,
|
|
234
|
+
};
|
|
235
|
+
if (!dryRun) {
|
|
236
|
+
try {
|
|
237
|
+
await taskManager.updateTask(task.id, {
|
|
238
|
+
status: 'blocked',
|
|
239
|
+
metadata: {
|
|
240
|
+
...(task.metadata || {}),
|
|
241
|
+
board_health_action: 'auto-blocked-stale',
|
|
242
|
+
board_health_action_at: now,
|
|
243
|
+
board_health_action_id: action.id,
|
|
244
|
+
board_health_stale_minutes: staleMinutes,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
// Notify the assignee
|
|
248
|
+
if (task.assignee) {
|
|
249
|
+
await routeMessage({
|
|
250
|
+
from: 'system',
|
|
251
|
+
content: `⚠️ Board health: auto-blocked **${task.id}** (${task.title}) — ${staleMinutes}m with no activity. @${task.assignee} update status or rollback via \`POST /board-health/rollback/${action.id}\`.`,
|
|
252
|
+
category: 'watchdog-alert',
|
|
253
|
+
severity: 'warning',
|
|
254
|
+
taskId: task.id,
|
|
255
|
+
mentions: [task.assignee],
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
this.auditLog.push(action);
|
|
264
|
+
return action;
|
|
265
|
+
}
|
|
266
|
+
// ── Policy: Suggest close ─────────────────────────────────────────────
|
|
267
|
+
findAbandonedTasks(now) {
|
|
268
|
+
const thresholdMs = this.config.suggestCloseThresholdMin * 60_000;
|
|
269
|
+
const candidates = taskManager.listTasks({}).filter(t => t.status === 'blocked' || t.status === 'todo');
|
|
270
|
+
return candidates.filter(task => {
|
|
271
|
+
// Verify task still exists (guards against stale cache)
|
|
272
|
+
if (!verifyTaskExists(task.id))
|
|
273
|
+
return false;
|
|
274
|
+
const lastActivity = this.getTaskLastActivityAt(task);
|
|
275
|
+
if (!lastActivity) {
|
|
276
|
+
// If no activity at all, check createdAt with validation
|
|
277
|
+
const createdAt = validateTaskTimestamp(task.createdAt, now);
|
|
278
|
+
return createdAt !== null && now - createdAt > thresholdMs;
|
|
279
|
+
}
|
|
280
|
+
const validatedTs = validateTaskTimestamp(lastActivity, now);
|
|
281
|
+
if (!validatedTs)
|
|
282
|
+
return false;
|
|
283
|
+
return now - validatedTs > thresholdMs;
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
async applySuggestClose(task, now, dryRun) {
|
|
287
|
+
// Check if we already suggested close for this task
|
|
288
|
+
const recentAction = this.auditLog.find(a => a.taskId === task.id && a.kind === 'suggest-close' && !a.rolledBack
|
|
289
|
+
&& now - a.appliedAt < 24 * 60 * 60 * 1000);
|
|
290
|
+
if (recentAction)
|
|
291
|
+
return null;
|
|
292
|
+
const lastActivity = this.getTaskLastActivityAt(task);
|
|
293
|
+
const staleMinutes = lastActivity
|
|
294
|
+
? Math.floor((now - lastActivity) / 60_000)
|
|
295
|
+
: Math.floor((now - (typeof task.createdAt === 'number' ? task.createdAt : now)) / 60_000);
|
|
296
|
+
const action = {
|
|
297
|
+
id: `bh-${now}-${Math.random().toString(36).slice(2, 8)}`,
|
|
298
|
+
kind: 'suggest-close',
|
|
299
|
+
taskId: task.id,
|
|
300
|
+
agent: task.assignee || null,
|
|
301
|
+
description: `Suggested close for abandoned task (${staleMinutes}m inactive, status: ${task.status})`,
|
|
302
|
+
previousState: null, // No state change — just a suggestion
|
|
303
|
+
appliedAt: now,
|
|
304
|
+
rolledBack: false,
|
|
305
|
+
rolledBackAt: null,
|
|
306
|
+
rollbackBy: null,
|
|
307
|
+
};
|
|
308
|
+
if (!dryRun) {
|
|
309
|
+
try {
|
|
310
|
+
// Add a comment to the task
|
|
311
|
+
const comments = taskManager.getTaskComments(task.id);
|
|
312
|
+
const hasRecentBotComment = comments.some(c => c.author === 'system' && now - (typeof c.timestamp === 'number' ? c.timestamp : 0) < 24 * 60 * 60 * 1000);
|
|
313
|
+
if (!hasRecentBotComment) {
|
|
314
|
+
await taskManager.addTaskComment(task.id, 'system', `🔍 Board health: this task has been inactive for ${Math.floor(staleMinutes / 60)}h${staleMinutes % 60}m. Consider closing if no longer needed. Action ID: ${action.id}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
this.auditLog.push(action);
|
|
322
|
+
return action;
|
|
323
|
+
}
|
|
324
|
+
// ── Policy: Ready-queue floor ──────────────────────────────────────────
|
|
325
|
+
/** Track last alert time per agent to enforce cooldown */
|
|
326
|
+
readyQueueLastAlertAt = {};
|
|
327
|
+
/** Track last alert state fingerprint per agent to suppress duplicate alerts */
|
|
328
|
+
readyQueueLastState = {};
|
|
329
|
+
/** Track when each agent's queue first went empty (for idle escalation) */
|
|
330
|
+
idleQueueSince = {};
|
|
331
|
+
async checkReadyQueueFloor(now, dryRun) {
|
|
332
|
+
const policy = policyManager.get();
|
|
333
|
+
const rqf = policy.readyQueueFloor;
|
|
334
|
+
if (!rqf?.enabled)
|
|
335
|
+
return [];
|
|
336
|
+
const actions = [];
|
|
337
|
+
for (const agent of rqf.agents) {
|
|
338
|
+
// Count unblocked todo tasks for this agent
|
|
339
|
+
const todoTasks = taskManager.listTasks({ status: 'todo', assignee: agent });
|
|
340
|
+
const unblockedTodo = todoTasks.filter(t => {
|
|
341
|
+
const blocked = t.metadata?.blocked_by;
|
|
342
|
+
if (!blocked)
|
|
343
|
+
return true;
|
|
344
|
+
// Check if blocker is still open
|
|
345
|
+
const blocker = taskManager.getTask(blocked);
|
|
346
|
+
return !blocker || blocker.status === 'done';
|
|
347
|
+
});
|
|
348
|
+
const doingTasks = taskManager.listTasks({ status: 'doing', assignee: agent });
|
|
349
|
+
const validatingTasks = taskManager.listTasks({ status: 'validating', assignee: agent });
|
|
350
|
+
const readyCount = unblockedTodo.length;
|
|
351
|
+
const activeCount = doingTasks.length + validatingTasks.length;
|
|
352
|
+
const belowFloor = readyCount < rqf.minReady;
|
|
353
|
+
// Breach definition: below-floor AND no active work (doing/validating).
|
|
354
|
+
// If the agent is active, we may still emit an informational note, but it is not a breach.
|
|
355
|
+
const isBreach = belowFloor && activeCount === 0;
|
|
356
|
+
// Check cooldown
|
|
357
|
+
let lastAlert = this.readyQueueLastAlertAt[agent] || 0;
|
|
358
|
+
const cooldownMs = (rqf.cooldownMin || 30) * 60_000;
|
|
359
|
+
// Ready-queue floor check (breach vs info)
|
|
360
|
+
if (belowFloor && now - lastAlert > cooldownMs) {
|
|
361
|
+
const deficit = rqf.minReady - readyCount;
|
|
362
|
+
// State fingerprint: suppress if identical to last alert
|
|
363
|
+
const blockedTasks = todoTasks.filter(t => !unblockedTodo.includes(t));
|
|
364
|
+
const stateFingerprint = `${readyCount}:${todoTasks.length}:${blockedTasks.map(t => t.id).sort().join(',')}:${doingTasks.length}:${validatingTasks.length}`;
|
|
365
|
+
const lastState = this.readyQueueLastState[agent];
|
|
366
|
+
if (lastState === stateFingerprint) {
|
|
367
|
+
// State unchanged since last alert — skip (debounce)
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
// Build breakdown: show blocked tasks and why
|
|
371
|
+
let breakdown = '';
|
|
372
|
+
if (todoTasks.length > readyCount) {
|
|
373
|
+
breakdown += `\n 📊 todo=${todoTasks.length}, unblocked=${readyCount}, blocked=${blockedTasks.length}, doing=${doingTasks.length}, validating=${validatingTasks.length}`;
|
|
374
|
+
const capped = blockedTasks.slice(0, 5);
|
|
375
|
+
for (const bt of capped) {
|
|
376
|
+
const blockedBy = bt.metadata?.blocked_by || 'unknown';
|
|
377
|
+
breakdown += `\n • ${bt.id} (${(bt.title || '').slice(0, 50)}) — blocked_by: ${blockedBy}`;
|
|
378
|
+
}
|
|
379
|
+
if (blockedTasks.length > 5)
|
|
380
|
+
breakdown += `\n … and ${blockedTasks.length - 5} more`;
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
breakdown += `\n 📊 todo=${todoTasks.length} (all unblocked), doing=${doingTasks.length}, validating=${validatingTasks.length}`;
|
|
384
|
+
}
|
|
385
|
+
// Snapshot timestamp for freshness judgment
|
|
386
|
+
const snapshotTime = new Date(now).toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
|
|
387
|
+
const msg = isBreach
|
|
388
|
+
? `⚠️ Ready-queue floor (idle): @${agent} has ${readyCount}/${rqf.minReady} unblocked todo tasks (need ${deficit} more). @sage @pixel — please spec/assign tasks to keep engineering lane fed.${breakdown}\n 🕐 snapshot: ${snapshotTime}`
|
|
389
|
+
: `ℹ️ Ready-queue in-flight: @${agent} is active (doing=${doingTasks.length}, validating=${validatingTasks.length}). In validating, next task suggested via /tasks/next. Queue below floor (unblocked todo=${readyCount}, floor=${rqf.minReady}, need ${deficit} more).${breakdown}\n 🕐 snapshot: ${snapshotTime}`;
|
|
390
|
+
if (!dryRun) {
|
|
391
|
+
try {
|
|
392
|
+
await routeMessage({
|
|
393
|
+
from: 'system',
|
|
394
|
+
content: msg,
|
|
395
|
+
category: 'watchdog-alert',
|
|
396
|
+
severity: isBreach ? 'warning' : 'info',
|
|
397
|
+
forceChannel: rqf.channel || 'general',
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
catch { /* chat may not be available in test */ }
|
|
401
|
+
this.readyQueueLastAlertAt[agent] = now;
|
|
402
|
+
this.readyQueueLastState[agent] = stateFingerprint;
|
|
403
|
+
lastAlert = now; // prevent same-tick escalation based on stale local lastAlert
|
|
404
|
+
}
|
|
405
|
+
if (isBreach) {
|
|
406
|
+
const action = {
|
|
407
|
+
id: `rqf-${agent}-${now}`,
|
|
408
|
+
kind: 'ready-queue-warning',
|
|
409
|
+
taskId: null,
|
|
410
|
+
agent,
|
|
411
|
+
description: `Ready queue below floor: ${readyCount}/${rqf.minReady} for @${agent}`,
|
|
412
|
+
previousState: { readyCount, doingCount: doingTasks.length, validatingCount: validatingTasks.length },
|
|
413
|
+
appliedAt: now,
|
|
414
|
+
rolledBack: false,
|
|
415
|
+
rolledBackAt: null,
|
|
416
|
+
rollbackBy: null,
|
|
417
|
+
};
|
|
418
|
+
this.auditLog.push(action);
|
|
419
|
+
actions.push(action);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Clear state fingerprint when floor is met (so next breach alerts fresh)
|
|
423
|
+
if (readyCount >= rqf.minReady) {
|
|
424
|
+
delete this.readyQueueLastState[agent];
|
|
425
|
+
}
|
|
426
|
+
// Idle escalation: agent has 0 doing + 0 validating + 0 (unblocked) todo for too long
|
|
427
|
+
const totalActive = doingTasks.length + validatingTasks.length + readyCount;
|
|
428
|
+
if (totalActive === 0) {
|
|
429
|
+
if (!this.idleQueueSince[agent]) {
|
|
430
|
+
this.idleQueueSince[agent] = now;
|
|
431
|
+
}
|
|
432
|
+
const idleMinutes = Math.floor((now - this.idleQueueSince[agent]) / 60_000);
|
|
433
|
+
if (idleMinutes >= (rqf.escalateAfterMin || 60) && now - lastAlert > cooldownMs) {
|
|
434
|
+
const idleSnapshotTime = new Date(now).toISOString().replace('T', ' ').slice(0, 19) + ' UTC';
|
|
435
|
+
const msg = `🚨 Idle escalation: @${agent} has had 0 tasks (doing + validating + todo) for ${idleMinutes}m. Immediate assignment needed. @sage\n 🕐 snapshot: ${idleSnapshotTime}`;
|
|
436
|
+
if (!dryRun) {
|
|
437
|
+
try {
|
|
438
|
+
await routeMessage({ from: 'system', content: msg, forceChannel: rqf.channel || 'general', category: 'escalation', severity: 'critical' });
|
|
439
|
+
}
|
|
440
|
+
catch { /* chat may not be available in test */ }
|
|
441
|
+
this.readyQueueLastAlertAt[agent] = now;
|
|
442
|
+
}
|
|
443
|
+
const action = {
|
|
444
|
+
id: `idle-${agent}-${now}`,
|
|
445
|
+
kind: 'idle-queue-escalation',
|
|
446
|
+
taskId: null,
|
|
447
|
+
agent,
|
|
448
|
+
description: `Idle queue escalation: @${agent} idle for ${idleMinutes}m`,
|
|
449
|
+
previousState: { idleMinutes },
|
|
450
|
+
appliedAt: now,
|
|
451
|
+
rolledBack: false,
|
|
452
|
+
rolledBackAt: null,
|
|
453
|
+
rollbackBy: null,
|
|
454
|
+
};
|
|
455
|
+
this.auditLog.push(action);
|
|
456
|
+
actions.push(action);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
// Reset idle tracker when agent has work
|
|
461
|
+
delete this.idleQueueSince[agent];
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return actions;
|
|
465
|
+
}
|
|
466
|
+
// ── Ready-queue sweeper ────────────────────────────────────────────────
|
|
467
|
+
/** Track last replenish time per agent to enforce cooldown between auto-creates */
|
|
468
|
+
replenishLastAt = {};
|
|
469
|
+
/**
|
|
470
|
+
* Sweeper tick: for each lane below readyFloor, emit a digest warning.
|
|
471
|
+
*
|
|
472
|
+
* Does NOT create placeholder tasks — the continuity loop (3e) handles
|
|
473
|
+
* real replenishment from promoted insights. Creating empty placeholders
|
|
474
|
+
* clutters the board with scopeless tasks that no one can work on.
|
|
475
|
+
*
|
|
476
|
+
* One warning per agent-deficit per tick, with a 30-minute per-agent cooldown.
|
|
477
|
+
*/
|
|
478
|
+
async sweepReadyQueue(now, dryRun) {
|
|
479
|
+
const { getLanesConfig } = await import('./lane-config.js');
|
|
480
|
+
const lanes = getLanesConfig();
|
|
481
|
+
const actions = [];
|
|
482
|
+
const cooldownMs = 30 * 60_000;
|
|
483
|
+
for (const lane of lanes) {
|
|
484
|
+
for (const agent of lane.agents) {
|
|
485
|
+
// Skip ghost agents that have never checked in
|
|
486
|
+
if (!presenceManager.getPresence(agent))
|
|
487
|
+
continue;
|
|
488
|
+
// Skip inactive agents (configurable threshold, default 24h)
|
|
489
|
+
const presence = presenceManager.getPresence(agent);
|
|
490
|
+
const lastActive = presence?.lastUpdate ?? 0;
|
|
491
|
+
const inactiveThresholdMs = this.config.inactiveAgentThresholdMin * 60_000;
|
|
492
|
+
if (lastActive > 0 && (now - lastActive) > inactiveThresholdMs)
|
|
493
|
+
continue;
|
|
494
|
+
// Enforce per-agent cooldown to avoid spam
|
|
495
|
+
const lastReplenish = this.replenishLastAt[agent] ?? 0;
|
|
496
|
+
if (now - lastReplenish < cooldownMs)
|
|
497
|
+
continue;
|
|
498
|
+
// Count unblocked todo tasks for this agent
|
|
499
|
+
const todoTasks = taskManager.listTasks({ status: 'todo', assignee: agent });
|
|
500
|
+
const unblockedTodo = todoTasks.filter(t => {
|
|
501
|
+
const blocked = t.metadata?.blocked_by;
|
|
502
|
+
if (!blocked)
|
|
503
|
+
return true;
|
|
504
|
+
const blocker = taskManager.getTask(blocked);
|
|
505
|
+
return !blocker || blocker.status === 'done';
|
|
506
|
+
});
|
|
507
|
+
const deficit = lane.readyFloor - unblockedTodo.length;
|
|
508
|
+
if (deficit <= 0)
|
|
509
|
+
continue;
|
|
510
|
+
// Emit a warning action — do NOT create placeholder tasks.
|
|
511
|
+
// The continuity loop will attempt real replenishment from insights.
|
|
512
|
+
const action = {
|
|
513
|
+
id: `rqs-${agent}-${now}-0`,
|
|
514
|
+
kind: 'ready-queue-replenish',
|
|
515
|
+
taskId: null,
|
|
516
|
+
agent,
|
|
517
|
+
description: `Ready queue below floor for @${agent} in lane "${lane.name}" (${unblockedTodo.length}/${lane.readyFloor} ready). Deferring to continuity loop for scoped replenishment.`,
|
|
518
|
+
previousState: { readyCount: unblockedTodo.length, readyFloor: lane.readyFloor },
|
|
519
|
+
appliedAt: now,
|
|
520
|
+
rolledBack: false,
|
|
521
|
+
rolledBackAt: null,
|
|
522
|
+
rollbackBy: null,
|
|
523
|
+
};
|
|
524
|
+
this.auditLog.push(action);
|
|
525
|
+
actions.push(action);
|
|
526
|
+
this.replenishLastAt[agent] = now;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return actions;
|
|
530
|
+
}
|
|
531
|
+
// ── Policy: Review SLA auto-reassignment ──────────────────────────────
|
|
532
|
+
/** Track last reassignment per task to avoid churning */
|
|
533
|
+
reviewReassignLastAt = {};
|
|
534
|
+
async checkReviewSla(now, dryRun) {
|
|
535
|
+
const thresholdMs = this.config.reviewSlaThresholdMin * 60_000;
|
|
536
|
+
const cooldownMs = thresholdMs; // Don't re-reassign within one SLA window
|
|
537
|
+
const actions = [];
|
|
538
|
+
const normalizeEpochMs = (v) => {
|
|
539
|
+
if (typeof v !== 'number' || !Number.isFinite(v))
|
|
540
|
+
return 0;
|
|
541
|
+
// Heuristic: values below ~2001-09-09 in ms are likely seconds.
|
|
542
|
+
if (v > 0 && v < 100_000_000_000)
|
|
543
|
+
return v * 1000;
|
|
544
|
+
// Clamp future timestamps
|
|
545
|
+
if (v > now + 60_000)
|
|
546
|
+
return now;
|
|
547
|
+
return v;
|
|
548
|
+
};
|
|
549
|
+
const validatingTasks = taskManager.listTasks({ status: 'validating' })
|
|
550
|
+
.filter(t => !isTestHarnessTask(t) && t.reviewer);
|
|
551
|
+
for (const task of validatingTasks) {
|
|
552
|
+
// Skip if we already reassigned this task recently
|
|
553
|
+
const lastReassignAt = this.reviewReassignLastAt[task.id] ?? 0;
|
|
554
|
+
if (now - lastReassignAt < cooldownMs)
|
|
555
|
+
continue;
|
|
556
|
+
// Check reviewer activity on this task using the review_last_activity_at metadata field
|
|
557
|
+
const meta = (task.metadata || {});
|
|
558
|
+
const reviewEnteredAt = normalizeEpochMs(meta.entered_validating_at) || (task.updatedAt ?? task.createdAt);
|
|
559
|
+
const reviewLastActivityAt = normalizeEpochMs(meta.review_last_activity_at) || reviewEnteredAt;
|
|
560
|
+
// Use the more recent of entered_validating and review_last_activity
|
|
561
|
+
const lastReviewActivity = Math.max(reviewEnteredAt || 0, reviewLastActivityAt || 0);
|
|
562
|
+
if (!lastReviewActivity || now - lastReviewActivity < thresholdMs)
|
|
563
|
+
continue;
|
|
564
|
+
// Race guard: task may have left validating between listTasks() and now.
|
|
565
|
+
// Never act on done/closed tasks.
|
|
566
|
+
const latest = taskManager.getTask(task.id);
|
|
567
|
+
if (!latest || latest.status !== 'validating')
|
|
568
|
+
continue;
|
|
569
|
+
const rawStaleMs = now - lastReviewActivity;
|
|
570
|
+
// Clamp to 30 days max — anything larger is a timestamp bug
|
|
571
|
+
const MAX_REVIEW_STALE_MS = 30 * 24 * 60 * 60_000;
|
|
572
|
+
if (rawStaleMs > MAX_REVIEW_STALE_MS) {
|
|
573
|
+
console.warn(`[board-health] review-sla: skipping ${task.id} — implausible stale time ${Math.floor(rawStaleMs / 60_000)}m (likely timestamp bug)`);
|
|
574
|
+
continue;
|
|
575
|
+
}
|
|
576
|
+
const staleMinutes = Math.floor(rawStaleMs / 60_000);
|
|
577
|
+
const currentReviewer = task.reviewer;
|
|
578
|
+
const newReviewer = this.pickAlternateReviewer(task, currentReviewer);
|
|
579
|
+
const action = {
|
|
580
|
+
id: `bh-${now}-${Math.random().toString(36).slice(2, 8)}`,
|
|
581
|
+
kind: 'review-reassign',
|
|
582
|
+
taskId: task.id,
|
|
583
|
+
agent: currentReviewer,
|
|
584
|
+
description: `Review SLA breach: reassigned reviewer ${currentReviewer} → ${newReviewer} (${staleMinutes}m without review activity, threshold: ${this.config.reviewSlaThresholdMin}m)`,
|
|
585
|
+
previousState: {
|
|
586
|
+
reviewer: currentReviewer,
|
|
587
|
+
review_state: meta.review_state ?? null,
|
|
588
|
+
},
|
|
589
|
+
appliedAt: now,
|
|
590
|
+
rolledBack: false,
|
|
591
|
+
rolledBackAt: null,
|
|
592
|
+
rollbackBy: null,
|
|
593
|
+
};
|
|
594
|
+
this.auditLog.push(action);
|
|
595
|
+
actions.push(action);
|
|
596
|
+
if (!dryRun) {
|
|
597
|
+
try {
|
|
598
|
+
await taskManager.updateTask(task.id, {
|
|
599
|
+
reviewer: newReviewer,
|
|
600
|
+
metadata: {
|
|
601
|
+
...meta,
|
|
602
|
+
review_reassigned_from: currentReviewer,
|
|
603
|
+
review_reassigned_at: now,
|
|
604
|
+
review_reassign_reason: `SLA breach: ${staleMinutes}m without reviewer activity (threshold: ${this.config.reviewSlaThresholdMin}m)`,
|
|
605
|
+
board_health_action: 'review-reassign',
|
|
606
|
+
board_health_action_id: action.id,
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
console.warn(`[board-health] review-reassign updateTask failed for ${task.id}:`, err.message);
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
await routeMessage({
|
|
615
|
+
from: 'system',
|
|
616
|
+
content: `🔄 Review SLA: reassigned reviewer on **${task.id}** (${(task.title || '').slice(0, 60)}) from @${currentReviewer} → @${newReviewer} (${staleMinutes}m without activity). ${newReviewer === this.config.reviewEscalationTarget ? '⚡ Escalated — no active reviewer available.' : ''}`,
|
|
617
|
+
category: 'watchdog-alert',
|
|
618
|
+
severity: 'warning',
|
|
619
|
+
taskId: task.id,
|
|
620
|
+
mentions: [newReviewer, currentReviewer],
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
// Message routing failure is non-critical
|
|
625
|
+
}
|
|
626
|
+
this.reviewReassignLastAt[task.id] = now;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
return actions;
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Pick an alternate reviewer for a task.
|
|
633
|
+
*
|
|
634
|
+
* IMPORTANT: reviewer reassignment must respect routing guardrails.
|
|
635
|
+
* We should not drift to designers/voice roles for ops/infra tasks just
|
|
636
|
+
* because they happened to be "most recently active".
|
|
637
|
+
*/
|
|
638
|
+
pickAlternateReviewer(task, currentReviewer) {
|
|
639
|
+
const assignee = (task.assignee || '').toLowerCase();
|
|
640
|
+
const current = currentReviewer.toLowerCase();
|
|
641
|
+
const allPresence = presenceManager.getAllPresence();
|
|
642
|
+
const now = Date.now();
|
|
643
|
+
const activeThresholdMs = 60 * 60 * 1000; // active if seen in last hour
|
|
644
|
+
const active = allPresence
|
|
645
|
+
.filter(p => p.status !== 'offline' && now - p.lastUpdate < activeThresholdMs)
|
|
646
|
+
.map(p => ({ agent: p.agent, agentLower: p.agent.toLowerCase(), lastUpdate: p.lastUpdate }));
|
|
647
|
+
const activeSet = new Set(active.map(a => a.agentLower));
|
|
648
|
+
// No active agent at all → escalate
|
|
649
|
+
if (active.length === 0)
|
|
650
|
+
return this.config.reviewEscalationTarget;
|
|
651
|
+
// Rank reviewers via assignment engine (respects opt-in/neverRoute guardrails)
|
|
652
|
+
let allTasks = [];
|
|
653
|
+
try {
|
|
654
|
+
allTasks = taskManager.listTasks({});
|
|
655
|
+
}
|
|
656
|
+
catch { /* ok */ }
|
|
657
|
+
const suggestion = suggestReviewer({
|
|
658
|
+
title: task.title,
|
|
659
|
+
assignee: task.assignee,
|
|
660
|
+
tags: task.tags,
|
|
661
|
+
done_criteria: task.done_criteria,
|
|
662
|
+
metadata: task.metadata,
|
|
663
|
+
}, allTasks);
|
|
664
|
+
const eligibleByScore = (suggestion.scores || []).map(s => s.agent);
|
|
665
|
+
const eligibleSet = new Set(eligibleByScore.map(a => a.toLowerCase()));
|
|
666
|
+
// Primary: pick the highest-ranked eligible reviewer who is active.
|
|
667
|
+
for (const candidate of eligibleByScore) {
|
|
668
|
+
const c = candidate.toLowerCase();
|
|
669
|
+
if (c === current || c === assignee)
|
|
670
|
+
continue;
|
|
671
|
+
if (c === this.config.reviewEscalationTarget.toLowerCase())
|
|
672
|
+
continue;
|
|
673
|
+
if (!activeSet.has(c))
|
|
674
|
+
continue;
|
|
675
|
+
return candidate;
|
|
676
|
+
}
|
|
677
|
+
// Secondary: if no ranked candidate is active, fall back to most-recent ACTIVE
|
|
678
|
+
// among eligible reviewers.
|
|
679
|
+
const fallback = active
|
|
680
|
+
.filter(p => {
|
|
681
|
+
if (p.agentLower === current || p.agentLower === assignee)
|
|
682
|
+
return false;
|
|
683
|
+
if (p.agentLower === this.config.reviewEscalationTarget.toLowerCase())
|
|
684
|
+
return false;
|
|
685
|
+
return eligibleSet.has(p.agentLower);
|
|
686
|
+
})
|
|
687
|
+
.sort((a, b) => b.lastUpdate - a.lastUpdate);
|
|
688
|
+
if (fallback.length > 0)
|
|
689
|
+
return fallback[0].agent;
|
|
690
|
+
// No eligible active reviewer available — escalate.
|
|
691
|
+
return this.config.reviewEscalationTarget;
|
|
692
|
+
}
|
|
693
|
+
// ── Digest ────────────────────────────────────────────────────────────
|
|
694
|
+
async emitDigest(now, recentActions, dryRun) {
|
|
695
|
+
const allTasks = taskManager.listTasks({});
|
|
696
|
+
const doingTasks = allTasks.filter(t => t.status === 'doing');
|
|
697
|
+
const blockedTasks = allTasks.filter(t => t.status === 'blocked');
|
|
698
|
+
const todoTasks = allTasks.filter(t => t.status === 'todo');
|
|
699
|
+
const validatingTasks = allTasks.filter(t => t.status === 'validating');
|
|
700
|
+
const staleDoingCount = this.findStaleDoingTasks(now).length;
|
|
701
|
+
const suggestedCloseCount = this.findAbandonedTasks(now).length;
|
|
702
|
+
const blockedTaskIds = recentActions
|
|
703
|
+
.filter(a => a.kind === 'auto-block-stale' && a.taskId)
|
|
704
|
+
.map(a => a.taskId);
|
|
705
|
+
const suggestedCloseTaskIds = recentActions
|
|
706
|
+
.filter(a => a.kind === 'suggest-close' && a.taskId)
|
|
707
|
+
.map(a => a.taskId);
|
|
708
|
+
const lines = [
|
|
709
|
+
`📊 **Board Health Digest**`,
|
|
710
|
+
``,
|
|
711
|
+
`**Board:** ${todoTasks.length} todo · ${doingTasks.length} doing · ${validatingTasks.length} validating · ${blockedTasks.length} blocked`,
|
|
712
|
+
`**Stale doing:** ${staleDoingCount} tasks (>${this.config.staleDoingThresholdMin}m threshold)`,
|
|
713
|
+
`**Abandoned candidates:** ${suggestedCloseCount} tasks (>${Math.floor(this.config.suggestCloseThresholdMin / 60)}h threshold)`,
|
|
714
|
+
];
|
|
715
|
+
if (recentActions.length > 0) {
|
|
716
|
+
lines.push(``, `**Actions this cycle:** ${recentActions.length}`);
|
|
717
|
+
for (const a of recentActions.slice(0, 5)) {
|
|
718
|
+
lines.push(`- ${a.kind}: ${a.taskId || 'n/a'} — ${a.description}`);
|
|
719
|
+
}
|
|
720
|
+
if (recentActions.length > 5) {
|
|
721
|
+
lines.push(`- ... and ${recentActions.length - 5} more`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
lines.push(``, `**Actions this cycle:** none (board is healthy ✅)`);
|
|
726
|
+
}
|
|
727
|
+
const summary = lines.join('\n');
|
|
728
|
+
const digest = {
|
|
729
|
+
timestamp: now,
|
|
730
|
+
staleDoingCount,
|
|
731
|
+
suggestedCloseCount,
|
|
732
|
+
actionsApplied: recentActions.length,
|
|
733
|
+
blockedTasks: blockedTaskIds,
|
|
734
|
+
suggestedCloseTasks: suggestedCloseTaskIds,
|
|
735
|
+
summary,
|
|
736
|
+
};
|
|
737
|
+
if (!dryRun) {
|
|
738
|
+
await routeMessage({
|
|
739
|
+
from: 'system',
|
|
740
|
+
content: summary,
|
|
741
|
+
category: 'digest',
|
|
742
|
+
severity: 'info',
|
|
743
|
+
}).catch(() => { });
|
|
744
|
+
// Log digest as audit action
|
|
745
|
+
this.auditLog.push({
|
|
746
|
+
id: `bh-digest-${now}`,
|
|
747
|
+
kind: 'digest-emitted',
|
|
748
|
+
taskId: null,
|
|
749
|
+
agent: null,
|
|
750
|
+
description: `Digest emitted: ${recentActions.length} actions, ${staleDoingCount} stale, ${suggestedCloseCount} abandoned`,
|
|
751
|
+
previousState: null,
|
|
752
|
+
appliedAt: now,
|
|
753
|
+
rolledBack: false,
|
|
754
|
+
rolledBackAt: null,
|
|
755
|
+
rollbackBy: null,
|
|
756
|
+
});
|
|
757
|
+
}
|
|
758
|
+
return digest;
|
|
759
|
+
}
|
|
760
|
+
// ── Rollback ──────────────────────────────────────────────────────────
|
|
761
|
+
async rollback(actionId, rolledBackBy = 'manual') {
|
|
762
|
+
const action = this.auditLog.find(a => a.id === actionId);
|
|
763
|
+
if (!action) {
|
|
764
|
+
return { success: false, message: `Action ${actionId} not found` };
|
|
765
|
+
}
|
|
766
|
+
if (action.rolledBack) {
|
|
767
|
+
return { success: false, message: `Action ${actionId} already rolled back at ${new Date(action.rolledBackAt).toISOString()}` };
|
|
768
|
+
}
|
|
769
|
+
const now = Date.now();
|
|
770
|
+
if (now - action.appliedAt > this.config.rollbackWindowMs) {
|
|
771
|
+
return {
|
|
772
|
+
success: false,
|
|
773
|
+
message: `Rollback window expired (${Math.floor(this.config.rollbackWindowMs / 60_000)}m). Action was applied ${Math.floor((now - action.appliedAt) / 60_000)}m ago.`,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
// Only auto-block-stale is rollbackable (it changes task state)
|
|
777
|
+
if (action.kind === 'auto-block-stale' && action.taskId && action.previousState) {
|
|
778
|
+
try {
|
|
779
|
+
const prev = action.previousState;
|
|
780
|
+
await taskManager.updateTask(action.taskId, {
|
|
781
|
+
status: (prev.status || 'doing'),
|
|
782
|
+
metadata: {
|
|
783
|
+
...(prev.metadata || {}),
|
|
784
|
+
board_health_rollback: true,
|
|
785
|
+
board_health_rollback_at: now,
|
|
786
|
+
board_health_rollback_by: rolledBackBy,
|
|
787
|
+
},
|
|
788
|
+
});
|
|
789
|
+
action.rolledBack = true;
|
|
790
|
+
action.rolledBackAt = now;
|
|
791
|
+
action.rollbackBy = rolledBackBy;
|
|
792
|
+
await routeMessage({
|
|
793
|
+
from: 'system',
|
|
794
|
+
content: `↩️ Board health rollback: **${action.taskId}** restored to \`${prev.status}\` (action ${actionId} reversed by ${rolledBackBy}).`,
|
|
795
|
+
category: 'system-info',
|
|
796
|
+
severity: 'info',
|
|
797
|
+
taskId: action.taskId || undefined,
|
|
798
|
+
}).catch(() => { });
|
|
799
|
+
return { success: true, message: `Rolled back action ${actionId}`, action };
|
|
800
|
+
}
|
|
801
|
+
catch (err) {
|
|
802
|
+
return { success: false, message: `Rollback failed: ${err.message || 'unknown error'}` };
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
if (action.kind === 'suggest-close') {
|
|
806
|
+
// Suggest-close doesn't change state, just mark as rolled back to suppress re-suggestion
|
|
807
|
+
action.rolledBack = true;
|
|
808
|
+
action.rolledBackAt = now;
|
|
809
|
+
action.rollbackBy = rolledBackBy;
|
|
810
|
+
return { success: true, message: `Close suggestion dismissed for ${action.taskId}`, action };
|
|
811
|
+
}
|
|
812
|
+
return { success: false, message: `Action kind '${action.kind}' is not rollbackable` };
|
|
813
|
+
}
|
|
814
|
+
// ── Query ─────────────────────────────────────────────────────────────
|
|
815
|
+
getStatus() {
|
|
816
|
+
const now = Date.now();
|
|
817
|
+
const recent = this.auditLog.filter(a => now - a.appliedAt < 24 * 60 * 60 * 1000);
|
|
818
|
+
const rollbackable = this.auditLog.filter(a => !a.rolledBack && a.previousState !== null && now - a.appliedAt < this.config.rollbackWindowMs);
|
|
819
|
+
return {
|
|
820
|
+
config: { ...this.config },
|
|
821
|
+
running: this.timer !== null,
|
|
822
|
+
lastTickAt: this.lastTickAt,
|
|
823
|
+
lastDigestAt: this.lastDigestAt,
|
|
824
|
+
tickCount: this.tickCount,
|
|
825
|
+
auditLogSize: this.auditLog.length,
|
|
826
|
+
recentActions: recent,
|
|
827
|
+
rollbackableActions: rollbackable,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
getAuditLog(options) {
|
|
831
|
+
let log = this.auditLog;
|
|
832
|
+
if (options?.since) {
|
|
833
|
+
log = log.filter(a => a.appliedAt >= options.since);
|
|
834
|
+
}
|
|
835
|
+
if (options?.kind) {
|
|
836
|
+
log = log.filter(a => a.kind === options.kind);
|
|
837
|
+
}
|
|
838
|
+
// Most recent first
|
|
839
|
+
log = log.slice().sort((a, b) => b.appliedAt - a.appliedAt);
|
|
840
|
+
if (options?.limit) {
|
|
841
|
+
log = log.slice(0, options.limit);
|
|
842
|
+
}
|
|
843
|
+
return log;
|
|
844
|
+
}
|
|
845
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
846
|
+
getTaskLastActivityAt(task) {
|
|
847
|
+
// getEffectiveActivity() has internal DB-availability guards + createdAt fallback
|
|
848
|
+
const signal = getEffectiveActivity(task.id, task.assignee, task.createdAt);
|
|
849
|
+
return signal.effectiveActivityTs;
|
|
850
|
+
}
|
|
851
|
+
isQuietHours(now) {
|
|
852
|
+
const hour = new Date(now).getHours();
|
|
853
|
+
if (this.config.quietHoursStart <= this.config.quietHoursEnd) {
|
|
854
|
+
return hour >= this.config.quietHoursStart && hour < this.config.quietHoursEnd;
|
|
855
|
+
}
|
|
856
|
+
// Wraps midnight (e.g., 22-6)
|
|
857
|
+
return hour >= this.config.quietHoursStart || hour < this.config.quietHoursEnd;
|
|
858
|
+
}
|
|
859
|
+
// ── Cleanup ───────────────────────────────────────────────────────────
|
|
860
|
+
/** Prune audit log entries older than 7 days */
|
|
861
|
+
pruneAuditLog(maxAgeDays = 7) {
|
|
862
|
+
const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
863
|
+
const before = this.auditLog.length;
|
|
864
|
+
this.auditLog = this.auditLog.filter(a => a.appliedAt >= cutoff);
|
|
865
|
+
return before - this.auditLog.length;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
// ── Singleton ────────────────────────────────────────────────────────────
|
|
869
|
+
export const boardHealthWorker = new BoardHealthWorker({
|
|
870
|
+
enabled: process.env.BOARD_HEALTH_ENABLED !== 'false',
|
|
871
|
+
intervalMs: Number(process.env.BOARD_HEALTH_INTERVAL_MS || 5 * 60 * 1000),
|
|
872
|
+
staleDoingThresholdMin: Number(process.env.BOARD_HEALTH_STALE_DOING_MIN || 240),
|
|
873
|
+
suggestCloseThresholdMin: Number(process.env.BOARD_HEALTH_SUGGEST_CLOSE_MIN || 1440),
|
|
874
|
+
rollbackWindowMs: Number(process.env.BOARD_HEALTH_ROLLBACK_WINDOW_MS || 60 * 60 * 1000),
|
|
875
|
+
digestIntervalMs: Number(process.env.BOARD_HEALTH_DIGEST_INTERVAL_MS || 4 * 60 * 60 * 1000),
|
|
876
|
+
digestChannel: process.env.BOARD_HEALTH_DIGEST_CHANNEL || 'ops',
|
|
877
|
+
quietHoursStart: Number(process.env.BOARD_HEALTH_QUIET_START || 0),
|
|
878
|
+
quietHoursEnd: Number(process.env.BOARD_HEALTH_QUIET_END || 6),
|
|
879
|
+
dryRun: process.env.BOARD_HEALTH_DRY_RUN === 'true',
|
|
880
|
+
maxActionsPerTick: Number(process.env.BOARD_HEALTH_MAX_ACTIONS || 5),
|
|
881
|
+
});
|
|
882
|
+
//# sourceMappingURL=boardHealthWorker.js.map
|