Skip to content

feat(backlog): starved-work sweep - queued items with no blocker decay silently #236

Description

@ergophobian

Problem

Queued backlog items with no formal blocker decay silently. The re-evaluation loop (teardown + heartbeat) only promotes items whose blocked-by: <id> cleared. An item queued behind attention — no dependency, just never picked — has nothing to clear, so it sits indefinitely and is effectively lost. Observed in practice: multiple explicitly-requested tasks sat 48h+ unblocked and undispatched, discovered only when the captain happened to ask about them. The schema makes this legal: ## Queued with no stated reason is a silent-decay bucket.

This especially hurts captains juggling many parallel projects (the exact user firstmate serves): out-of-sight/out-of-mind is the failure mode the tool should be compensating for, and instead the backlog encodes it.

Solution

Two tiers — the second captures ~90% of the value in ~5 lines and can ship independently:

Tier 1 (schema): every ## Queued item must carry exactly one of blocked-by: <id>, deferred-until: <date|condition>, or parked: <reason>. No reason ⇒ the item is ready and dispatch must consider it. Enforce in tasks-axi / a bin/fm-backlog-lint.sh.

Tier 2 (STARVED WORK digest section): bin/fm-session-start.sh prints any queued item older than a threshold with no blocker, so every session opens with the rotting work in the captain's face. Sketch:

# bin/fm-stale-sweep.sh — surface queued items with no blocker older than FM_STARVED_HOURS (default 24)
thresh=$(( $(date +%s) - ${FM_STARVED_HOURS:-24} * 3600 ))
awk '/^## Queued/{q=1;next} /^## /{q=0} q && /^- \[ \]/' data/backlog.md |
grep -vE 'blocked-by:|deferred-until:|parked:' |
while IFS= read -r line; do
  d=$(sed -nE 's/.*\(since ([0-9-]+)\).*/\1/p' <<<"$line")
  [ -n "$d" ] && [ "$(date -j -f %Y-%m-%d "$d" +%s 2>/dev/null || date -d "$d" +%s)" -lt "$thresh" ] &&
    printf 'STARVED: %s\n' "$line"
done

Session-start calls it under a STARVED WORK header; optionally fm-watch.sh gains a starved: wake kind. AGENTS.md instructs: each starved item is a forced decision — dispatch / reprioritize / parked: with a reason.

Tradeoffs (why it wasn't built, and calibration warnings)

  • The gap is the shadow of a deliberate invariant: firstmate never self-initiates work, so "nag about idle work" was left human. Cost of that default: it never nags about what the captain forgot. Fix must surface, never auto-act — auto-dispatching stale work would erode the captain-control safety model.
  • Alert fatigue is the real risk. Fire too eagerly and it becomes an ignorable ping stream — worse than nothing. Threshold should start conservative (24h+).
  • parked: must be first-class and guilt-free ("deliberately not deciding yet"), or the forcing function creates decision pressure instead of relief.
  • Small complexity cost to the bash+markdown simplicity; Tier 2 alone is ~5 lines and no schema change.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions