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
package/dist/tasks.js
ADDED
|
@@ -0,0 +1,1281 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) Reflectt AI
|
|
3
|
+
import { promises as fs } from 'fs';
|
|
4
|
+
import { join } from 'path';
|
|
5
|
+
import { eventBus } from './events.js';
|
|
6
|
+
import { DATA_DIR, LEGACY_DATA_DIR } from './config.js';
|
|
7
|
+
import { createTaskStateAdapterFromEnv } from './taskStateSync.js';
|
|
8
|
+
import { getDb, importJsonlIfNeeded, safeJsonStringify, safeJsonParse } from './db.js';
|
|
9
|
+
import { isTestHarnessTask, TEST_TASK_EXCLUDE_SQL } from './test-task-filter.js';
|
|
10
|
+
import { assertDuplicateClosureHasCanonicalRefs } from './duplicateClosureGuard.js';
|
|
11
|
+
import { getAgentAliases } from './assignment.js';
|
|
12
|
+
const TASKS_FILE = join(DATA_DIR, 'tasks.jsonl');
|
|
13
|
+
const LEGACY_TASKS_FILE = join(LEGACY_DATA_DIR, 'tasks.jsonl');
|
|
14
|
+
const RECURRING_TASKS_FILE = join(DATA_DIR, 'tasks.recurring.jsonl');
|
|
15
|
+
const TASK_HISTORY_FILE = join(DATA_DIR, 'tasks.history.jsonl');
|
|
16
|
+
const TASK_COMMENTS_FILE = join(DATA_DIR, 'tasks.comments.jsonl');
|
|
17
|
+
/**
|
|
18
|
+
* Import functions for one-time JSONL → SQLite migration
|
|
19
|
+
*/
|
|
20
|
+
function importTasks(db, records) {
|
|
21
|
+
const insert = db.prepare(`
|
|
22
|
+
INSERT OR REPLACE INTO tasks (
|
|
23
|
+
id, title, description, status, assignee, reviewer, done_criteria,
|
|
24
|
+
created_by, created_at, updated_at, priority, blocked_by, epic_id,
|
|
25
|
+
tags, metadata, team_id, comment_count
|
|
26
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
27
|
+
`);
|
|
28
|
+
const insertMany = db.transaction((tasks) => {
|
|
29
|
+
for (const record of tasks) {
|
|
30
|
+
const task = record;
|
|
31
|
+
insert.run(task.id, task.title, task.description ?? null, task.status, task.assignee ?? null, task.reviewer ?? null, safeJsonStringify(task.done_criteria), task.createdBy, task.createdAt, task.updatedAt, task.priority ?? null, safeJsonStringify(task.blocked_by), task.epic_id ?? null, safeJsonStringify(task.tags), safeJsonStringify(task.metadata), task.teamId ?? null, 0 // comment_count will be recalculated when comments are imported
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
insertMany(records);
|
|
36
|
+
return records.length;
|
|
37
|
+
}
|
|
38
|
+
function importRecurringTasks(db, records) {
|
|
39
|
+
const insert = db.prepare(`
|
|
40
|
+
INSERT OR REPLACE INTO recurring_tasks (
|
|
41
|
+
id, title, description, assignee, reviewer, done_criteria, created_by,
|
|
42
|
+
priority, blocked_by, epic_id, tags, metadata, schedule, enabled,
|
|
43
|
+
status, last_run_at, last_skip_at, last_skip_reason, next_run_at,
|
|
44
|
+
created_at, updated_at
|
|
45
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
46
|
+
`);
|
|
47
|
+
const insertMany = db.transaction((tasks) => {
|
|
48
|
+
for (const record of tasks) {
|
|
49
|
+
const rt = record;
|
|
50
|
+
insert.run(rt.id, rt.title, rt.description ?? null, rt.assignee ?? null, rt.reviewer ?? null, safeJsonStringify(rt.done_criteria), rt.createdBy, rt.priority ?? null, safeJsonStringify(rt.blocked_by), rt.epic_id ?? null, safeJsonStringify(rt.tags), safeJsonStringify(rt.metadata), safeJsonStringify(rt.schedule), rt.enabled ? 1 : 0, rt.status ?? 'todo', rt.lastRunAt ?? null, rt.lastSkipAt ?? null, rt.lastSkipReason ?? null, rt.nextRunAt, rt.createdAt, rt.updatedAt);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
insertMany(records);
|
|
54
|
+
return records.length;
|
|
55
|
+
}
|
|
56
|
+
function importTaskHistory(db, records) {
|
|
57
|
+
const insert = db.prepare(`
|
|
58
|
+
INSERT OR REPLACE INTO task_history (
|
|
59
|
+
id, task_id, type, actor, timestamp, data
|
|
60
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
61
|
+
`);
|
|
62
|
+
const insertMany = db.transaction((events) => {
|
|
63
|
+
for (const record of events) {
|
|
64
|
+
const event = record;
|
|
65
|
+
insert.run(event.id, event.taskId, event.type, event.actor, event.timestamp, safeJsonStringify(event.data));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
insertMany(records);
|
|
69
|
+
return records.length;
|
|
70
|
+
}
|
|
71
|
+
function importTaskComments(db, records) {
|
|
72
|
+
const insert = db.prepare(`
|
|
73
|
+
INSERT OR REPLACE INTO task_comments (
|
|
74
|
+
id, task_id, author, content, timestamp,
|
|
75
|
+
category, suppressed, suppressed_reason, suppressed_rule
|
|
76
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
77
|
+
`);
|
|
78
|
+
const updateCommentCount = db.prepare(`
|
|
79
|
+
UPDATE tasks
|
|
80
|
+
SET comment_count = (SELECT COUNT(*) FROM task_comments WHERE task_id = ?)
|
|
81
|
+
WHERE id = ?
|
|
82
|
+
`);
|
|
83
|
+
const insertMany = db.transaction((comments) => {
|
|
84
|
+
const taskIds = new Set();
|
|
85
|
+
for (const record of comments) {
|
|
86
|
+
const comment = record;
|
|
87
|
+
insert.run(comment.id, comment.taskId, comment.author, comment.content, comment.timestamp, comment.category ?? null, comment.suppressed ? 1 : 0, comment.suppressedReason ?? null, comment.suppressedRule ?? null);
|
|
88
|
+
taskIds.add(comment.taskId);
|
|
89
|
+
}
|
|
90
|
+
// Update comment counts for all affected tasks
|
|
91
|
+
for (const taskId of taskIds) {
|
|
92
|
+
updateCommentCount.run(taskId, taskId);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
insertMany(records);
|
|
96
|
+
return records.length;
|
|
97
|
+
}
|
|
98
|
+
function rowToTask(row) {
|
|
99
|
+
return {
|
|
100
|
+
id: row.id,
|
|
101
|
+
title: row.title,
|
|
102
|
+
description: row.description ?? undefined,
|
|
103
|
+
status: row.status,
|
|
104
|
+
assignee: row.assignee ?? undefined,
|
|
105
|
+
reviewer: row.reviewer ?? undefined,
|
|
106
|
+
done_criteria: safeJsonParse(row.done_criteria),
|
|
107
|
+
createdBy: row.created_by,
|
|
108
|
+
createdAt: row.created_at,
|
|
109
|
+
updatedAt: row.updated_at,
|
|
110
|
+
priority: row.priority ?? undefined,
|
|
111
|
+
blocked_by: safeJsonParse(row.blocked_by),
|
|
112
|
+
epic_id: row.epic_id ?? undefined,
|
|
113
|
+
tags: safeJsonParse(row.tags),
|
|
114
|
+
metadata: safeJsonParse(row.metadata),
|
|
115
|
+
teamId: row.team_id ?? undefined,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Query all tasks from SQLite */
|
|
119
|
+
function queryAllTasks() {
|
|
120
|
+
const db = getDb();
|
|
121
|
+
const rows = db.prepare('SELECT * FROM tasks').all();
|
|
122
|
+
return rows.map(rowToTask);
|
|
123
|
+
}
|
|
124
|
+
/** Query single task by ID from SQLite */
|
|
125
|
+
function queryTask(id) {
|
|
126
|
+
const db = getDb();
|
|
127
|
+
const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
|
|
128
|
+
return row ? rowToTask(row) : undefined;
|
|
129
|
+
}
|
|
130
|
+
/** Check if task exists in SQLite */
|
|
131
|
+
function taskExists(id) {
|
|
132
|
+
const db = getDb();
|
|
133
|
+
const row = db.prepare('SELECT 1 FROM tasks WHERE id = ? LIMIT 1').get(id);
|
|
134
|
+
return !!row;
|
|
135
|
+
}
|
|
136
|
+
/** Get count of tasks */
|
|
137
|
+
function taskCount() {
|
|
138
|
+
const db = getDb();
|
|
139
|
+
const row = db.prepare('SELECT COUNT(*) as count FROM tasks').get();
|
|
140
|
+
return row.count;
|
|
141
|
+
}
|
|
142
|
+
class TaskManager {
|
|
143
|
+
// No in-memory tasks Map — all reads go to SQLite
|
|
144
|
+
subscribers = new Set();
|
|
145
|
+
recurringTasks = new Map();
|
|
146
|
+
// taskHistory and taskComments are queried from SQLite on demand — no in-memory cache
|
|
147
|
+
initialized = false;
|
|
148
|
+
recurringInitialized = false;
|
|
149
|
+
recurringTicker;
|
|
150
|
+
taskStateAdapter = createTaskStateAdapterFromEnv();
|
|
151
|
+
isCanonicalArtifactPath(path) {
|
|
152
|
+
const normalized = path.trim();
|
|
153
|
+
if (normalized.length === 0)
|
|
154
|
+
return false;
|
|
155
|
+
if (normalized.startsWith('/') || normalized.startsWith('~'))
|
|
156
|
+
return false;
|
|
157
|
+
if (normalized.includes('\\'))
|
|
158
|
+
return false;
|
|
159
|
+
if (normalized.includes('..'))
|
|
160
|
+
return false;
|
|
161
|
+
return normalized.startsWith('process/');
|
|
162
|
+
}
|
|
163
|
+
validateLifecycleGates(task) {
|
|
164
|
+
if (task.status === 'todo')
|
|
165
|
+
return;
|
|
166
|
+
const hasReviewer = Boolean(task.reviewer && task.reviewer.trim().length > 0);
|
|
167
|
+
const hasDoneCriteria = Boolean(task.done_criteria && task.done_criteria.length > 0);
|
|
168
|
+
const eta = task.metadata?.eta;
|
|
169
|
+
const hasEta = typeof eta === 'string' && eta.trim().length > 0;
|
|
170
|
+
const artifactPath = task.metadata?.artifact_path;
|
|
171
|
+
const hasArtifactPath = typeof artifactPath === 'string' && artifactPath.trim().length > 0;
|
|
172
|
+
// Duplicate-closure contract (server-side): never allow a task to be closed as a
|
|
173
|
+
// "duplicate" without canonical refs (duplicate_of + canonical_pr + canonical_commit).
|
|
174
|
+
// Auto-close writers can bypass interactive precheck flows, so this belongs here.
|
|
175
|
+
if (task.status === 'done') {
|
|
176
|
+
assertDuplicateClosureHasCanonicalRefs(task.metadata);
|
|
177
|
+
}
|
|
178
|
+
if (!hasDoneCriteria) {
|
|
179
|
+
throw new Error('Lifecycle gate: done_criteria is required before starting task work');
|
|
180
|
+
}
|
|
181
|
+
if (!hasReviewer) {
|
|
182
|
+
throw new Error('Lifecycle gate: reviewer is required before starting task work');
|
|
183
|
+
}
|
|
184
|
+
if (task.status === 'doing' && !hasEta) {
|
|
185
|
+
throw new Error('Status contract: doing requires metadata.eta');
|
|
186
|
+
}
|
|
187
|
+
if (task.status === 'validating' && !hasArtifactPath) {
|
|
188
|
+
throw new Error('Status contract: validating requires metadata.artifact_path');
|
|
189
|
+
}
|
|
190
|
+
if (task.status === 'validating' && hasArtifactPath && !this.isCanonicalArtifactPath(artifactPath)) {
|
|
191
|
+
throw new Error('Status contract: validating requires metadata.artifact_path under process/ (repo-relative, workspace-agnostic)');
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
constructor() {
|
|
195
|
+
this.loadTasks()
|
|
196
|
+
.then(() => this.importLegacyHistory())
|
|
197
|
+
.then(() => this.importLegacyComments())
|
|
198
|
+
.then(() => this.loadRecurringTasks())
|
|
199
|
+
.then(() => this.materializeDueRecurringTasks())
|
|
200
|
+
.catch(err => {
|
|
201
|
+
console.error('[Tasks] Failed to load tasks:', err);
|
|
202
|
+
});
|
|
203
|
+
this.recurringTicker = setInterval(() => {
|
|
204
|
+
this.materializeDueRecurringTasks().catch(err => {
|
|
205
|
+
console.error('[Tasks] Recurring materialization failed:', err);
|
|
206
|
+
});
|
|
207
|
+
}, 60_000);
|
|
208
|
+
this.recurringTicker.unref();
|
|
209
|
+
}
|
|
210
|
+
async loadTasks() {
|
|
211
|
+
try {
|
|
212
|
+
// Ensure data directory exists
|
|
213
|
+
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
214
|
+
const db = getDb();
|
|
215
|
+
// Import JSONL → SQLite if needed (one-time migration)
|
|
216
|
+
importJsonlIfNeeded(db, TASKS_FILE, 'tasks', importTasks);
|
|
217
|
+
// Also check legacy location for migration
|
|
218
|
+
importJsonlIfNeeded(db, LEGACY_TASKS_FILE, 'tasks', importTasks);
|
|
219
|
+
// All tasks queried from SQLite on demand — no in-memory Map
|
|
220
|
+
const count = taskCount();
|
|
221
|
+
console.log(`[Tasks] SQLite has ${count} tasks (all queries go to DB)`);
|
|
222
|
+
// Cloud hydration if empty
|
|
223
|
+
if (count === 0 && this.taskStateAdapter) {
|
|
224
|
+
try {
|
|
225
|
+
const remoteTasks = await this.taskStateAdapter.pullTasks();
|
|
226
|
+
for (const task of remoteTasks) {
|
|
227
|
+
this.writeTaskToDb(task);
|
|
228
|
+
}
|
|
229
|
+
if (remoteTasks.length > 0) {
|
|
230
|
+
console.log(`[Tasks] Hydrated ${remoteTasks.length} tasks from cloud state`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
console.error('[Tasks] Failed to hydrate tasks from cloud state:', err);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
this.initialized = true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
normalizeRecurringTask(recurring) {
|
|
243
|
+
return {
|
|
244
|
+
...recurring,
|
|
245
|
+
enabled: typeof recurring.enabled === 'boolean' ? recurring.enabled : true,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
async loadRecurringTasks() {
|
|
249
|
+
try {
|
|
250
|
+
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
251
|
+
const db = getDb();
|
|
252
|
+
// Import JSONL → SQLite if needed
|
|
253
|
+
importJsonlIfNeeded(db, RECURRING_TASKS_FILE, 'recurring_tasks', importRecurringTasks);
|
|
254
|
+
// Load from SQLite into in-memory Map
|
|
255
|
+
const rows = db.prepare('SELECT * FROM recurring_tasks').all();
|
|
256
|
+
for (const row of rows) {
|
|
257
|
+
const recurring = {
|
|
258
|
+
id: row.id,
|
|
259
|
+
title: row.title,
|
|
260
|
+
description: row.description ?? undefined,
|
|
261
|
+
assignee: row.assignee ?? undefined,
|
|
262
|
+
reviewer: row.reviewer ?? undefined,
|
|
263
|
+
done_criteria: safeJsonParse(row.done_criteria),
|
|
264
|
+
createdBy: row.created_by,
|
|
265
|
+
priority: row.priority ?? undefined,
|
|
266
|
+
blocked_by: safeJsonParse(row.blocked_by),
|
|
267
|
+
epic_id: row.epic_id ?? undefined,
|
|
268
|
+
tags: safeJsonParse(row.tags),
|
|
269
|
+
metadata: safeJsonParse(row.metadata),
|
|
270
|
+
schedule: safeJsonParse(row.schedule),
|
|
271
|
+
enabled: Boolean(row.enabled),
|
|
272
|
+
status: row.status ?? undefined,
|
|
273
|
+
lastRunAt: row.last_run_at ?? undefined,
|
|
274
|
+
lastSkipAt: row.last_skip_at ?? undefined,
|
|
275
|
+
lastSkipReason: row.last_skip_reason ?? undefined,
|
|
276
|
+
nextRunAt: row.next_run_at,
|
|
277
|
+
createdAt: row.created_at,
|
|
278
|
+
updatedAt: row.updated_at,
|
|
279
|
+
};
|
|
280
|
+
this.recurringTasks.set(recurring.id, this.normalizeRecurringTask(recurring));
|
|
281
|
+
}
|
|
282
|
+
console.log(`[Tasks] Loaded ${this.recurringTasks.size} recurring task definitions from SQLite`);
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
this.recurringInitialized = true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/** One-time JSONL → SQLite import for task history (no in-memory loading) */
|
|
289
|
+
async importLegacyHistory() {
|
|
290
|
+
try {
|
|
291
|
+
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
292
|
+
const db = getDb();
|
|
293
|
+
importJsonlIfNeeded(db, TASK_HISTORY_FILE, 'task_history', importTaskHistory);
|
|
294
|
+
const countRow = db.prepare('SELECT COUNT(*) as count FROM task_history').get();
|
|
295
|
+
console.log(`[Tasks] SQLite has ${countRow.count} task history events (queried on demand)`);
|
|
296
|
+
}
|
|
297
|
+
catch (err) {
|
|
298
|
+
console.error('[Tasks] Failed to import task history:', err);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/** One-time JSONL → SQLite import for task comments (no in-memory loading) */
|
|
302
|
+
async importLegacyComments() {
|
|
303
|
+
try {
|
|
304
|
+
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
305
|
+
const db = getDb();
|
|
306
|
+
importJsonlIfNeeded(db, TASK_COMMENTS_FILE, 'task_comments', importTaskComments);
|
|
307
|
+
const countRow = db.prepare('SELECT COUNT(*) as count FROM task_comments').get();
|
|
308
|
+
console.log(`[Tasks] SQLite has ${countRow.count} task comments (queried on demand)`);
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
console.error('[Tasks] Failed to import task comments:', err);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async appendTaskHistory(event) {
|
|
315
|
+
try {
|
|
316
|
+
const db = getDb();
|
|
317
|
+
// Write to SQLite (primary)
|
|
318
|
+
const insert = db.prepare(`
|
|
319
|
+
INSERT INTO task_history (id, task_id, type, actor, timestamp, data)
|
|
320
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
321
|
+
`);
|
|
322
|
+
insert.run(event.id, event.taskId, event.type, event.actor, event.timestamp, safeJsonStringify(event.data));
|
|
323
|
+
// Append to JSONL (audit log)
|
|
324
|
+
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
325
|
+
await fs.appendFile(TASK_HISTORY_FILE, `${JSON.stringify(event)}\n`, 'utf-8');
|
|
326
|
+
}
|
|
327
|
+
catch (err) {
|
|
328
|
+
console.error('[Tasks] Failed to append task history:', err);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
async recordTaskHistoryEvent(taskId, type, actor, data) {
|
|
332
|
+
const event = {
|
|
333
|
+
id: `thevt-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
334
|
+
taskId,
|
|
335
|
+
type,
|
|
336
|
+
actor,
|
|
337
|
+
timestamp: Date.now(),
|
|
338
|
+
data,
|
|
339
|
+
};
|
|
340
|
+
await this.appendTaskHistory(event);
|
|
341
|
+
}
|
|
342
|
+
async appendTaskComment(comment) {
|
|
343
|
+
try {
|
|
344
|
+
const db = getDb();
|
|
345
|
+
// Write to SQLite (primary)
|
|
346
|
+
const insert = db.prepare(`
|
|
347
|
+
INSERT INTO task_comments (
|
|
348
|
+
id, task_id, author, content, timestamp,
|
|
349
|
+
category, suppressed, suppressed_reason, suppressed_rule
|
|
350
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
351
|
+
`);
|
|
352
|
+
insert.run(comment.id, comment.taskId, comment.author, comment.content, comment.timestamp, comment.category ?? null, comment.suppressed ? 1 : 0, comment.suppressedReason ?? null, comment.suppressedRule ?? null);
|
|
353
|
+
// Update comment count + updated_at for the task.
|
|
354
|
+
// Comments are material activity and should advance updated_at to avoid autonomy/heartbeat false positives.
|
|
355
|
+
const updateTask = db.prepare(`
|
|
356
|
+
UPDATE tasks
|
|
357
|
+
SET
|
|
358
|
+
comment_count = (SELECT COUNT(*) FROM task_comments WHERE task_id = ?),
|
|
359
|
+
updated_at = ?
|
|
360
|
+
WHERE id = ?
|
|
361
|
+
`);
|
|
362
|
+
updateTask.run(comment.taskId, comment.timestamp, comment.taskId);
|
|
363
|
+
// Touch task updated_at so comment activity counts as task activity (autonomy, SLA, sorting)
|
|
364
|
+
const touchUpdatedAt = db.prepare(`
|
|
365
|
+
UPDATE tasks
|
|
366
|
+
SET updated_at = MAX(updated_at, ?)
|
|
367
|
+
WHERE id = ?
|
|
368
|
+
`);
|
|
369
|
+
touchUpdatedAt.run(comment.timestamp, comment.taskId);
|
|
370
|
+
// Append to JSONL (audit log)
|
|
371
|
+
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
372
|
+
await fs.appendFile(TASK_COMMENTS_FILE, `${JSON.stringify(comment)}\n`, 'utf-8');
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
console.error('[Tasks] Failed to append task comment:', err);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
async persistRecurringTasks() {
|
|
379
|
+
try {
|
|
380
|
+
const db = getDb();
|
|
381
|
+
const upsert = db.prepare(`
|
|
382
|
+
INSERT OR REPLACE INTO recurring_tasks (
|
|
383
|
+
id, title, description, assignee, reviewer, done_criteria, created_by,
|
|
384
|
+
priority, blocked_by, epic_id, tags, metadata, schedule, enabled,
|
|
385
|
+
status, last_run_at, last_skip_at, last_skip_reason, next_run_at,
|
|
386
|
+
created_at, updated_at
|
|
387
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
388
|
+
`);
|
|
389
|
+
const upsertAll = db.transaction(() => {
|
|
390
|
+
for (const rt of this.recurringTasks.values()) {
|
|
391
|
+
upsert.run(rt.id, rt.title, rt.description ?? null, rt.assignee ?? null, rt.reviewer ?? null, safeJsonStringify(rt.done_criteria), rt.createdBy, rt.priority ?? null, safeJsonStringify(rt.blocked_by), rt.epic_id ?? null, safeJsonStringify(rt.tags), safeJsonStringify(rt.metadata), safeJsonStringify(rt.schedule), rt.enabled ? 1 : 0, rt.status ?? 'todo', rt.lastRunAt ?? null, rt.lastSkipAt ?? null, rt.lastSkipReason ?? null, rt.nextRunAt, rt.createdAt, rt.updatedAt);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
upsertAll();
|
|
395
|
+
}
|
|
396
|
+
catch (err) {
|
|
397
|
+
console.error('[Tasks] Failed to persist recurring tasks to SQLite:', err);
|
|
398
|
+
}
|
|
399
|
+
// JSONL audit log
|
|
400
|
+
try {
|
|
401
|
+
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
402
|
+
const lines = Array.from(this.recurringTasks.values()).map(task => JSON.stringify({
|
|
403
|
+
...task,
|
|
404
|
+
enabled: Boolean(task.enabled),
|
|
405
|
+
}));
|
|
406
|
+
await fs.writeFile(RECURRING_TASKS_FILE, lines.join('\n') + '\n', 'utf-8');
|
|
407
|
+
}
|
|
408
|
+
catch (err) {
|
|
409
|
+
console.error('[Tasks] Failed to write recurring tasks JSONL audit log:', err);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
computeNextRunAt(schedule, fromMs, createdAt) {
|
|
413
|
+
if (schedule.kind === 'interval') {
|
|
414
|
+
const everyMs = Math.max(60_000, schedule.everyMs);
|
|
415
|
+
const anchor = schedule.anchorAt ?? createdAt;
|
|
416
|
+
if (fromMs < anchor)
|
|
417
|
+
return anchor;
|
|
418
|
+
const periods = Math.floor((fromMs - anchor) / everyMs) + 1;
|
|
419
|
+
return anchor + periods * everyMs;
|
|
420
|
+
}
|
|
421
|
+
const hour = schedule.hour ?? 9;
|
|
422
|
+
const minute = schedule.minute ?? 0;
|
|
423
|
+
const candidate = new Date(fromMs);
|
|
424
|
+
candidate.setSeconds(0, 0);
|
|
425
|
+
candidate.setHours(hour, minute, 0, 0);
|
|
426
|
+
let dayDelta = schedule.dayOfWeek - candidate.getDay();
|
|
427
|
+
if (dayDelta < 0 || (dayDelta === 0 && candidate.getTime() <= fromMs)) {
|
|
428
|
+
dayDelta += 7;
|
|
429
|
+
}
|
|
430
|
+
candidate.setDate(candidate.getDate() + dayDelta);
|
|
431
|
+
if (candidate.getTime() <= fromMs) {
|
|
432
|
+
candidate.setDate(candidate.getDate() + 7);
|
|
433
|
+
}
|
|
434
|
+
return candidate.getTime();
|
|
435
|
+
}
|
|
436
|
+
hasMaterializedRun(recurringId, scheduledFor) {
|
|
437
|
+
const db = getDb();
|
|
438
|
+
// Use JSON extract to check recurring metadata without loading all tasks
|
|
439
|
+
const row = db.prepare(`
|
|
440
|
+
SELECT 1 FROM tasks
|
|
441
|
+
WHERE json_extract(metadata, '$.recurring.id') = ?
|
|
442
|
+
AND json_extract(metadata, '$.recurring.scheduledFor') = ?
|
|
443
|
+
LIMIT 1
|
|
444
|
+
`).get(recurringId, scheduledFor);
|
|
445
|
+
return !!row;
|
|
446
|
+
}
|
|
447
|
+
getLatestRecurringInstance(recurringId) {
|
|
448
|
+
const db = getDb();
|
|
449
|
+
const row = db.prepare(`
|
|
450
|
+
SELECT * FROM tasks
|
|
451
|
+
WHERE json_extract(metadata, '$.recurring.id') = ?
|
|
452
|
+
ORDER BY created_at DESC
|
|
453
|
+
LIMIT 1
|
|
454
|
+
`).get(recurringId);
|
|
455
|
+
return row ? rowToTask(row) : undefined;
|
|
456
|
+
}
|
|
457
|
+
async materializeDueRecurringTasks(now = Date.now(), options) {
|
|
458
|
+
let created = 0;
|
|
459
|
+
let skipped = 0;
|
|
460
|
+
let recurringChanged = false;
|
|
461
|
+
for (const recurring of this.recurringTasks.values()) {
|
|
462
|
+
if (!recurring.enabled)
|
|
463
|
+
continue;
|
|
464
|
+
let safetyCounter = 0;
|
|
465
|
+
while (recurring.nextRunAt <= now && safetyCounter < 16) {
|
|
466
|
+
const scheduledFor = recurring.nextRunAt;
|
|
467
|
+
const previousInstance = this.getLatestRecurringInstance(recurring.id);
|
|
468
|
+
const shouldSkipForOpenPredecessor = !options?.force &&
|
|
469
|
+
previousInstance !== undefined &&
|
|
470
|
+
previousInstance.status !== 'done';
|
|
471
|
+
if (shouldSkipForOpenPredecessor) {
|
|
472
|
+
const reason = `skip: previous recurring instance still open (${previousInstance.id}, status=${previousInstance.status})`;
|
|
473
|
+
recurring.lastSkipAt = Date.now();
|
|
474
|
+
recurring.lastSkipReason = reason;
|
|
475
|
+
console.log(`[Tasks] Recurring materialization skipped for ${recurring.id}: ${reason}`);
|
|
476
|
+
skipped += 1;
|
|
477
|
+
}
|
|
478
|
+
else if (!this.hasMaterializedRun(recurring.id, scheduledFor)) {
|
|
479
|
+
await this.createTask({
|
|
480
|
+
title: recurring.title,
|
|
481
|
+
description: recurring.description,
|
|
482
|
+
status: recurring.status ?? 'todo',
|
|
483
|
+
assignee: recurring.assignee,
|
|
484
|
+
reviewer: recurring.reviewer,
|
|
485
|
+
done_criteria: recurring.done_criteria,
|
|
486
|
+
createdBy: recurring.createdBy,
|
|
487
|
+
priority: recurring.priority,
|
|
488
|
+
blocked_by: recurring.blocked_by,
|
|
489
|
+
epic_id: recurring.epic_id,
|
|
490
|
+
tags: recurring.tags,
|
|
491
|
+
metadata: {
|
|
492
|
+
...(recurring.metadata || {}),
|
|
493
|
+
recurring: {
|
|
494
|
+
id: recurring.id,
|
|
495
|
+
scheduledFor,
|
|
496
|
+
},
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
created += 1;
|
|
500
|
+
}
|
|
501
|
+
recurring.lastRunAt = scheduledFor;
|
|
502
|
+
recurring.nextRunAt = this.computeNextRunAt(recurring.schedule, scheduledFor, recurring.createdAt);
|
|
503
|
+
recurring.updatedAt = Date.now();
|
|
504
|
+
recurringChanged = true;
|
|
505
|
+
safetyCounter += 1;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (recurringChanged) {
|
|
509
|
+
await this.persistRecurringTasks();
|
|
510
|
+
}
|
|
511
|
+
return { created, skipped };
|
|
512
|
+
}
|
|
513
|
+
/** Write a single task to SQLite + JSONL audit */
|
|
514
|
+
writeTaskToDb(task) {
|
|
515
|
+
try {
|
|
516
|
+
const db = getDb();
|
|
517
|
+
const commentCount = this.getTaskCommentCount(task.id);
|
|
518
|
+
db.prepare(`
|
|
519
|
+
INSERT OR REPLACE INTO tasks (
|
|
520
|
+
id, title, description, status, assignee, reviewer, done_criteria,
|
|
521
|
+
created_by, created_at, updated_at, priority, blocked_by, epic_id,
|
|
522
|
+
tags, metadata, team_id, comment_count
|
|
523
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
524
|
+
`).run(task.id, task.title, task.description ?? null, task.status, task.assignee ?? null, task.reviewer ?? null, safeJsonStringify(task.done_criteria), task.createdBy, task.createdAt, task.updatedAt, task.priority ?? null, safeJsonStringify(task.blocked_by), task.epic_id ?? null, safeJsonStringify(task.tags), safeJsonStringify(task.metadata), task.teamId ?? null, commentCount);
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
console.error(`[Tasks] Failed to write task ${task.id} to SQLite:`, err);
|
|
528
|
+
}
|
|
529
|
+
// JSONL audit (best-effort, async)
|
|
530
|
+
fs.appendFile(TASKS_FILE, JSON.stringify(task) + '\n', 'utf-8').catch(() => { });
|
|
531
|
+
}
|
|
532
|
+
/** Legacy bulk persist — now just writes all tasks from DB to JSONL */
|
|
533
|
+
async persistTasks() {
|
|
534
|
+
try {
|
|
535
|
+
await fs.mkdir(DATA_DIR, { recursive: true });
|
|
536
|
+
const tasks = queryAllTasks();
|
|
537
|
+
const lines = tasks.map(task => JSON.stringify(task));
|
|
538
|
+
await fs.writeFile(TASKS_FILE, lines.join('\n') + '\n', 'utf-8');
|
|
539
|
+
}
|
|
540
|
+
catch (err) {
|
|
541
|
+
console.error('[Tasks] Failed to write JSONL audit log:', err);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
async syncTaskToCloud(task) {
|
|
545
|
+
if (!this.taskStateAdapter)
|
|
546
|
+
return;
|
|
547
|
+
try {
|
|
548
|
+
await this.taskStateAdapter.upsertTask(task);
|
|
549
|
+
}
|
|
550
|
+
catch (err) {
|
|
551
|
+
console.error('[Tasks] Cloud sync upsert failed, continuing with local JSON fallback:', err);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
async syncTaskDeleteToCloud(taskId) {
|
|
555
|
+
if (!this.taskStateAdapter)
|
|
556
|
+
return;
|
|
557
|
+
try {
|
|
558
|
+
await this.taskStateAdapter.deleteTask(taskId);
|
|
559
|
+
}
|
|
560
|
+
catch (err) {
|
|
561
|
+
console.error('[Tasks] Cloud sync delete failed, continuing with local JSON fallback:', err);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async createTask(data) {
|
|
565
|
+
let metadata = data.metadata ? { ...data.metadata } : undefined;
|
|
566
|
+
const metadataTeamId = typeof metadata?.teamId === 'string' && metadata.teamId.trim().length > 0
|
|
567
|
+
? metadata.teamId.trim()
|
|
568
|
+
: undefined;
|
|
569
|
+
const explicitTeamId = typeof data.teamId === 'string' && data.teamId.trim().length > 0
|
|
570
|
+
? data.teamId.trim()
|
|
571
|
+
: undefined;
|
|
572
|
+
const normalizedTeamId = explicitTeamId ?? metadataTeamId;
|
|
573
|
+
if (normalizedTeamId) {
|
|
574
|
+
metadata = {
|
|
575
|
+
...(metadata || {}),
|
|
576
|
+
teamId: normalizedTeamId,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const normalizedData = {
|
|
580
|
+
...data,
|
|
581
|
+
...(normalizedTeamId ? { teamId: normalizedTeamId } : {}),
|
|
582
|
+
metadata,
|
|
583
|
+
};
|
|
584
|
+
this.validateLifecycleGates(normalizedData);
|
|
585
|
+
// Validate blocked_by references
|
|
586
|
+
if (normalizedData.blocked_by && normalizedData.blocked_by.length > 0) {
|
|
587
|
+
for (const blockerId of normalizedData.blocked_by) {
|
|
588
|
+
if (!taskExists(blockerId)) {
|
|
589
|
+
throw new Error(`Invalid blocked_by reference: task ${blockerId} does not exist`);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
const task = {
|
|
594
|
+
...normalizedData,
|
|
595
|
+
id: `task-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
596
|
+
createdAt: Date.now(),
|
|
597
|
+
updatedAt: Date.now(),
|
|
598
|
+
};
|
|
599
|
+
this.writeTaskToDb(task);
|
|
600
|
+
await this.syncTaskToCloud(task);
|
|
601
|
+
await this.recordTaskHistoryEvent(task.id, 'created', task.createdBy, {
|
|
602
|
+
status: task.status,
|
|
603
|
+
assignee: task.assignee ?? null,
|
|
604
|
+
});
|
|
605
|
+
if (task.assignee) {
|
|
606
|
+
await this.recordTaskHistoryEvent(task.id, 'assigned', task.createdBy, {
|
|
607
|
+
from: null,
|
|
608
|
+
to: task.assignee,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
this.notifySubscribers(task, 'created');
|
|
612
|
+
// Emit events to event bus
|
|
613
|
+
eventBus.emitTaskCreated(task);
|
|
614
|
+
if (task.assignee) {
|
|
615
|
+
eventBus.emitTaskAssigned(task);
|
|
616
|
+
}
|
|
617
|
+
return task;
|
|
618
|
+
}
|
|
619
|
+
async createRecurringTask(data) {
|
|
620
|
+
if (data.blocked_by && data.blocked_by.length > 0) {
|
|
621
|
+
for (const blockerId of data.blocked_by) {
|
|
622
|
+
if (!taskExists(blockerId)) {
|
|
623
|
+
throw new Error(`Invalid blocked_by reference: task ${blockerId} does not exist`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
const now = Date.now();
|
|
628
|
+
const recurring = {
|
|
629
|
+
id: `rtask-${now}-${Math.random().toString(36).substr(2, 9)}`,
|
|
630
|
+
title: data.title,
|
|
631
|
+
description: data.description,
|
|
632
|
+
assignee: data.assignee,
|
|
633
|
+
reviewer: data.reviewer,
|
|
634
|
+
done_criteria: data.done_criteria,
|
|
635
|
+
createdBy: data.createdBy,
|
|
636
|
+
priority: data.priority,
|
|
637
|
+
blocked_by: data.blocked_by,
|
|
638
|
+
epic_id: data.epic_id,
|
|
639
|
+
tags: data.tags,
|
|
640
|
+
metadata: data.metadata,
|
|
641
|
+
schedule: data.schedule,
|
|
642
|
+
enabled: data.enabled ?? true,
|
|
643
|
+
status: data.status ?? 'todo',
|
|
644
|
+
nextRunAt: this.computeNextRunAt(data.schedule, now, now),
|
|
645
|
+
createdAt: now,
|
|
646
|
+
updatedAt: now,
|
|
647
|
+
};
|
|
648
|
+
this.recurringTasks.set(recurring.id, recurring);
|
|
649
|
+
await this.persistRecurringTasks();
|
|
650
|
+
await this.materializeDueRecurringTasks();
|
|
651
|
+
return recurring;
|
|
652
|
+
}
|
|
653
|
+
listRecurringTasks(options) {
|
|
654
|
+
let tasks = Array.from(this.recurringTasks.values());
|
|
655
|
+
if (typeof options?.enabled === 'boolean') {
|
|
656
|
+
tasks = tasks.filter(task => task.enabled === options.enabled);
|
|
657
|
+
}
|
|
658
|
+
return tasks.sort((a, b) => a.nextRunAt - b.nextRunAt);
|
|
659
|
+
}
|
|
660
|
+
async updateRecurringTask(id, updates) {
|
|
661
|
+
const recurring = this.recurringTasks.get(id);
|
|
662
|
+
if (!recurring)
|
|
663
|
+
return undefined;
|
|
664
|
+
const next = {
|
|
665
|
+
...recurring,
|
|
666
|
+
...updates,
|
|
667
|
+
updatedAt: Date.now(),
|
|
668
|
+
};
|
|
669
|
+
if (typeof updates.enabled === 'boolean') {
|
|
670
|
+
next.enabled = updates.enabled;
|
|
671
|
+
}
|
|
672
|
+
if (updates.schedule) {
|
|
673
|
+
next.nextRunAt = this.computeNextRunAt(updates.schedule, Date.now(), recurring.createdAt);
|
|
674
|
+
}
|
|
675
|
+
this.recurringTasks.set(id, next);
|
|
676
|
+
await this.persistRecurringTasks();
|
|
677
|
+
return next;
|
|
678
|
+
}
|
|
679
|
+
async deleteRecurringTask(id) {
|
|
680
|
+
const existed = this.recurringTasks.delete(id);
|
|
681
|
+
if (!existed)
|
|
682
|
+
return false;
|
|
683
|
+
await this.persistRecurringTasks();
|
|
684
|
+
return true;
|
|
685
|
+
}
|
|
686
|
+
getTask(id) {
|
|
687
|
+
return queryTask(id);
|
|
688
|
+
}
|
|
689
|
+
resolveTaskId(inputId) {
|
|
690
|
+
const raw = String(inputId || '').trim();
|
|
691
|
+
if (!raw) {
|
|
692
|
+
return { matchType: 'not_found', suggestions: [] };
|
|
693
|
+
}
|
|
694
|
+
const exact = queryTask(raw);
|
|
695
|
+
if (exact) {
|
|
696
|
+
return { task: exact, resolvedId: raw, matchType: 'exact', suggestions: [] };
|
|
697
|
+
}
|
|
698
|
+
const lowerRaw = raw.toLowerCase();
|
|
699
|
+
const db = getDb();
|
|
700
|
+
const allIds = db.prepare('SELECT id FROM tasks').all().map(r => r.id);
|
|
701
|
+
const prefixMatches = allIds.filter(id => id.toLowerCase().startsWith(lowerRaw));
|
|
702
|
+
if (prefixMatches.length === 1) {
|
|
703
|
+
const resolvedId = prefixMatches[0];
|
|
704
|
+
return {
|
|
705
|
+
task: queryTask(resolvedId),
|
|
706
|
+
resolvedId,
|
|
707
|
+
matchType: 'prefix',
|
|
708
|
+
suggestions: [],
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
if (prefixMatches.length > 1) {
|
|
712
|
+
return {
|
|
713
|
+
matchType: 'ambiguous',
|
|
714
|
+
suggestions: prefixMatches.slice(0, 8),
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
const containsMatches = allIds.filter((id) => id.toLowerCase().includes(lowerRaw)).slice(0, 8);
|
|
718
|
+
return {
|
|
719
|
+
matchType: 'not_found',
|
|
720
|
+
suggestions: containsMatches,
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Lightweight metadata-only patch that bypasses lifecycle gates.
|
|
725
|
+
* Used by internal subsystems (sweeper, auto-close) to persist bookkeeping
|
|
726
|
+
* fields without triggering full validation or history events.
|
|
727
|
+
*/
|
|
728
|
+
patchTaskMetadata(id, metadataUpdates) {
|
|
729
|
+
const task = queryTask(id);
|
|
730
|
+
if (!task)
|
|
731
|
+
return false;
|
|
732
|
+
const merged = { ...(task.metadata || {}), ...metadataUpdates };
|
|
733
|
+
const updated = { ...task, metadata: merged, updatedAt: Date.now() };
|
|
734
|
+
this.writeTaskToDb(updated);
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
getTaskHistory(id) {
|
|
738
|
+
const db = getDb();
|
|
739
|
+
const rows = db.prepare('SELECT * FROM task_history WHERE task_id = ? ORDER BY timestamp ASC').all(id);
|
|
740
|
+
return rows.map(row => ({
|
|
741
|
+
id: row.id,
|
|
742
|
+
taskId: row.task_id,
|
|
743
|
+
type: row.type,
|
|
744
|
+
actor: row.actor,
|
|
745
|
+
timestamp: row.timestamp,
|
|
746
|
+
data: safeJsonParse(row.data),
|
|
747
|
+
}));
|
|
748
|
+
}
|
|
749
|
+
getTaskComments(id, options) {
|
|
750
|
+
const db = getDb();
|
|
751
|
+
const includeSuppressed = Boolean(options?.includeSuppressed);
|
|
752
|
+
const sql = includeSuppressed
|
|
753
|
+
? 'SELECT * FROM task_comments WHERE task_id = ? ORDER BY timestamp ASC'
|
|
754
|
+
: 'SELECT * FROM task_comments WHERE task_id = ? AND (suppressed IS NULL OR suppressed = 0) ORDER BY timestamp ASC';
|
|
755
|
+
const rows = db.prepare(sql).all(id);
|
|
756
|
+
return rows.map(row => ({
|
|
757
|
+
id: row.id,
|
|
758
|
+
taskId: row.task_id,
|
|
759
|
+
author: row.author,
|
|
760
|
+
content: row.content,
|
|
761
|
+
timestamp: row.timestamp,
|
|
762
|
+
category: row.category ?? null,
|
|
763
|
+
suppressed: Boolean(row.suppressed),
|
|
764
|
+
suppressedReason: row.suppressed_reason ?? null,
|
|
765
|
+
suppressedRule: row.suppressed_rule ?? null,
|
|
766
|
+
}));
|
|
767
|
+
}
|
|
768
|
+
getTaskCommentCount(id) {
|
|
769
|
+
const db = getDb();
|
|
770
|
+
const row = db.prepare('SELECT COUNT(*) as count FROM task_comments WHERE task_id = ?').get(id);
|
|
771
|
+
return row.count;
|
|
772
|
+
}
|
|
773
|
+
async addTaskComment(taskId, author, content, options) {
|
|
774
|
+
const task = queryTask(taskId);
|
|
775
|
+
if (!task) {
|
|
776
|
+
throw new Error('Task not found');
|
|
777
|
+
}
|
|
778
|
+
const now = Date.now();
|
|
779
|
+
// ── Comms policy enforcement (store always; suppress from default feeds) ──
|
|
780
|
+
const meta = (task.metadata || {});
|
|
781
|
+
const rule = meta?.comms_policy?.rule;
|
|
782
|
+
const extractCategoryFromContent = (raw) => {
|
|
783
|
+
const s = String(raw || '').trim();
|
|
784
|
+
// [restart] ...
|
|
785
|
+
const bracket = s.match(/^\[(restart|rollback_trigger|promote_due_verdict)\]\s*/i);
|
|
786
|
+
if (bracket?.[1])
|
|
787
|
+
return bracket[1].toLowerCase();
|
|
788
|
+
// restart: ...
|
|
789
|
+
const colon = s.match(/^(restart|rollback_trigger|promote_due_verdict)\s*:\s+/i);
|
|
790
|
+
if (colon?.[1])
|
|
791
|
+
return colon[1].toLowerCase();
|
|
792
|
+
// category=restart / category: restart
|
|
793
|
+
const kv = s.match(/^(?:category|cat)\s*[:=]\s*(restart|rollback_trigger|promote_due_verdict)\b/i);
|
|
794
|
+
if (kv?.[1])
|
|
795
|
+
return kv[1].toLowerCase();
|
|
796
|
+
return null;
|
|
797
|
+
};
|
|
798
|
+
let category = (options?.category ? String(options.category).trim() : '') || null;
|
|
799
|
+
if (!category)
|
|
800
|
+
category = extractCategoryFromContent(content);
|
|
801
|
+
let suppressed = false;
|
|
802
|
+
let suppressedReason = null;
|
|
803
|
+
let suppressedRule = null;
|
|
804
|
+
if (rule === 'silent_until_restart_or_promote_due') {
|
|
805
|
+
suppressedRule = rule;
|
|
806
|
+
const allowed = new Set(['restart', 'rollback_trigger', 'promote_due_verdict']);
|
|
807
|
+
const normalized = category ? category.toLowerCase() : null;
|
|
808
|
+
if (!normalized) {
|
|
809
|
+
suppressed = true;
|
|
810
|
+
suppressedReason = 'missing_category';
|
|
811
|
+
}
|
|
812
|
+
else if (!allowed.has(normalized)) {
|
|
813
|
+
suppressed = true;
|
|
814
|
+
suppressedReason = `non_whitelisted_category:${normalized}`;
|
|
815
|
+
}
|
|
816
|
+
category = normalized;
|
|
817
|
+
}
|
|
818
|
+
const comment = {
|
|
819
|
+
id: `tcomment-${now}-${Math.random().toString(36).substr(2, 9)}`,
|
|
820
|
+
taskId,
|
|
821
|
+
author,
|
|
822
|
+
content,
|
|
823
|
+
timestamp: now,
|
|
824
|
+
category,
|
|
825
|
+
suppressed,
|
|
826
|
+
suppressedReason,
|
|
827
|
+
suppressedRule,
|
|
828
|
+
};
|
|
829
|
+
await this.appendTaskComment(comment);
|
|
830
|
+
await this.recordTaskHistoryEvent(taskId, 'commented', author, {
|
|
831
|
+
commentId: comment.id,
|
|
832
|
+
content,
|
|
833
|
+
category,
|
|
834
|
+
suppressed,
|
|
835
|
+
suppressedReason,
|
|
836
|
+
suppressedRule,
|
|
837
|
+
provenance: options?.provenance ?? null,
|
|
838
|
+
});
|
|
839
|
+
return comment;
|
|
840
|
+
}
|
|
841
|
+
/** Tracks last materialization time to debounce recurring task checks */
|
|
842
|
+
lastRecurringMaterializeAt = 0;
|
|
843
|
+
static RECURRING_DEBOUNCE_MS = 60_000; // 1 minute
|
|
844
|
+
listTasks(options) {
|
|
845
|
+
// Debounce recurring task materialization: at most once per minute
|
|
846
|
+
// This was previously called on every listTasks() invocation (~50+ callers),
|
|
847
|
+
// causing significant event loop blocking under load.
|
|
848
|
+
const now = Date.now();
|
|
849
|
+
if (now - this.lastRecurringMaterializeAt >= TaskManager.RECURRING_DEBOUNCE_MS) {
|
|
850
|
+
this.lastRecurringMaterializeAt = now;
|
|
851
|
+
void this.materializeDueRecurringTasks().catch(() => { });
|
|
852
|
+
}
|
|
853
|
+
// Helper: check if a task is blocked by incomplete dependencies
|
|
854
|
+
const isBlocked = (task) => {
|
|
855
|
+
if (!task.blocked_by || task.blocked_by.length === 0)
|
|
856
|
+
return false;
|
|
857
|
+
return task.blocked_by.some(blockerId => {
|
|
858
|
+
const blocker = queryTask(blockerId);
|
|
859
|
+
return blocker && blocker.status !== 'done';
|
|
860
|
+
});
|
|
861
|
+
};
|
|
862
|
+
const db = getDb();
|
|
863
|
+
const conditions = [];
|
|
864
|
+
const params = [];
|
|
865
|
+
if (options?.status) {
|
|
866
|
+
conditions.push('status = ?');
|
|
867
|
+
params.push(options.status);
|
|
868
|
+
}
|
|
869
|
+
const assigneeFilter = options?.assignee || options?.assignedTo;
|
|
870
|
+
const assigneeIn = options?.assigneeIn;
|
|
871
|
+
if (assigneeIn && assigneeIn.length > 0) {
|
|
872
|
+
const normalized = assigneeIn.map(a => String(a || '').trim().toLowerCase()).filter(Boolean);
|
|
873
|
+
if (normalized.length > 0) {
|
|
874
|
+
conditions.push(`LOWER(assignee) IN (${normalized.map(() => '?').join(', ')})`);
|
|
875
|
+
params.push(...normalized);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
else if (assigneeFilter) {
|
|
879
|
+
conditions.push('assignee = ?');
|
|
880
|
+
params.push(assigneeFilter);
|
|
881
|
+
}
|
|
882
|
+
if (options?.createdBy) {
|
|
883
|
+
conditions.push('created_by = ?');
|
|
884
|
+
params.push(options.createdBy);
|
|
885
|
+
}
|
|
886
|
+
if (options?.teamId) {
|
|
887
|
+
conditions.push("(team_id = ? OR json_extract(metadata, '$.teamId') = ?)");
|
|
888
|
+
params.push(options.teamId, options.teamId);
|
|
889
|
+
}
|
|
890
|
+
if (options?.priority) {
|
|
891
|
+
conditions.push('priority = ?');
|
|
892
|
+
params.push(options.priority);
|
|
893
|
+
}
|
|
894
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
895
|
+
const sql = `SELECT * FROM tasks ${where} ORDER BY updated_at DESC`;
|
|
896
|
+
let tasks = db.prepare(sql).all(...params).map(rowToTask);
|
|
897
|
+
// Tag filtering requires JSON parsing (can't easily do in SQL)
|
|
898
|
+
if (options?.tags && options.tags.length > 0) {
|
|
899
|
+
tasks = tasks.filter(t => t.tags && options.tags.some(tag => t.tags.includes(tag)));
|
|
900
|
+
}
|
|
901
|
+
// Filter blocked tasks if requested
|
|
902
|
+
if (options?.includeBlocked === false) {
|
|
903
|
+
tasks = tasks.filter(t => !isBlocked(t));
|
|
904
|
+
}
|
|
905
|
+
// Filter out test-harness-generated tasks by default (shared classifier in test-task-filter.ts)
|
|
906
|
+
if (!options?.includeTest) {
|
|
907
|
+
tasks = tasks.filter(t => !isTestHarnessTask(t));
|
|
908
|
+
}
|
|
909
|
+
return tasks;
|
|
910
|
+
}
|
|
911
|
+
searchTasks(query) {
|
|
912
|
+
const normalized = query.trim().toLowerCase();
|
|
913
|
+
if (!normalized)
|
|
914
|
+
return [];
|
|
915
|
+
const db = getDb();
|
|
916
|
+
const pattern = `%${normalized}%`;
|
|
917
|
+
const rows = db.prepare(`
|
|
918
|
+
SELECT * FROM tasks
|
|
919
|
+
WHERE title LIKE ? COLLATE NOCASE OR description LIKE ? COLLATE NOCASE
|
|
920
|
+
ORDER BY updated_at DESC
|
|
921
|
+
`).all(pattern, pattern);
|
|
922
|
+
return rows.map(rowToTask);
|
|
923
|
+
}
|
|
924
|
+
resolveHistoryActor(task, updates) {
|
|
925
|
+
const metadataActor = updates.metadata?.actor;
|
|
926
|
+
if (typeof metadataActor === 'string' && metadataActor.trim().length > 0) {
|
|
927
|
+
return metadataActor.trim();
|
|
928
|
+
}
|
|
929
|
+
if (typeof updates.assignee === 'string' && updates.assignee.trim().length > 0) {
|
|
930
|
+
return updates.assignee.trim();
|
|
931
|
+
}
|
|
932
|
+
if (task.assignee && task.assignee.trim().length > 0) {
|
|
933
|
+
return task.assignee.trim();
|
|
934
|
+
}
|
|
935
|
+
return task.createdBy;
|
|
936
|
+
}
|
|
937
|
+
parseLaneTransition(updates) {
|
|
938
|
+
const transition = updates.metadata?.transition;
|
|
939
|
+
if (!transition || typeof transition !== 'object' || Array.isArray(transition))
|
|
940
|
+
return undefined;
|
|
941
|
+
return transition;
|
|
942
|
+
}
|
|
943
|
+
applyLaneStateLock(task, updates, actor) {
|
|
944
|
+
const transition = this.parseLaneTransition(updates);
|
|
945
|
+
const nextStatus = updates.status ?? task.status;
|
|
946
|
+
const nextAssignee = updates.assignee ?? task.assignee;
|
|
947
|
+
const statusChanged = nextStatus !== task.status;
|
|
948
|
+
const assigneeChanged = updates.assignee !== undefined && updates.assignee !== task.assignee;
|
|
949
|
+
const requireTransition = (expectedType, requiredFields, contextLabel) => {
|
|
950
|
+
if (!transition) {
|
|
951
|
+
throw new Error(`Lane-state lock: ${contextLabel} requires metadata.transition`);
|
|
952
|
+
}
|
|
953
|
+
const type = transition.type;
|
|
954
|
+
if (type !== expectedType) {
|
|
955
|
+
throw new Error(`Lane-state lock: ${contextLabel} requires metadata.transition.type="${expectedType}"`);
|
|
956
|
+
}
|
|
957
|
+
for (const field of requiredFields) {
|
|
958
|
+
const value = transition[field];
|
|
959
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
960
|
+
throw new Error(`Lane-state lock: ${contextLabel} requires metadata.transition.${field}`);
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
return transition;
|
|
964
|
+
};
|
|
965
|
+
let transitionEvent;
|
|
966
|
+
if (task.status === 'doing' && nextStatus === 'blocked') {
|
|
967
|
+
const parsed = requireTransition('pause', ['reason'], 'doing->blocked transition');
|
|
968
|
+
transitionEvent = {
|
|
969
|
+
type: 'pause',
|
|
970
|
+
reason: parsed.reason,
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
else if (task.status === 'blocked' && nextStatus === 'doing') {
|
|
974
|
+
const parsed = requireTransition('resume', ['reason'], 'blocked->doing transition');
|
|
975
|
+
transitionEvent = {
|
|
976
|
+
type: 'resume',
|
|
977
|
+
reason: parsed.reason,
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
else if (task.status === 'doing' && nextStatus === 'doing' && assigneeChanged) {
|
|
981
|
+
const parsed = requireTransition('handoff', ['handoff_to', 'reason'], 'doing handoff transition');
|
|
982
|
+
if (typeof nextAssignee !== 'string' || nextAssignee.trim().length === 0) {
|
|
983
|
+
throw new Error('Lane-state lock: handoff requires assignee to be set');
|
|
984
|
+
}
|
|
985
|
+
if (String(parsed.handoff_to).trim() !== nextAssignee.trim()) {
|
|
986
|
+
throw new Error('Lane-state lock: metadata.transition.handoff_to must match new assignee');
|
|
987
|
+
}
|
|
988
|
+
transitionEvent = {
|
|
989
|
+
type: 'handoff',
|
|
990
|
+
reason: parsed.reason,
|
|
991
|
+
handoff_to: parsed.handoff_to,
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
if (!transitionEvent) {
|
|
995
|
+
return {};
|
|
996
|
+
}
|
|
997
|
+
const timestamp = Date.now();
|
|
998
|
+
const metadata = {
|
|
999
|
+
...(updates.metadata || {}),
|
|
1000
|
+
lane_state: nextStatus === 'blocked' ? 'paused' : 'active',
|
|
1001
|
+
last_transition: {
|
|
1002
|
+
type: transitionEvent.type,
|
|
1003
|
+
actor,
|
|
1004
|
+
timestamp,
|
|
1005
|
+
from_status: task.status,
|
|
1006
|
+
to_status: nextStatus,
|
|
1007
|
+
from_assignee: task.assignee ?? null,
|
|
1008
|
+
to_assignee: nextAssignee ?? null,
|
|
1009
|
+
reason: transitionEvent.reason ?? null,
|
|
1010
|
+
handoff_to: transitionEvent.handoff_to ?? null,
|
|
1011
|
+
},
|
|
1012
|
+
};
|
|
1013
|
+
updates.metadata = metadata;
|
|
1014
|
+
return {
|
|
1015
|
+
transitionEvent: {
|
|
1016
|
+
...transitionEvent,
|
|
1017
|
+
actor,
|
|
1018
|
+
timestamp,
|
|
1019
|
+
from_status: task.status,
|
|
1020
|
+
to_status: nextStatus,
|
|
1021
|
+
from_assignee: task.assignee ?? null,
|
|
1022
|
+
to_assignee: nextAssignee ?? null,
|
|
1023
|
+
},
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
async updateTask(id, updates) {
|
|
1027
|
+
const task = queryTask(id);
|
|
1028
|
+
if (!task)
|
|
1029
|
+
return undefined;
|
|
1030
|
+
// Validate blocked_by references if being updated
|
|
1031
|
+
if (updates.blocked_by && updates.blocked_by.length > 0) {
|
|
1032
|
+
for (const blockerId of updates.blocked_by) {
|
|
1033
|
+
if (blockerId === id) {
|
|
1034
|
+
throw new Error('Task cannot be blocked by itself');
|
|
1035
|
+
}
|
|
1036
|
+
if (!taskExists(blockerId)) {
|
|
1037
|
+
throw new Error(`Invalid blocked_by reference: task ${blockerId} does not exist`);
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
// Check for circular dependencies
|
|
1041
|
+
// We need to verify that none of the new blockers (or their dependencies) point back to this task
|
|
1042
|
+
const checkCircular = (taskId, visited = new Set()) => {
|
|
1043
|
+
// If we've reached the original task, there's a cycle
|
|
1044
|
+
if (taskId === id)
|
|
1045
|
+
return true;
|
|
1046
|
+
// If we've already visited this node in this path, no cycle (but avoid infinite loops)
|
|
1047
|
+
if (visited.has(taskId))
|
|
1048
|
+
return false;
|
|
1049
|
+
visited.add(taskId);
|
|
1050
|
+
// Get the task and check its dependencies
|
|
1051
|
+
const t = queryTask(taskId);
|
|
1052
|
+
if (!t || !t.blocked_by)
|
|
1053
|
+
return false;
|
|
1054
|
+
// Recursively check each dependency
|
|
1055
|
+
for (const bid of t.blocked_by) {
|
|
1056
|
+
if (checkCircular(bid, new Set(visited)))
|
|
1057
|
+
return true;
|
|
1058
|
+
}
|
|
1059
|
+
return false;
|
|
1060
|
+
};
|
|
1061
|
+
for (const blockerId of updates.blocked_by) {
|
|
1062
|
+
if (checkCircular(blockerId)) {
|
|
1063
|
+
throw new Error('Circular dependency detected in blocked_by chain');
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
const actor = this.resolveHistoryActor(task, updates);
|
|
1068
|
+
const { transitionEvent } = this.applyLaneStateLock(task, updates, actor);
|
|
1069
|
+
const updated = {
|
|
1070
|
+
...task,
|
|
1071
|
+
...updates,
|
|
1072
|
+
updatedAt: Date.now(),
|
|
1073
|
+
};
|
|
1074
|
+
this.validateLifecycleGates(updated);
|
|
1075
|
+
this.writeTaskToDb(updated);
|
|
1076
|
+
await this.syncTaskToCloud(updated);
|
|
1077
|
+
if (updates.assignee !== undefined && updates.assignee !== task.assignee) {
|
|
1078
|
+
await this.recordTaskHistoryEvent(id, 'assigned', actor, {
|
|
1079
|
+
from: task.assignee ?? null,
|
|
1080
|
+
to: updates.assignee ?? null,
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
if (updates.status !== undefined && updates.status !== task.status) {
|
|
1084
|
+
await this.recordTaskHistoryEvent(id, 'status_changed', actor, {
|
|
1085
|
+
from: task.status,
|
|
1086
|
+
to: updates.status,
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
if (transitionEvent) {
|
|
1090
|
+
await this.recordTaskHistoryEvent(id, 'lane_transition', actor, transitionEvent);
|
|
1091
|
+
}
|
|
1092
|
+
this.notifySubscribers(updated, 'updated');
|
|
1093
|
+
// Emit events to event bus
|
|
1094
|
+
eventBus.emitTaskUpdated(updated, updates);
|
|
1095
|
+
// If assignee changed, emit task_assigned
|
|
1096
|
+
if (updates.assignee && updates.assignee !== task.assignee) {
|
|
1097
|
+
eventBus.emitTaskAssigned(updated);
|
|
1098
|
+
}
|
|
1099
|
+
// If task completed, check for unblocked tasks
|
|
1100
|
+
if (updates.status === 'done' && task.status !== 'done') {
|
|
1101
|
+
this.checkUnblockedTasks(id);
|
|
1102
|
+
}
|
|
1103
|
+
return updated;
|
|
1104
|
+
}
|
|
1105
|
+
async deleteTask(id) {
|
|
1106
|
+
const task = queryTask(id);
|
|
1107
|
+
if (!task)
|
|
1108
|
+
return false;
|
|
1109
|
+
// Delete from SQLite
|
|
1110
|
+
try {
|
|
1111
|
+
const db = getDb();
|
|
1112
|
+
db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
|
|
1113
|
+
db.prepare('DELETE FROM task_comments WHERE task_id = ?').run(id);
|
|
1114
|
+
db.prepare('DELETE FROM task_history WHERE task_id = ?').run(id);
|
|
1115
|
+
}
|
|
1116
|
+
catch (err) {
|
|
1117
|
+
console.error(`[Tasks] SQLite delete failed for ${id}:`, err);
|
|
1118
|
+
}
|
|
1119
|
+
await this.syncTaskDeleteToCloud(id);
|
|
1120
|
+
this.notifySubscribers(task, 'deleted');
|
|
1121
|
+
return true;
|
|
1122
|
+
}
|
|
1123
|
+
subscribe(callback) {
|
|
1124
|
+
this.subscribers.add(callback);
|
|
1125
|
+
return () => this.subscribers.delete(callback);
|
|
1126
|
+
}
|
|
1127
|
+
notifySubscribers(task, action) {
|
|
1128
|
+
this.subscribers.forEach(callback => {
|
|
1129
|
+
try {
|
|
1130
|
+
callback(task, action);
|
|
1131
|
+
}
|
|
1132
|
+
catch (err) {
|
|
1133
|
+
console.error('[Tasks] Subscriber error:', err);
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
checkUnblockedTasks(completedTaskId) {
|
|
1138
|
+
// Find all tasks that were blocked by this completed task
|
|
1139
|
+
const unblockedTasks = [];
|
|
1140
|
+
const db = getDb();
|
|
1141
|
+
const blockedRows = db.prepare(`SELECT * FROM tasks WHERE blocked_by LIKE ?`).all(`%${completedTaskId}%`);
|
|
1142
|
+
for (const task of blockedRows.map(rowToTask)) {
|
|
1143
|
+
if (task.blocked_by && task.blocked_by.includes(completedTaskId)) {
|
|
1144
|
+
const stillBlocked = task.blocked_by.some(blockerId => {
|
|
1145
|
+
const blocker = queryTask(blockerId);
|
|
1146
|
+
return blocker && blocker.status !== 'done';
|
|
1147
|
+
});
|
|
1148
|
+
if (!stillBlocked) {
|
|
1149
|
+
unblockedTasks.push(task);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
if (unblockedTasks.length > 0) {
|
|
1154
|
+
console.log(`[Tasks] Task ${completedTaskId} completion unblocked ${unblockedTasks.length} task(s):`, unblockedTasks.map(t => t.id).join(', '));
|
|
1155
|
+
// Emit event for each unblocked task
|
|
1156
|
+
for (const task of unblockedTasks) {
|
|
1157
|
+
eventBus.emit({
|
|
1158
|
+
id: `evt-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
|
1159
|
+
type: 'task_updated',
|
|
1160
|
+
timestamp: Date.now(),
|
|
1161
|
+
data: {
|
|
1162
|
+
...task,
|
|
1163
|
+
unblocked: true,
|
|
1164
|
+
unblockedBy: completedTaskId
|
|
1165
|
+
}
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
getNextTask(agent, opts) {
|
|
1171
|
+
void this.materializeDueRecurringTasks().catch(() => { });
|
|
1172
|
+
// Filter out test-harness-generated tasks (shared classifier in test-task-filter.ts)
|
|
1173
|
+
const shouldExcludeTest = !opts?.includeTest;
|
|
1174
|
+
const filterTestTask = (task) => shouldExcludeTest ? !isTestHarnessTask(task) : true;
|
|
1175
|
+
// Priority order: P0 > P1 > P2 > P3
|
|
1176
|
+
const priorityOrder = {
|
|
1177
|
+
'P0': 0,
|
|
1178
|
+
'P1': 1,
|
|
1179
|
+
'P2': 2,
|
|
1180
|
+
'P3': 3,
|
|
1181
|
+
};
|
|
1182
|
+
// Helper: check if a task is blocked by incomplete dependencies
|
|
1183
|
+
const isBlocked = (task) => {
|
|
1184
|
+
if (!task.blocked_by || task.blocked_by.length === 0)
|
|
1185
|
+
return false;
|
|
1186
|
+
return task.blocked_by.some(blockerId => {
|
|
1187
|
+
const blocker = queryTask(blockerId);
|
|
1188
|
+
return blocker && blocker.status !== 'done';
|
|
1189
|
+
});
|
|
1190
|
+
};
|
|
1191
|
+
const sortByPriority = (a, b) => {
|
|
1192
|
+
const aPriority = priorityOrder[a.priority || 'P3'] ?? 999;
|
|
1193
|
+
const bPriority = priorityOrder[b.priority || 'P3'] ?? 999;
|
|
1194
|
+
if (aPriority !== bPriority)
|
|
1195
|
+
return aPriority - bPriority;
|
|
1196
|
+
return a.createdAt - b.createdAt;
|
|
1197
|
+
};
|
|
1198
|
+
// If agent specified, first return their highest-priority doing task
|
|
1199
|
+
// This ensures agents resume in-progress work before picking up new tasks
|
|
1200
|
+
const db = getDb();
|
|
1201
|
+
if (agent) {
|
|
1202
|
+
const agentNames = getAgentAliases(agent);
|
|
1203
|
+
if (agentNames.length > 0) {
|
|
1204
|
+
const inClause = agentNames.map(() => '?').join(', ');
|
|
1205
|
+
const doingRows = db.prepare(`SELECT * FROM tasks WHERE status = ? AND LOWER(assignee) IN (${inClause})`).all('doing', ...agentNames);
|
|
1206
|
+
const doingTasks = doingRows.map(rowToTask).filter(t => !isBlocked(t) && filterTestTask(t)).sort(sortByPriority);
|
|
1207
|
+
if (doingTasks.length > 0) {
|
|
1208
|
+
return doingTasks[0];
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
// Then check todo tasks: unassigned or assigned to this agent
|
|
1213
|
+
const todoUnassignedRows = db.prepare('SELECT * FROM tasks WHERE status = ? AND assignee IS NULL').all('todo');
|
|
1214
|
+
let tasks = todoUnassignedRows.map(rowToTask).filter(t => !isBlocked(t) && filterTestTask(t));
|
|
1215
|
+
if (agent) {
|
|
1216
|
+
const agentNames = getAgentAliases(agent);
|
|
1217
|
+
if (agentNames.length > 0) {
|
|
1218
|
+
const inClause = agentNames.map(() => '?').join(', ');
|
|
1219
|
+
const agentTodoRows = db.prepare(`SELECT * FROM tasks WHERE status = ? AND LOWER(assignee) IN (${inClause})`).all('todo', ...agentNames);
|
|
1220
|
+
tasks = [...tasks, ...agentTodoRows.map(rowToTask).filter(t => !isBlocked(t) && filterTestTask(t))];
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
if (tasks.length === 0)
|
|
1224
|
+
return undefined;
|
|
1225
|
+
tasks.sort(sortByPriority);
|
|
1226
|
+
return tasks[0];
|
|
1227
|
+
}
|
|
1228
|
+
getLifecycleInstrumentation() {
|
|
1229
|
+
const db = getDb();
|
|
1230
|
+
const activeRows = db.prepare(`SELECT * FROM tasks WHERE status NOT IN ('todo', 'done')`).all();
|
|
1231
|
+
const active = activeRows.map(rowToTask);
|
|
1232
|
+
const missingReviewer = active.filter(t => !t.reviewer || t.reviewer.trim().length === 0);
|
|
1233
|
+
const missingDoneCriteria = active.filter(t => !t.done_criteria || t.done_criteria.length === 0);
|
|
1234
|
+
const missingEtaOnDoing = active.filter(t => {
|
|
1235
|
+
if (t.status !== 'doing')
|
|
1236
|
+
return false;
|
|
1237
|
+
const eta = t.metadata?.eta;
|
|
1238
|
+
return typeof eta !== 'string' || eta.trim().length === 0;
|
|
1239
|
+
});
|
|
1240
|
+
const missingArtifactPathOnValidating = active.filter(t => {
|
|
1241
|
+
if (t.status !== 'validating')
|
|
1242
|
+
return false;
|
|
1243
|
+
const artifactPath = t.metadata?.artifact_path;
|
|
1244
|
+
return typeof artifactPath !== 'string' || artifactPath.trim().length === 0;
|
|
1245
|
+
});
|
|
1246
|
+
return {
|
|
1247
|
+
activeCount: active.length,
|
|
1248
|
+
gateViolations: {
|
|
1249
|
+
missingReviewer: missingReviewer.length,
|
|
1250
|
+
missingDoneCriteria: missingDoneCriteria.length,
|
|
1251
|
+
},
|
|
1252
|
+
statusContractViolations: {
|
|
1253
|
+
missingEtaOnDoing: missingEtaOnDoing.length,
|
|
1254
|
+
missingArtifactPathOnValidating: missingArtifactPathOnValidating.length,
|
|
1255
|
+
},
|
|
1256
|
+
violatingTaskIds: {
|
|
1257
|
+
missingReviewer: missingReviewer.map(t => t.id),
|
|
1258
|
+
missingDoneCriteria: missingDoneCriteria.map(t => t.id),
|
|
1259
|
+
missingEtaOnDoing: missingEtaOnDoing.map(t => t.id),
|
|
1260
|
+
missingArtifactPathOnValidating: missingArtifactPathOnValidating.map(t => t.id),
|
|
1261
|
+
},
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
getStats(options) {
|
|
1265
|
+
const db = getDb();
|
|
1266
|
+
const excludeTest = !options?.includeTest;
|
|
1267
|
+
const whereClause = excludeTest ? `WHERE ${TEST_TASK_EXCLUDE_SQL}` : '';
|
|
1268
|
+
const total = db.prepare(`SELECT COUNT(*) as count FROM tasks ${whereClause}`).get().count;
|
|
1269
|
+
const byStatusRows = db.prepare(`SELECT status, COUNT(*) as count FROM tasks ${whereClause} GROUP BY status`).all();
|
|
1270
|
+
const byStatus = {
|
|
1271
|
+
todo: 0, doing: 0, blocked: 0, validating: 0, done: 0, 'in-progress': 0,
|
|
1272
|
+
};
|
|
1273
|
+
for (const row of byStatusRows) {
|
|
1274
|
+
byStatus[row.status] = row.count;
|
|
1275
|
+
}
|
|
1276
|
+
byStatus['in-progress'] = byStatus.doing; // Backward compatibility
|
|
1277
|
+
return { total, byStatus };
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
export const taskManager = new TaskManager();
|
|
1281
|
+
//# sourceMappingURL=tasks.js.map
|