Menu

#257 fix(pm): make task status dynamic across all providers

closed
nobody
released (243)
2026-06-11
2026-06-11
Anonymous
No

Originally created by: Akarsh-Hegde

Problem

GitHub Project sync broke for users whose board columns didn't match our three hardcoded status buckets. Every provider collapsed its native status into todo / in_progress / done — GitHub via substring guessing, Jira/Linear/Azure via a fixed map — and discarded the real column name. Any custom GitHub column the matcher didn't anticipate ("In Review", "QA", "Ready for Deploy", "Shipped") silently became todo, so the product's lanes no longer matched the user's board.

The reported bug is GitHub-specific in practice because GitHub Projects v2 single-select fields expose only the raw column name — there is no semantic category — whereas Jira/Linear/Azure ship a separate, fixed done/closed signal alongside the custom name. (Verified against Atlassian docs: Jira boards do have arbitrary custom columns and statuses; what they additionally provide is the non-customizable statusCategory, which is why our Jira path never had to guess. That category is reliable for new/indeterminate/done but can be undefined on JSM / self-hosted Server/DC — so it isn't infallible either.)

Change

Replace the bucketed status_category with two columns on pm_tasks:

column role
status_raw verbatim provider column/state name, shown to the user as-is
is_terminal the one normalized signal logic needs: is this ticket done/closed?

A new shared resolver (src/intelligence/providers/status.rs) is used by every provider, with precedence:

  1. Env override<PROVIDER>_TERMINAL_STATUSES / <PROVIDER>_OPEN_STATUSES (comma-separated, case-insensitive). The per-board correction path; an override always wins.
  2. Native category — Jira statusCategory, Linear state type, Azure StateCategory. Preserved where reliable; Jira's undefined defers to the heuristic instead of blind-bucketing.
  3. Keyword heuristic on the raw name — for GitHub/Trello, which have no native category.

Downstream wiring

  • Worklog risk flag (ground.rs, hooks.py): the ticket_closed flag now keys off is_terminal, not status == "done". collect.rs + SessionBundle (Rust & Python) carry pm_task_is_terminal.
  • Classifier (run_task_linker_mlx.py): candidate-task query now selects status_raw + is_terminal (the raw name is better prompt context anyway).
  • UI (StatusPill): renders the raw name verbatim, colored by terminality; never collapses unknown columns to "Todo".
  • MCP server: now reads status_raw. (It had been selecting a status column that migration 022 dropped — a latent bug fixed here.)
  • Migration 035: adds the columns, backfills from the old buckets, drops status_category.

Not done (by design, deferred)

  • No pm_status_map table / dashboard editor yet — v1 uses env-var overrides as the correction path.
  • No LLM-assisted mapping — the deterministic resolver covers it.

Verification

  • cargo test — all pass (new resolver unit tests + updated provider/ground tests).
  • cargo clippy --all-targets — clean.
  • cargo fmt --check — clean.
  • UI tsc --noEmit — clean (only pre-existing bun:test import errors remain).
  • MCP dist/ rebuilt.

https://claude.ai/code/session_013mv3epyceLahz9Cue1mtPj


Generated by Claude Code

Related

Tickets: #194
Tickets: #259
Tickets: #262

Discussion

  • Anonymous

    Anonymous - 2026-06-11

    Originally posted by: Akarsh-Hegde

    Review comments addressed + conflicts resolved

    HIGH — build_dataset.py status_category references

    Already fixed in commit 41b8746 (fix(pm): update eval dataset builder for dynamic status) — that commit replaces status_category with status_raw in the SELECT and LOWER(status_category) != 'done' with is_terminal = 0 in the WHERE. Nothing further needed there.


    MEDIUM — Keyword heuristic substring false positives

    Fixed in 92b8901 (fix(status): word-boundary keyword matching + once_cell env cache).

    Switched heuristic_terminal from lower.contains(kw) to word-boundary matching — splits the raw status name on non-alphanumeric characters and checks if any word exactly equals a keyword:

    lower
        .split(|c: char| !c.is_alphanumeric())
        .any(|word| TERMINAL_KEYWORDS.contains(&word))
    
    • "Incomplete" → words: ["incomplete"] → no match (was: hits "complete") ✓
    • "Uncancelled" → words: ["uncancelled"] → no match (was: hits "cancel") ✓
    • "Complete", "Cancelled", "Canceled" → still classified terminal ✓

    Also expanded the keyword list to include "completed", "cancelled", "canceled" so their explicit forms still match after removing the substring shortcut.

    Both "Incomplete" and "Uncancelled" added to the heuristic_leaves_open_columns_open test.


    LOW — env::var() uncached

    Fixed in the same commit. Replaced the two env_list_contains calls with a cached_env_lists(key) function backed by a Lazy<RwLock<HashMap<String, (Vec<String>, Vec<String>)>>>. Both terminal and open lists for a provider are read once per process lifetime (2 syscalls total, regardless of task count). Added clear_env_cache() under #[cfg(test)] and call it at the start of each env-manipulation test to prevent stale cache entries from bleeding between test cases.


    Conflicts

    Conflicted with PR [#258] (due_date/start_date additions now on main) in jira.rs, linear.rs, and tasks/route.ts. Resolved by merging both sets of changes:

    • jira.rs: status_raw + is_terminal + due_date in column list (14 cols, 13 ?-placeholders + strftime) — also fixed the extra ? count mismatch that existed in main's linear upsert before this PR
    • linear.rs: status_raw + is_terminal + due_date (15 cols, 13 ?-placeholders + '' for issue_type + strftime)
    • tasks/route.ts: SELECT now includes status_raw, is_terminal, parent_key, epic_title, due_date, start_date

    All 9 status unit tests pass after the changes.

     

    Related

    Tickets: #258

  • Anonymous

    Anonymous - 2026-06-11

    Originally posted by: Akarsh-Hegde

    Pushed 82e352d to fix the red CI after the [#258] merge — three things:

    1. Migration version collision (the real failure): [#258] shipped 035_pm_tasks_dates.sql on main while this PR added 035_pm_tasks_dynamic_status.sql — duplicate version 35 tripped UNIQUE constraint failed: _sqlx_migrations.version and failed every DB-backed test. Renumbered this PR's migration to 036_pm_tasks_dynamic_status.sql (the two add independent columns, so order doesn't matter). Heads-up in case you have it referenced as 035 locally.
    2. cargo fmt on the reworked keyword list / test arrays in status.rs.
    3. Factored the env-cache map value into an EnvLists type alias to satisfy clippy::type_complexity.

    Full suite green locally (259 + integration). The word-boundary heuristic and env-cache changes look good — nice catch on the "Incomplete"/"Uncancelled" substring false positives.


    Generated by Claude Code

     

    Related

    Tickets: #258

  • Anonymous

    Anonymous - 2026-06-11

    Ticket changed by: Akarsh-Hegde

    • status: open --> closed
     
  • Anonymous

    Anonymous - 2026-06-11

    Originally posted by: adityaharishch

    🎉 This PR is included in version 1.48.2 🎉

    The release is available on:

    Your semantic-release bot 📦🚀

     

Log in to post a comment.