Skip to content

fix: surface phantom backlog blockers as integrity warnings - #60

Merged
Freudator86 merged 5 commits into
mainfrom
fm/fm-status-phantom-blocker-surface
Aug 3, 2026
Merged

fix: surface phantom backlog blockers as integrity warnings#60
Freudator86 merged 5 commits into
mainfrom
fm/fm-status-phantom-blocker-surface

Conversation

@Freudator86

Copy link
Copy Markdown
Owner

Intent

Fix a status-surface bug where finished/ready backlog work masqueraded as permanently blocked. A backlog dependency is recorded two ways: the structured blocked_by edge tasks-axi reads, and a free-text 'blocked-by:' token the snapshots parse into unresolved_blocker_ids. When the target id exists nowhere (never created, renamed, or mistyped), tasks-axi drops the edge (so 'tasks-axi ready' correctly lists the item) but the parsed token dangles forever and bin/fm-bearings-snapshot.sh, bin/fm-fleet-snapshot.sh, and bin/fm-sea-chart.sh render the item as permanently gated, silently hiding ready work.

Key decision: fm-backlog-lint.sh already detects this exact case as a dangling BACKLOG_STALE finding (it reads the snapshot's free-text-derived blocked_by_ids, not tasks-axi's structured edge), so detection was never the gap - the status views were. I deliberately did NOT write a second detector. Instead I added bin/fm-blocker-class-lib.sh as the single owner of 'is a blocked-by target real' (present as a structured record in the live backlog or the done archive), spliced into the fleet snapshot, sea chart, and lint so they agree on what dangling means (the brief's one-owner requirement).

Deliberate scoping choices a diff reader would not know: (1) I made only fm-fleet-snapshot's --json path archive-aware and left the per-file --backlog-json output UNCHANGED on purpose, because lint and sea-chart combine live+archive themselves and depend on the raw token still appearing in unresolved_blocker_ids - changing per-file output would have broken lint's reader-disagreement class. (2) Only a truly dangling target (absent from BOTH live and archive) is pulled out of gating; a target still present as a not-Done record, live or long-archived, keeps gating exactly as before, preserving the existing reader-disagreement and sea-chart stale-edge paths. (3) captain_actionable is intentionally left computed pre-narrow, so a captain decision blocked only by a phantom stays off the decision board (captain_actionable=false) and is surfaced loudly via the new integrity surface and sea-chart's dangling-edge withheld cause instead - this avoids duplicating the captain-actionability predicate (one-owner rule) and treats a broken dependency edge as something to clear before the decision is offered. I updated an existing fleet-snapshot test that had encoded the old silent-gate behavior (it asserted a missing blocker stays in unresolved_blocker_ids) to the new contract. Also added a coverage-map arm in bin/fm-test-run.sh for the new lib, and extended the /bearings SKILL.md so the human report renders the integrity array as ready-with-caution under Charted Next (keeping the strict four-section contract). No change to data/backlog.md - the live data was already reconciled by hand. Colocated tests added in all three snapshot test suites; 59 changed-selection suites and shellcheck are green.

What Changed

  • Added shared blocker classification so fleet snapshots, sea charts, and backlog lint agree that blocked-by:<id> tokens only gate work when the target exists in the live backlog or done archive.
  • Updated fleet, bearings, and sea-chart status surfaces to pull truly dangling blocker IDs out of gating and expose them as integrity warnings, including secondmate summaries and Charted Next ready-with-caution output.
  • Documented the phantom-blocker status behavior and added focused coverage for fleet snapshot, bearings snapshot, sea chart, backlog lint, and the new blocker classification library.

Risk Assessment

✅ Low: Captain, the change is narrowly scoped to blocker classification/projection and the follow-up fixes align the main and secondmate current surfaces without introducing a substantiated merge-blocking risk.

Testing

Startup completed with one unrelated watcher-unit advisory left untouched; the changed fleet, bearings, sea-chart, and backlog-lint test suites all passed, and an independent CLI fixture demonstrated the intended end-to-end behavior: phantom blockers no longer silently gate ready work, real blockers still gate, sea-chart/bearings surface the integrity warning, and lint still reports the dangling edge with a fix.

Evidence: tasks-axi ready output

tasks-axi ready listed voy-phantom as ready while omitting voy-real-blocked, matching the end-user readiness surface.

count: 4
ready[4]{id,state,kind,repo,title}:
  voy,queued,ship,sample,Phantom blocker surface validation
  real-target,queued,ship,sample,Genuine unfinished blocker
  voy-phantom,queued,ship,sample,Ready work that names a missing blocker
  voy-archive-done,queued,ship,sample,Work whose blocker is done in the archive
ready_public_followups: 0 delivery-ready obligations
help[1]:
  - Run `tasks-axi start <id>` to dispatch one of these
Evidence: Fleet phantom blocker excerpt

Fleet snapshot excerpt shows voy-phantom has unresolved_blocker_ids: [] and dangling_blocker_ids: [&#34;ghost-x&#34;]; voy-real-blocked still has unresolved_blocker_ids: [&#34;real-target&#34;].

{
  "backlog_records": [
    {
      "id": "voy-phantom",
      "blocked_by_ids": [
        "ghost-x"
      ],
      "unresolved_blocker_ids": [],
      "dangling_blocker_ids": [
        "ghost-x"
      ],
      "captain_actionable": false
    },
    {
      "id": "voy-real-blocked",
      "blocked_by_ids": [
        "real-target"
      ],
      "unresolved_blocker_ids": [
        "real-target"
      ],
      "dangling_blocker_ids": [],
      "captain_actionable": false
    },
    {
      "id": "voy-archive-done",
      "blocked_by_ids": [
        "old-done"
      ],
      "unresolved_blocker_ids": [
        "old-done"
      ],
      "dangling_blocker_ids": [],
      "captain_actionable": false
    },
    {
      "id": "voy-judge-decision-phantom",
      "blocked_by_ids": [
        "ghost-x"
      ],
      "unresolved_blocker_ids": [],
      "dangling_blocker_ids": [
        "ghost-x"
      ],
      "captain_actionable": false
    }
  ],
  "gates": [],
  "integrity": null
}
Evidence: Bearings text output

Bearings text output shows phantom-blocked items with blocked_by as - plus integrity rows naming ghost-x.

schema: fm-bearings.v1
home: phantom-blocker-1785769897/home
generated: "2026-08-03T15:12:21Z"
prs: "not_requested (run: /bearings include PRs)"
in_flight: []
secondmates: []
decisions_open: []
landed: []
gates[6]{id,title,blocked_by,reason,owner}:
  voy,Phantom blocker surface validation,"-","-",(main)
  real-target,Genuine unfinished blocker,"-","-",(main)
  voy-phantom,Ready work that names a missing blocker,"-","-",(main)
  voy-real-blocked,Genuinely blocked work,real-target,"-",(main)
  voy-archive-done,Work whose blocker is done in the archive,old-done,"-",(main)
  voy-judge-decision-phantom,Captain decision masked by a missing blocker,"-",pick deployment order,(main)
integrity[2]{id,title,phantom_blocked_by,owner}:
  voy-judge-decision-phantom,Captain decision masked by a missing blocker,ghost-x,(main)
  voy-phantom,Ready work that names a missing blocker,ghost-x,(main)
reports: []
recorded_prs: []
omitted[7]{surface,reveal}:
  backlog item bodies,"--fields bodies"
  task paths,"--fields paths"
  watch/steer actions,"--fields actions"
  healthy endpoint detail,"--fields endpoints"
  full scout-report inventory,"--all-reports"
  superseded queued items,"--all-queued"
  live PR discovery + checks,"--include-prs"
Evidence: Sea chart summary

Sea chart summary renders voy-phantom under TAKEABLE NOW with a stale-edge caution, while the phantom-blocked captain decision is shown under WITHHELD with an explicit dangling-edge cause.

chart: voy
destination: Phantom blocker surface validation   [read from the backlog record]

INCOMPLETENESS, computed fresh for this build:
  1 captain-gated record in the backlog for this chart
    of those, 0 reached the actionable surface -> 0 shown (0 folded away)
    withheld from the actionable surface: 1
  possibly already answered: 0

members: 5   rule: id is "voy" or begins with "voy-"; a longer undertaking sharing this prefix is drawn here too, which is the recoverable direction

WITHHELD - open captain-gated records the actionable surface did not return:
  ! voy-judge-decision-phantom
      held off by a dangling edge only: it names ghost-x as blocking, and that is a real record in neither the backlog nor the archive - never created, renamed, or mistyped. Nothing is holding this decision - it can be answered now, and the blocked-by edge wants clearing.

TAKEABLE NOW:
  > voy-phantom
      unsupervised edit: true   landing: no-mistakes
      a supervised worker branches from the tip, reviews it as first reader, and drives it commit by commit through the pipeline
      ! stale edge to clear: names ghost-x as blocking, a real record nowhere - never held this
  > voy-archive-done
      unsupervised edit: true   landing: no-mistakes
      a supervised worker branches from the tip, reviews it as first reader, and drives it commit by commit through the pipeline
Evidence: Backlog lint output

Backlog lint reports the dangling ghost-x edges as BACKLOG_STALE with concrete unblock fixes, preserving detection in the lint surface.

BACKLOG_STALE: task voy-phantom has dangling blocked-by ghost-x (target is absent from data/backlog.md and data/done-archive.md); fix: run tasks-axi unblock voy-phantom --by ghost-x, then add the intended existing blocker if this id was a typo
BACKLOG_STALE: task voy-archive-done has reader-disagreement blocked-by old-done (tasks-axi says satisfied; fm-fleet-snapshot says unresolved after the target moved to data/done-archive.md); fix: run tasks-axi unblock voy-archive-done --by old-done
BACKLOG_STALE: task voy-judge-decision-phantom has dangling blocked-by ghost-x (target is absent from data/backlog.md and data/done-archive.md); fix: run tasks-axi unblock voy-judge-decision-phantom --by ghost-x, then add the intended existing blocker if this id was a typo

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed (2) ✅
  • ⚠️ bin/fm-fleet-snapshot.sh:1363 - The new archive-aware dangling-blocker normalization only runs for the full --json path; --secondmate-home-summary returns before this block, and registered secondmate homes are read through that mode. A phantom blocker in a secondmate backlog will still be emitted as unresolved_blocker_ids with no dangling_blocker_ids, so /bearings can continue rendering secondmate work as blocked instead of ready-with-integrity-warning. Apply the same live+archive narrowing before building the secondmate summary, or share the normalization helper between both output paths.

🔧 Fix: Surface secondmate dangling blockers
1 warning still open:

  • ⚠️ bin/fm-bearings-snapshot.sh:441 - The new integrity projection scans every structured main-backlog record with dangling_blocker_ids, regardless of lifecycle state. Because the snapshot normalizes Done and in-flight records too, a completed or actively worked item that still contains an old typo token will be reported in /bearings as Charted Next ready work. Restrict the main integrity rows to the same current queued/held backlog surface used for gates, or at least exclude state == &#34;done&#34; and actively working in-flight rows.

🔧 Fix: Restrict integrity to current gates
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • bin/fm-session-start.sh
  • git status --short
  • git diff --stat c8798d32fca67d07031478f29514f777499035b5..3d1a266d8a87cbc1ea244ddb0f8576a72dd8fb3b
  • git diff --name-only c8798d32fca67d07031478f29514f777499035b5..3d1a266d8a87cbc1ea244ddb0f8576a72dd8fb3b
  • rg -n "blocker|blocked_by|fm-blocker|fleet-snapshot|sea-chart|bearings|BACKLOG_STALE|ready-with-caution|integrity" test tests bin .agents -g '!node_modules'
  • bash tests/fm-fleet-snapshot-view.test.sh
  • bash tests/fm-bearings-snapshot.test.sh
  • bash tests/fm-sea-chart.test.sh
  • bash tests/fm-backlog-lint.test.sh
  • Created an independent fixture under /tmp/no-mistakes-evidence/01KZ40SHJH51TME5R36S25S1NY/phantom-blocker-1785769897/home and ran tasks-axi ready, bin/fm-fleet-snapshot.sh --json, bin/fm-bearings-snapshot.sh --json, bin/fm-bearings-snapshot.sh, bin/fm-sea-chart.sh voy --summary, bin/fm-sea-chart.sh voy --json, and bin/fm-backlog-lint.sh against it.
  • jq -e assertions over the captured fleet, bearings, and sea-chart JSON evidence.
  • Final git status --short check.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Crew Agent added 5 commits August 3, 2026 14:35
…warning, not a silent gate

A backlog dependency is recorded two ways: the structured blocked_by edge
tasks-axi reads, and the free-text "blocked-by:<id>" token the snapshots parse
into unresolved_blocker_ids. When the target exists nowhere - never created,
renamed, or mistyped - tasks-axi drops the edge (so `tasks-axi ready` lists the
item) while the parsed token dangles forever and fm-bearings-snapshot,
fm-fleet-snapshot, and fm-sea-chart render the item as permanently gated, hiding
ready work with no warning.

fm-backlog-lint already classifies this exact case as a dangling BACKLOG_STALE
finding, because it reads the snapshot's free-text-derived blocked_by_ids, not
tasks-axi's structured edge - so detection was never the gap; the snapshots were.

Add bin/fm-blocker-class-lib.sh as the one owner of "is a blocked-by target
real" (present as a structured record in the live backlog or the done archive),
spliced into the fleet snapshot, sea chart, and backlog lint so they agree on
what dangling means:

- fm-fleet-snapshot --json now reads the archive and, per record, records
  dangling_blocker_ids and drops those ids from unresolved_blocker_ids, so a
  phantom no longer gates. A target still present as a not-Done record (live or
  archived) keeps gating exactly as before, preserving the reader-disagreement
  and stale-edge paths. The per-file --backlog-json reader is unchanged, so the
  callers that combine live and archive themselves stay correct.
- fm-bearings-snapshot adds a loud integrity surface; a phantom-blocked item now
  appears under Charted Next as ready with no blocker, plus a named integrity row.
- fm-sea-chart keeps a phantom-blocked leg takeable and names the stale edge, and
  recovers a phantom-blocked captain decision as answerable now.
- fm-backlog-lint routes its dangling decision through the shared predicate.

The /bearings skill renders the integrity array as ready-with-caution under
Charted Next. Colocated tests cover the dangling, real, and archived cases in all
three renderers; the existing silent-gate assertion is updated to the new
contract. No change to data/backlog.md.
@Freudator86
Freudator86 merged commit 8e8ae25 into main Aug 3, 2026
10 checks passed
@Freudator86
Freudator86 deleted the fm/fm-status-phantom-blocker-surface branch August 3, 2026 16:08
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