Skip to content

fix(routing): make route_state authoritative + aligned with OpenLoops drain eligibility#37

Merged
andrei-hasna merged 2 commits into
mainfrom
chief-route-state-authoritative
Jul 5, 2026
Merged

fix(routing): make route_state authoritative + aligned with OpenLoops drain eligibility#37
andrei-hasna merged 2 commits into
mainfrom
chief-route-state-authoritative

Conversation

@andrei-hasna

@andrei-hasna andrei-hasna commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Problem (audit lane 6)

Todos route_state and OpenLoops drain eligibility disagreed: getTaskRouteState reported eligible:false / route_not_enabled for tasks the OpenLoops todos-task drain would happily route — i.e. auto:route-tagged, pending, unblocked tasks that lacked a separate route_enabled metadata flag the drain never inspects. That black-holed real work. Concrete audited examples: repo-pr-merge-queue task 4bae59b7, open-loops task c53c303b (both auto:route, no explicit route_enabled).

Root cause

The drain (@hasna/loops 0.4.10) admits work from todos ready gated on the auto:route tag. route_state instead required metadata.route_enabled === true and treated the tag as mere "intent, not authorization". Two different opt-in mechanisms → two different answers.

Fix — compute eligibility ONE 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, even with the tag (kill switch preserved).
  • no_auto / manual / manual_required / approval gates still deny even with the tag (an auto:route+no-auto task 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 (throttled is OpenLoops-owned, never emitted by Todos — shared vocabulary only).
  • evidence: owner/assignee/lock, current run + workflow-invocation pointers, stale flag (surfaces stale in_progress ownership), and project-root verification result.
  • gates.missing_project_root (opt-in via verifyProjectRoot) surfaces invalid project roots before route admission; task route-state --verify-project-root exposes it.
  • gates.workflow_pointer_active / gates.workflow_pointer_terminal distinguish 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/pointers shapes are preserved (new keys only). The 0.4.10 consumer reads todos ready + the tag and is unaffected; a future 0.4.12 can consume route_class/evidence directly. 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.ts21 pass / 0 fail (added 6 tests; the one flipped assertion is the audited disagreement).
  • tsc --noEmit clean.
  • cli.test.ts route/ready/source-discovery cases pass.
  • E2E CLI smoke: an auto:route task with no route_enabled now returns eligible:true, route_class:eligible, reasons:[] (was eligible:false, route_not_enabled).

Reviewer focus (adversarial)

  1. Is flipping "tag = authorization" safe? (explicit route_enabled:false + all deny gates still win; only unset→tag opt-in changed.)
  2. Any 0.4.10 consumer path that does a strict-shape/toEqual on route_state and would break on added keys?
  3. route_class precedence 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:route tasks (fixes drain disagreement); additive API fields are low risk, but any consumer that assumed tags were non-authorizing may need to update expectations.

Overview
getTaskRouteState now matches how the OpenLoops drain admits work: when route_enabled is unset on the task and task list, auto:route / route:enabled tags count as authorization (explicit route_enabled: false and existing deny gates still win). The no-auto / noauto / no:auto tags 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 new gates for missing project root and active vs terminal workflow pointers. Optional verifyProjectRoot mirrors filesystem admission; task route-state --verify-project-root and 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.

…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.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ 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.

Comment thread src/lib/task-routing.ts
pointers.current_workflow_invocation_id || pointers.current_run_id || workflowState,
);
const workflowPointerTerminal = hasWorkflowPointer && TERMINAL_WORKFLOW_STATES.has(workflowState);
const workflowPointerActive = hasWorkflowPointer && !workflowPointerTerminal;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bb8d775. Configure here.

@andrei-hasna
andrei-hasna merged commit 285d727 into main Jul 5, 2026
2 of 3 checks passed
@andrei-hasna

Copy link
Copy Markdown
Contributor Author

Post-merge worker review for todos task 958cba66-7a42-47ab-b4c9-e00ede97bb3e:

  • PR fix(routing): make route_state authoritative + aligned with OpenLoops drain eligibility #37 was already merged as 285d727 on 2026-07-05.
  • The PR test check was red due to three src/lib/event-hooks.test.ts cleanup failures, not the routing test files. I reproduced this locally as EROFS removing /home/hasna/.cache/todos--hooks-home- and created follow-up todos task 5d1fe7b8 for that unrelated CI harness issue.
  • Independent adversarial review found two route_state issues: workflow_state-only metadata could classify as deduped_active without an actual run/invocation id, and drain-facing source discovery did not verify project roots by default.
  • I pushed corrective branch openloops/open-todos/958cba66-7a42-47ab-b4c9-e00ede97bb3e-1ff40b81 and opened follow-up PR fix(routing): harden authoritative route state #39 with commit aca2156.

Validation for PR #39:

  • bun test src/lib/task-routing.test.ts src/lib/task-route-sources.test.ts
  • bun test src/cli/cli.test.ts --test-name-pattern "route state|source discovery JSON"
  • bun run typecheck
  • git diff --check
  • staged secrets scan: no hits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant