reconcile-backlog-vs-issues — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited reconcile-backlog-vs-issues (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You audit ai_docs/backlog.md against live GitHub Issue state. The backlog references Issues two ways:
**Reserved — [gh #NNN](https://github.com/<owner>/<repo>/issues/NNN) (good first issue); skip in sweeps until contributor pickup.** — these rows are parked for outside contributors; sweeps must skip them.[gh #NNN](...) somewhere in the do cell with no Reserved marker — the backlog row is the canonical work item, the Issue is just a public mirror.When PRs merge, Issues close, contributors abandon, or labels drift, the two sides desynchronize. This skill walks every [gh #NNN] reference, queries gh issue view N, classifies the result, and emits a triage report. It does not mutate `backlog.md` or any Issue — the maintainer applies recommendations manually (or via /close-backlog-rows for the easy cases).
This skill is read-only. There is no destructive failure mode. The only side effects are network reads via gh.
This skill reads ai_docs/backlog.md and shells out to gh. Roslyn MCP is unrelated — no workspace loading, no analyzer calls. If you think you need Roslyn MCP, you are in the wrong skill.
$ARGUMENTS accepts a single optional flag:
--stale-days N (default 60) — the updatedAt threshold past which a Reserved row is classified reserved-stale (no contributor activity on the Issue for N days).If $ARGUMENTS is empty, use the defaults.
"No ai_docs/backlog.md at repo root. Are you in the right repository?".gh --version exits 0). If not, refuse: "gh CLI not available — cannot query Issue state.".github.com. If not, refuse with the gh auth login hint.[gh #NNN] referencesRead ai_docs/backlog.md. For each non-header row in any P-band table (High / Medium / Low / Reference), extract:
id cell (between the first pair of backticks).[gh #NNN] reference inside the row's do cell. A single row CAN reference multiple Issues (rare — usually one).**Reserved marker (case-sensitive, with the two leading asterisks).(good first issue) parenthetical.Produce an in-memory list of (rowId, issueNumber, isReserved, hasGoodFirstIssueLabel) tuples. De-duplicate identical tuples (defensive — should not happen).
If the list is empty, report "No [gh #NNN] references found in ai_docs/backlog.md — nothing to reconcile." and exit.
Collect the unique set of Issue numbers from Step 1. For each, run:
gh issue view <N> --json number,state,closed,closedAt,updatedAt,labels,titleRate-limit discipline. gh does not auto-batch single-issue lookups. For ≤30 unique Issues, run sequentially. For >30, page in chunks of 10 with a brief pause between chunks to stay under the secondary-rate-limit threshold (~900 points/min for unauthenticated GraphQL; the REST gh issue view path is more generous but still bounded).
On transient failure (HTTP 5xx, network blip), retry once with a 2-second delay. On persistent failure, record (issueNumber, error) in a failed-lookups list and continue with the rest. Do NOT abort the whole report on a single failed lookup.
Parse each successful JSON response into a map: issueNumber → { state, closed, closedAt, updatedAt, labels[], title }. Note: state is "OPEN" or "CLOSED"; labels is an array of { name, color, ... } objects.
For each tuple from Step 1, look up the Issue state from Step 2 and assign exactly one classification:
| # | Classification | Condition | Recommendation |
|---|---|---|---|
| 1 | issue-closed-row-open | state == CLOSED AND isReserved == false | Run /close-backlog-rows <rowId> — the Issue closed (PR merged or manual close), the row should follow. |
| 2 | issue-closed-row-reserved | state == CLOSED AND isReserved == true | Run /close-backlog-rows <rowId> — the Reserved marker has nothing left to reserve; this is a zombie row. |
| 3 | reserved-stale | state == OPEN AND isReserved == true AND updatedAt older than --stale-days | Maintainer decision: reclaim (remove Reserved marker so sweep can plan against it) OR leave parked. The Issue had no contributor activity in N days. |
| 4 | label-drift | state == OPEN AND hasGoodFirstIssueLabel == true AND labels[].name does NOT contain good first issue | Refresh the row's (good first issue) parenthetical — the Issue's label was removed and the row text now lies. |
| 5 | issue-reopened-row-missing | The Issue is OPEN but its closedAt is non-null (reopened) AND no row currently references this Issue number — discovered by inverting the Step 1 set against a gh issue list --state open --search "is:issue label:tracked-only" query. | Manual row recreation via /backlog-intake. |
If none of the above match, classify as ok (no action). Most references will be ok — that is the healthy state.
State 5 caveat. The inverse search (Issues that exist but have no backlog row) requires a second gh issue list call against a label filter. If your repo does not use a tracked-only (or equivalent) label convention, this state is undetectable and should be omitted from the report rather than reported as zero — emit a one-line note that state 5 was skipped for lack of a label convention. The maintainer can hand-audit if desired.
Print a structured report to stdout. Group by classification, in the order above. Within each group, sort by rowId ascending. Format:
Backlog ↔ GH Issues reconciliation — {YYYY-MM-DD HH:MM UTC}
Backlog file: ai_docs/backlog.md ({total-row-count} rows, {total-gh-refs} [gh #NNN] references)
Stale threshold: {N} days
== issue-closed-row-open ({k}) — run /close-backlog-rows ==
{row-id-1} gh #{n} (closed {YYYY-MM-DD}, "{issue-title-truncated}")
{row-id-2} gh #{n} (closed {YYYY-MM-DD}, "{issue-title-truncated}")
== issue-closed-row-reserved ({k}) — run /close-backlog-rows (zombie) ==
{row-id} gh #{n} (closed {YYYY-MM-DD})
== reserved-stale ({k}) — maintainer decision ==
{row-id} gh #{n} (updated {YYYY-MM-DD}, {days} days idle)
== label-drift ({k}) — refresh row text ==
{row-id} gh #{n} (row claims "good first issue", labels: {actual-labels})
== issue-reopened-row-missing ({k}) — manual /backlog-intake ==
gh #{n} "{issue-title}" (reopened {YYYY-MM-DD})
[or "Skipped — no tracked-only label convention in this repo."]
== ok ({k}) ==
({k}) references match expected state — no action.
== failed-lookups ({k}) ==
gh #{n} {error-message}
({k} references could not be classified — retry the skill or check `gh auth status`.)
Summary: {issue-closed-row-open} + {issue-closed-row-reserved} closable, {reserved-stale} stale, {label-drift} drifted, {issue-reopened-row-missing} missing, {ok} ok, {failed-lookups} failed.Truncate Issue titles to 60 chars. Use ISO short dates (YYYY-MM-DD) throughout.
If (issue-closed-row-open + issue-closed-row-reserved) > 0, emit a single ready-to-run /close-backlog-rows invocation listing every closable row id:
Suggested next step:
/close-backlog-rows {row-id-1},{row-id-2},{row-id-3}The maintainer copy-pastes that line. Do NOT auto-run /close-backlog-rows — this skill is read-only.
If reserved-stale > 0, emit a one-line nudge: "{k} reserved rows have been idle ≥ {N} days. Consider reclaiming via manual backlog.md edit (remove the **Reserved — [gh #N] (good first issue); skip in sweeps...** prefix from the do cell).".
If label-drift > 0, emit: "{k} rows claim 'good first issue' but the Issue no longer carries the label. Update each row's parenthetical or remove it.".
If the report is entirely ok + failed-lookups == 0, emit: "Backlog ↔ GH Issues are reconciled — no action.".
Promotion-scorecard drift is outside this skill's GitHub-Issue reconciliation scope. If audit-reports/_aggregated-promotion-scorecard.json exists, mention that /backlog-intake now consumes eng/propose-promotion-scorecard-backlog-rows.ps1 proposals for promote: blocked and needs-more-evidence entries; do not try to infer those rows from GitHub Issue state here.
failed-lookups populated, and suggest re-running after the rate window resets./close-backlog-rows does the deletion. Two-step pipeline.state.json + plan.md against PR state. Different file set, different mutation target. Both skills run "between waves" but address different drift sources (PR↔plan vs Issue↔backlog).issue-reopened-row-missing as an input signal for /backlog-intake, but never invokes it directly.Input: /reconcile-backlog-vs-issues --stale-days 90
Backlog ↔ GH Issues reconciliation — 2026-05-11 19:30 UTC
Backlog file: ai_docs/backlog.md (47 rows, 19 [gh #NNN] references)
Stale threshold: 90 days
== issue-closed-row-open (2) — run /close-backlog-rows ==
test-related-column-required-schema-mismatch gh #618 (closed 2026-05-08, "test_related schema marks column optional...")
semantic-search-grep-pattern-broken gh #627 (closed 2026-05-09, "semantic_search and semantic_grep both...")
== issue-closed-row-reserved (1) — run /close-backlog-rows (zombie) ==
goto-type-definition-builtins-invalidoperation gh #607 (closed 2026-05-07)
== reserved-stale (1) — maintainer decision ==
add-project-reference-self-reference-not-rejected gh #608 (updated 2026-02-01, 99 days idle)
== label-drift (0) ==
== issue-reopened-row-missing — Skipped — no tracked-only label convention in this repo.
== ok (15) ==
(15) references match expected state — no action.
== failed-lookups (0) ==
Summary: 3 closable, 1 stale, 0 drifted, 0 missing, 15 ok, 0 failed.
Suggested next step:
/close-backlog-rows test-related-column-required-schema-mismatch,semantic-search-grep-pattern-broken,goto-type-definition-builtins-invalidoperation
1 reserved row has been idle ≥ 90 days. Consider reclaiming via manual backlog.md edit (remove the **Reserved — [gh #N] (good first issue); skip in sweeps...** prefix from the do cell).Four distinct drift gaps existed before this skill, surfaced in the 2026-05-11 adversarial audit:
Fixes #N closes Issue N, but the corresponding backlog row remains until the next manual sweep notices.good first issue label removed (e.g. contributor claimed it then abandoned) but row text still advertises it.This skill catches all four in one pass, plus the 5th state (reopened-but-not-tracked) where the label convention permits.
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.