fix(routing): make route_state authoritative + aligned with OpenLoops drain eligibility#37
Conversation
…ps drain route_state reported eligible:false / route_not_enabled for tasks the OpenLoops task-drain would happily route (auto:route tag, pending, unblocked) because it required a separate route_enabled metadata flag the drain never checks. That disagreement black-holed real work (audit lane 6: repo-pr-merge-queue 4bae59b7, open-loops c53c303b). Compute eligibility one authoritative way, matching the drain: - the auto:route / route:enabled tag is the opt-in when route_enabled is unset; - an explicit route_enabled:false (task or task list) still denies, and no_auto / manual / approval gates still deny even with the tag present. Add fields drains need to classify without recomputing eligibility: - route_class: eligible | deduped_active | terminal_requeue_needed | terminal | in_progress | blocked | locked | unroutable | missing_metadata | throttled - evidence: owner/assignee/lock + run/workflow pointers + stale flag (surfaces stale in_progress ownership) and project-root verification result - gates.missing_project_root (opt-in verifyProjectRoot) surfaces invalid project roots before admission - gates.workflow_pointer_active / _terminal distinguish in-flight dedupe from a requeue past a dead invocation task route-state gains --verify-project-root. Tests cover every audited case. Contract stays additive/back-compatible with @hasna/loops 0.4.10's consumer.
…:route) The fleet gates automation with the no-auto TAG (the drain's invalid-path remediation adds it), not only automation.no_auto metadata. Without honoring the tag, an auto:route+no-auto tagged task would read eligible:true — a silent normal candidate, exactly what the acceptance forbids. gates.no_auto now also fires on the no-auto/noauto/no:auto tag; test covers the tag path.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit bb8d775. Configure here.
| pointers.current_workflow_invocation_id || pointers.current_run_id || workflowState, | ||
| ); | ||
| const workflowPointerTerminal = hasWorkflowPointer && TERMINAL_WORKFLOW_STATES.has(workflowState); | ||
| const workflowPointerActive = hasWorkflowPointer && !workflowPointerTerminal; |
There was a problem hiding this comment.
State alone triggers false dedupe
Medium Severity
The hasWorkflowPointer logic incorrectly flags a task as having an active workflow pointer if only workflow_state is set, even without current_workflow_invocation_id or current_run_id. This results in workflow_pointer_active being true and route_class becoming deduped_active, incorrectly suggesting an in-flight invocation that needs deduplication.
Reviewed by Cursor Bugbot for commit bb8d775. Configure here.
|
Post-merge worker review for todos task 958cba66-7a42-47ab-b4c9-e00ede97bb3e:
Validation for PR #39:
|


Problem (audit lane 6)
Todos
route_stateand OpenLoops drain eligibility disagreed:getTaskRouteStatereportedeligible:false/route_not_enabledfor tasks the OpenLoopstodos-taskdrain would happily route — i.e.auto:route-tagged, pending, unblocked tasks that lacked a separateroute_enabledmetadata flag the drain never inspects. That black-holed real work. Concrete audited examples:repo-pr-merge-queuetask4bae59b7,open-loopstaskc53c303b(bothauto:route, no explicitroute_enabled).Root cause
The drain (
@hasna/loops0.4.10) admits work fromtodos readygated on theauto:routetag.route_stateinstead requiredmetadata.route_enabled === trueand treated the tag as mere "intent, not authorization". Two different opt-in mechanisms → two different answers.Fix — compute eligibility ONE way, matching the drain
auto:route/route:enabledtag is the opt-in whenroute_enabledis unset.route_enabled:false(task or task list) still denies, even with the tag (kill switch preserved).no_auto/manual/manual_required/ approval gates still deny even with the tag (anauto:route+no-autotask is never a silent candidate).Net effect: eligibility only changes in the single direction that makes it agree with what the drain actually does. All prior explicit-deny semantics are unchanged.
Added fields drains need (so eligibility is computed one way, not recomputed)
route_class:eligible | deduped_active | terminal_requeue_needed | terminal | in_progress | blocked | locked | unroutable | missing_metadata | throttled(throttledis OpenLoops-owned, never emitted by Todos — shared vocabulary only).evidence: owner/assignee/lock, current run + workflow-invocation pointers,staleflag (surfaces stalein_progressownership), and project-root verification result.gates.missing_project_root(opt-in viaverifyProjectRoot) surfaces invalid project roots before route admission;task route-state --verify-project-rootexposes it.gates.workflow_pointer_active/gates.workflow_pointer_terminaldistinguish an in-flight dedupe from a requeue past a dead invocation.Compatibility with @hasna/loops 0.4.10
Purely additive — no field removed or renamed;
eligible/reasons/gates/route/pointersshapes are preserved (new keys only). The 0.4.10 consumer readstodos ready+ the tag and is unaffected; a future 0.4.12 can consumeroute_class/evidencedirectly. No open-loops change is required for this to land.Verification
bun test src/lib/task-routing.test.ts src/lib/task-route-sources.test.ts→ 21 pass / 0 fail (added 6 tests; the one flipped assertion is the audited disagreement).tsc --noEmitclean.cli.test.tsroute/ready/source-discovery cases pass.auto:routetask with noroute_enablednow returnseligible:true, route_class:eligible, reasons:[](waseligible:false, route_not_enabled).Reviewer focus (adversarial)
route_enabled:false+ all deny gates still win; only unset→tag opt-in changed.)toEqualonroute_stateand would break on added keys?route_classprecedence correctness (terminal > in_progress > blocked > locked > missing_metadata > unroutable > deduped_active > terminal_requeue_needed > eligible).Data-cleanup (stale in_progress release, invalid paths, auto/no-auto contradictions) is handled separately with an undo record; see task
4e339d57.Note
Medium Risk
Eligibility semantics change for tag-only
auto:routetasks (fixes drain disagreement); additive API fields are low risk, but any consumer that assumed tags were non-authorizing may need to update expectations.Overview
getTaskRouteStatenow matches how the OpenLoops drain admits work: whenroute_enabledis unset on the task and task list,auto:route/route:enabledtags count as authorization (explicitroute_enabled: falseand existing deny gates still win). Theno-auto/noauto/no:autotags are honored as deny signals alongside automation metadata.The response is extended so drains do not re-derive behavior:
route_class(e.g.eligible,deduped_active,terminal_requeue_needed,unroutable,missing_metadata),evidence(owner, workflow pointers, stale in-progress, optional project-root check), and newgatesfor missing project root and active vs terminal workflow pointers. OptionalverifyProjectRootmirrors filesystem admission;task route-state --verify-project-rootand human output for class/owner/stale expose this in the CLI.Reviewed by Cursor Bugbot for commit bb8d775. Bugbot is set up for automated code reviews on this repo. Configure here.