Read-only derived status for the repository you are standing in — is anything wrong, what is pending, what is next — with branch-protection enforcement gaps named.
Install
npx skillscat add platypeeps/sd-ai-command-pack/sd-status Install via the SkillsCat registry.
sd-status
bin/sd-status answers "where does this repository actually stand" without
writing anything — not the repo, not the state directory, not a cache. Every
section derives its answer at run time from the filesystem, git, GitHub, or the shared database.
There is no repo-path argument and no fleet walk (R10-D6). The repository
is the one enclosing cwd, full stop. The old fleet-walking sd-status is
dropped; the dashboard provides the cross-repo view of repositories. The one
line here that is not scoped to the checkout is the jira section: a Jira
ticket belongs to no repository, so it is read from the shared database whole.
The report leads with the judgement. abnormalities, pending and next
answer "is anything wrong", "what is waiting" and "what do I do now" before a
single inventory section appears, because those are the questions a reader
came with; the eight older sections keep their order underneath them so
nobody's muscle memory breaks.
When the deliverable is a finished document, applyreferences/publication-contract.md: it publishes to the Obsidian
vault and the dashboard's Documents tab by default, and reaches an
outward destination — Notion, Google Drive — only where the user designated that
document for it.
The fifteen sections, in output order
Above them all sits the banner — two lines, sd-status: <repo path> and thepack: line naming the checkout these tools came from with its branch and
head. It is a header rather than a section: it names where the report ran and
what ran it, and nothing under it is a finding. Count it and the report
opens with sixteen top-level lines; the fifteen below are the sections.
| Section | What it shows |
|---|---|
abnormalities |
every abnormal class in the ranking table, whether or not it fired: clear, n findings, or unchecked: <reason>. At most three findings print per class, and the elision says how many were held back |
pending |
at most ten actionable rows by rank, with the line above them stating the denominator — 10 of 123, by rank. A class with a cap shows at most that many rows, and a line under the list says how many of its rows the list does not show |
next |
one row: the top-ranked id and its suggest. Not a menu, not three options |
open threads |
a count per source, then the exclusions named in full, so the counts are never read as a total of everything that exists |
work items |
item status from each item's row, counted, archived ones counted and not listed |
contributions (this repo, shared database order) |
upstream activity, local evidence, and dependency readiness from the shared contribution projection |
open pull requests |
open pull requests, via the same code path as sd-pr-state |
merge lane (this repo, shared database) |
each item's newest lane entry that has not merged or been cancelled, with its state; sd-ship lane list is the full view |
detected setup |
mode, lowering reason, shared-tree paths to move, and detected check entrypoints |
issues (this repo, shared database) |
open GitHub issues for this repository from the shared database, as context; no row in pending comes from them |
jira (shared database, all repositories) |
the operator's Jira involvement from the shared database, across every repository: one line per ticket, key and state, open rows first, then rows closed within seven days. Not scoped to the checkout |
protection |
branch-protection enforcement, gap by gap, plus the merge-settings and fleet-baseline flags |
resumable handoffs |
the pending local packet for this directory (read, never consumed) |
backends |
the review lanes the provider registry declares, plus copilot, which no entry names — names and states only, enumerated at runtime |
legacy residue |
legacy leftovers, each with the exact command that removes it |
The contribution section filters the shared projection by the current checkout path and its GitHub repository name.
Its order is newly unblocked, awaiting you, awaiting them, merged, then closed.closed is terminal and holds any row whose last observation reads closed: an upstream issue, or a pull request closed without merging; an unacknowledged event keeps either in awaiting you until it is acknowledged.
It also holds a row with no item that the operator dismissed, until a lifecycle change.
The dashboard uses the same projection and order across repositories.
A row is a pull request, an upstream issue, or local work that is neither filed yet.
An issue row carries issue_url; an issue you have written up but not filed carries target_repo, draft_title, draft_path and draft_verified instead, and filing it keeps the item ID.
Read event IDs and their revision with sd task contribution show KEY --json.
Use sd task contribution ack KEY --event EVENT --if-revision REVISION to acknowledge an event explicitly.
Use sd task contribution dismiss KEY --if-revision REVISION to close a row with no item that will not settle; only the operator dismisses.
Acknowledgement records attention separately from notification delivery and local task completion.
A class that could not be asked prints unchecked and is not counted
clear. The summary line is assembled so the word clear cannot appear while
any class is blind, because a banner that says clear over a class it never read
asserts health it did not establish.
One table enumerates the checks
CLASSES in bin/sd-status is the single answer to "which checks exist". The
renderer iterates it, the ranking reads rank off it, the banner readsabnormal off it, and open threads reads source off it. Adding a check
means adding a row there and a producer — never editing a renderer, a sort, or
a list. That is how those drift apart.
Rows are ordered (rank, -age_days, id): the class first, the oldest of a
class next, the id last so the order is total and reproducible. A reader can
predict pending from this table before running the command.
| Rank | Check | Id prefix | In the banner | Source | What it means |
|---|---|---|---|---|---|
| 10 | branch-already-merged |
w |
yes | work item + git |
an open item with delivery evidence in the default branch |
| 10 | in-progress-without-branch |
w |
yes | work item |
status: in_progress with no branch: field to work on |
| 10 | branch-unresolvable |
w |
yes | work item + git |
a non-done item's branch: field naming a ref no local or remote head carries |
| 10 | status-unreadable |
w |
yes | work item |
an item whose authoritative source will not yield a status |
| 20 | unresolved-concern |
c |
yes | ## Review ledger |
a review concern left open by its own ledger row |
| 20 | unreadable-concern-row |
c |
yes | ledger row nothing can classify |
a C- row whose disposition this reader does not recognise |
| 25 | lane-entry-failed |
l |
yes | merge lane queue |
an item whose newest lane entry failed or was skipped short of a merge |
| 30 | pr-check-failing |
p |
yes | open pull requests |
an open pull request with a failing check |
| 30 | pr-check-missing |
p |
yes | open PRs + protection |
an open pull request reporting no check the branch requires |
| 30 | dirty-tree-with-open-pr |
p |
yes | git + open PRs |
uncommitted work on a branch that already has a pull request |
| 40 | protection-gap |
g |
yes | protection |
an enforcement leg missing on the default branch |
| 45 | accepted-gap-standing |
g |
no | .github/sd-status.json accepted_gaps[] |
a written acceptance whose until condition nothing re-reads |
| 60 | pr-needs-action |
p |
no | open pull requests |
an open pull request waiting on a review or a merge |
| 75 | mirror-sync-pending |
n |
no | ~/.claude/pending-mirror-syncs/ |
a published document whose designated mirror is not written yet |
| 80 | unmerged-branch |
b |
no | origin heads |
a branch on origin with no open pull request carrying it |
| 90 | parked-concern |
c |
no | ## Review ledger |
a concern parked behind a trigger nobody is watching |
| 100 | idle-planning |
w |
yes | item activity + idle threshold |
an item idle in planning past the 45-day threshold |
| 100 | undated-planning |
w |
yes | work item |
a planning item with no date to age it by |
| 120 | source-marker |
t |
no | marker scan over the index |
a marker left in tracked source |
| 125 | undisclosed-tool |
k |
no | skill claims bin/<tool> |
a skill disclosing a tool that is not built |
The idle-planning row is rule R10-D1, a registry row. bin/sd-status cites
R10-D1 in the comments beside the CLASSES row and the reason text, never
inside either string, so no string in the tool is a second copy of the
registry.
Ids: <letter><4 hex digits, 8 on collision>, from the data alone
Every actionable row carries an id like w0a35 — short enough to type, and a
pure function of the data, so "do w0a35" in a later session still names the
same row. Nothing reads a clock, a sequence number or a ledger, and no id
ledger is written anywhere; the same data yields the same ids every run.
The hash is over (check name, natural key), never over the class letter.
Two checks in one class routinely fire on one object — a pull request that is
both pr-check-failing and dirty-tree-with-open-pr, an item that is bothbranch-already-merged and idle-planning. Keyed on the letter, each such
pair hashes identically at four digits and at eight, so widening could never
separate them and one id would name two actions. The letter is a display
prefix; identity is (check, key).
Colliding ids widen to eight hex digits, both of them, and record that they
did. The widened rows carry "widened": true in --json; the text report
shows the wider id and names the widened ids in a line under the rows, so
the change is visible rather than silent. So a consumer matching ids wants^[a-z][0-9a-f]{4,}$ — {4} is wrong, and this checkout already
carries two eight-digit ids that a fixed-width pattern misses.
Picking a row through AskUserQuestion
The harness component allows at most four options per question and at most
four questions. Ten pending rows do not fit, and spreading them over three
questions abuses a control meant for distinct dimensions — the user would
answer three questions to express one choice.
The resolution: ids are the primary interface and the component is a
shortcut. Render exactly one question, multiSelect: true, whose four
options are pending rows 1 to 4, each labelled by its id. Rows 5 to 10 are
addressable by typing the id, which the component's own free-text answer
already supports, and --actions lists every row, the ones pending leaves
out included. Take the options from the text report's pending, not fromactions in --json: pending is the first ten of it.
Three rules on that question:
- Never renumber the options. The label is the id the report printed.
- Never substitute your own ordering. The order is
pending's, which is
the ranking table's, which the user can predict. - Never invent an option that is not an inventory row. An option the report
did not produce has no id, and nothing acts on it.
The protection section is the one that matters
The doctrine is that merge authority is GitHub branch protection wherever
protection is actually enforcing. Protection is read from both of GitHub's
mechanisms: the classic object, and when that answers 404, the branch's active
rulesets -- a pull_request or required_status_checks rule there is
protection and gets the same gap analysis; deletion alone is not, and theunprotected finding then names the rules it saw.
Rules that could not be read leave protection unknown, with the read error as the reason (sd:1000).
Protection that exempts admins is prose, not
authority: it stops collaborators and leaves the one account that does the
merging entirely ungated. So this section reports enforcement state, and each
missing leg prints as a named gap:
enforce_adminsoff — every rule below it stops at the admin who merges;
on a ruleset, anOrganizationAdminbypass, named per ruleset with the
rules that ruleset carries, then the rulesets still binding administrators
and the ones not known either way — GitHub layers rulesets, and a bypass
on the review ruleset reaches nothing the checks ruleset requires. The
same id says unknown, and never enforced, for a bypass list GitHub
withheld and for a repository-role bypass, whose numeric id nothing here
resolves to admin or notbypass— a ruleset an app, a team, a deploy key or a user can walk past,
named with the rules it carries; that actor is not subject to them, and
the rulesets administrators stay subject to are named- no required status checks — a red PR still merges
strictoff — a green check run against a base that moved still mergesrequired_not_produced— required contexts no workflow here produces; each
blocks every PR until an external app reports itproduced_not_required— checks this repo runs but does not require; they
can go red without blocking a mergereviews— no PR review required, or zero required approvals- the two r7 merge-settings flags: squash title/message source (a
wip:
subject reaching main) and whether rebase-merge is allowed - the two fleet-baseline flags, for a repository whose owner is in
sd.fleet_owners, elseplatypeepsas on the dashboard (sd:1807):protection_source, raised unless rulesets alone
protect the branch, andrequired_check, raised unlessciis required
(sd/local-gateunderrepo.ci = local). The dashboard shows the same ids.
A classic read that failed carries neither, as the system files it unknown.
A finding is a report, not a failure. The command exits 0 whether or not it
found gaps or abnormalities. Exit 2 is reserved for an invocation or
configuration fault, printed as a sentence, never a traceback.
Accepted states are not silenced states
A repository can record, in tracked .github/sd-status.json, a protection
state it has looked at and decided to keep. Those findings print asok [id] accepted <date>: <reason> with the ending condition on the line
below, and move from protection.gaps to protection.accepted in --json —
so a consumer counting gaps counts open ones only, and never mistakes an
accepted finding for an absent one.
What makes it an acknowledgement rather than a suppression:
- Keyed on the observed protection state, never on the gap id.
reviews
is emitted for two opposite states — the review object being absent, so no
pull request is required at all, and the object existing while asking for
zero approvals. An entry pins the facts it was accepted under, and stops
applying the moment any of them changes; the gap then prints as a gap,
carrying a line saying an acknowledgement exists and no longer matches. becauseanduntilare both required, and both print every run.untilis the deletion criterion: when it comes true the entry is deleted
and the gap returns on its own.- A malformed file accepts nothing and says so — each fault prints as its
own gap. It fails closed and loudly, never silently.
sd-ship merge reads two kinds of acceptance itself, at the reviewed head:bypass, for a bypass by DeployKey actors only, and strict. Each must pin
its own fact, and every fact it pins must equal the live state. No other
actor type and no other gap can be accepted that way.
An acceptance nobody re-reads is itself a row: accepted-gap-standing at rank
45 surfaces a written acceptance whose until condition nothing is watching.
Flags
--json (one machine-readable object) · --actions (every actionable row,
uncapped, one per line, count first) · --limit N (most PRs to list).
--json wins when both --json and --actions are given. pending caps
at ten because a report is read whole; --actions is the list a caller pipes,
so capping it would make the cap the interface.
The --json schema is version 3. Beyond the section keys it carriesinventory (rows plus the unchecked map),abnormalities, actions — the uncapped inventory, of which pending is the
first ten — and next. It
has no top-level pending key, and the two nested ones are something else
again: handoff.packet.pending is a boolean about the handoff packet, andpull_requests.pull_requests[].checks.pending an integer count of that pull
request's checks that have not finished. _BUCKETS in bin/sd-pr-state folds
six states into it: PENDING, QUEUED, IN_PROGRESS, WAITING, REQUESTED
and EXPECTED. QUEUED, IN_PROGRESS, WAITING and REQUESTED are a check
run's status, which _outcome reads only while the run has no conclusion;EXPECTED is a commit status GitHub expects and has not received; PENDING
is either. rollup_buckets writes only the buckets it saw, so with no such
check the key is absent, not 0. Neither is this list.next is an object with id, check and suggest, not a bare string,
because a caller acting on the suggestion needs the id it belongs to in the
same breath.
Never
- Never present the absence of gaps you did not read as safety. If the
protection section could not be fetched, say the enforcement state is
unknown; do not say the repo is protected. - Never call a class clear when it printed
unchecked. A class that could
not be asked was not looked at, and the two words are not interchangeable. - Never report an accepted finding as an absent one. "No open gaps" is a
different sentence from "protection is fully enforcing", and only the second
is a claim about the branch. Readprotection.acceptedbefore saying either. - Never claim a guarantee the config does not provide. In a repo with no
protection, the honest statement is that nothing enforces merge authority
there. - Never read
pendingas the whole list. It is capped at ten and says so;
the total is on its own heading line, and--actionsand--jsoncarry the
rest. - Never invent an id, and never renumber one. Ids come from the run you are
looking at; an id you composed names nothing. - Never consume the handoff packet from here. The
resumable handoffs
section reads it; onlysd-handoff-restoreorsd-handoff --showconsumes
it. Reading status must never eat a pending packet. - Never write, anywhere. No cache, no index row, no state file, no id
ledger. If you want a fact recorded, that is a different command. - Never point it at another repository, and never loop it over checkouts to
rebuild the fleet view that was deliberately removed.