Task — engineering-spec@1

"ship-tasks queue is permanently empty: backlog_reader parses a table BACKLOG.md that nothing generates"

doneTASK-CUO-301
module cuo · class product · priority p0 · created · shipped null
depends on none · blocks none

ship-tasks queue is permanently empty

Reproduction

1. cd modules/cuo
2. python3 -c "from pathlib import Path; from cuo.core.backlog_reader import parse_backlog, next_eligible; \
   r = parse_backlog(Path('../../docs/tasks/BACKLOG.md')); print(len(r), next_eligible(r))"
3. observe: 0 rows, next_eligible = None

Environment: any checkout of this repo, any Python. Frequency: always.

Expected vs observed

Expectedparse_backlog returns one row per task, and next_eligible returns the first ready_to_implement task whose dependency cone is done. STATUS-REFERENCE §1.1 promises ship-tasks picks up eligible work.
Observed0 rows. next_eligible returns None. ship-tasks reports "no eligible task" regardless of backlog contents. The applier's _rewrite_status_cell also no-ops, because it bails on any line not starting with `\`.

Blast radius

Root cause

modules/cuo/cuo/core/backlog_reader.py:29 compiles _TASK_ROW_RE against a markdown table row (^\|\s*(TASK-[A-Z]+-\d+)\s*\|...). docs/tasks/BACKLOG.md contains 0 table rows and 357 bullet rows. Nothing has generated the table shape in a long time: BACKLOG.md's own header declares "Source of truth = task frontmatter", and render-status-hub.mjs:6 names its inputs as "task frontmatter, CHANGELOG.md, VERSION" — it never opens BACKLOG.md at all. The reader was left pointing at an orphan.

The failure is silent because an empty parse is not an error: re.match returning None on every line is indistinguishable from an empty backlog.

Fix

parse_specs() hydrates the queue from the 507 docs/tasks/<module>/TASK-*/spec.md frontmatters — the same source render-status-hub.mjs reads, so the CLI and the status page can never again disagree about what is eligible. parse_backlog() keeps the table path for back-compat and falls back to spec mode when the table yields nothing.

Regression test

modules/cuo/tests/test_backlog_reader_specs.py::test_parses_live_backlog

Red at HEAD~1 (returns 0 rows), green at HEAD (returns 507).

Edge cases

categorytriggercovered by
malformedfrontmatter status carries a trailing YAML comment (status: on_hold # was "blocked")test_strips_yaml_comment
emptya TASK-*/ dir with no spec.mdtest_skips_dir_without_spec
boundarya table-shaped BACKLOG still parses in table modetest_table_mode_still_works

Prevention

A parser whose "no matches" path is indistinguishable from "empty input" will fail silently forever. parse_specs now returns a count the caller can assert on, and the regression test pins it against the real backlog rather than a fixture — a fixture would have kept passing through all of this.