Retired Items
Retired Items
Section titled “Retired Items”What happened to every source item that is no longer where it was — declared once, in
scripts/lib/retired-items.json, so that a consuming project’s dangling.claude/link can be named and converged instead of cleaned by hand one incident at a time (#318, #558, #661, #1660, #1719, #2009, #2079 — and #2314, which made the record).
Why a record
Section titled “Why a record”Retiring, archiving or demoting an item in BOB_SOURCE leaves a symlink to it
in every project that linked it. Until #2314 the only knowledge of what
disappeared was two hand-edited sets inside the removed-hook pruner (hooks only)
and the archive commit’s changelog line; commands, agents and skills had
nothing, and no convergence step ran the pruner anyway. The record closes both
gaps: it is the single source of truth for retired items, every consumer
derives from it, and make check refuses a checkout where an item vanished
without an entry.
Which items exist is still owned by the directories themselves (hooks/,
commands/, agents/, skills/ and their registry/ twins) and which items a
project carries by provisions/<project>.json. The record owns only the past.
The file
Section titled “The file”scripts/lib/retired-items.json, validated by schemas/retired-items.schema.json
(make check-schemas and the gate below). Synced to BOB_HOME/scripts/lib/ by
deploy.sh like every script.
{ "_meta": { "version": 1, "convention": "…" }, "items": [ { "type": "hooks", "name": "autoloop-gate.sh", "from": "universal", "disposition": "superseded-by", "replacement": "warp-drive-gate.sh", "retired_by": "#2079" }, { "type": "commands", "name": "cdfork", "from": "universal", "disposition": "moved-to-registry", "retired_by": "#1132" }, { "type": "commands", "name": "flightplan", "from": "registry", "disposition": "moved-to-universal", "retired_by": "#1872" }, { "type": "agents", "name": "cloudflare-expert","from": "universal", "disposition": "removed", "retired_by": "a1b2c3d" } ]}| Field | Meaning |
|---|---|
type |
hooks · commands · agents · skills — the item directory. |
name |
What a manifest or link calls it: commands/agents without .md, skills by directory, hooks with .sh (what settings.json wires). |
from |
Where the item used to live: universal (<type>/) or registry (registry/<type>/). |
disposition |
removed — nothing replaces it, a link is pruned · superseded-by — replacement names the live item of the same type (or mechanism:<slug> when a mechanism took over, e.g. mechanism:github-issues for the PM-file validators), a link is pruned · moved-to-registry — same name now in registry/<type>/, a link is retargeted when the project declares it, else pruned with a bob provision add hint · moved-to-universal — same name now in <type>/, a link is retargeted. |
replacement |
Required for superseded-by. |
retired_by |
The issue (#NNNN) that retired it, or the retiring commit sha when history names no issue. |
retires_on |
Optional YYYY-MM-DD: a scheduled retirement (below). Until that date the item is live; from it on, the record is an ordinary one. |
note |
Optional one-line context. |
Identity is type + name + from. A name retired on both sides — bug,
spec, sprint, auto-loop were removed from commands/ and later from
registry/commands/ — has two honest records. A superseded-by item may still
be present on disk but unwired (the #661 set: pre-release-checklist.sh,
validate-requirement-refs.sh, validate-pr-requirements.sh); every other
disposition describes a path that is absent.
How it was seeded
Section titled “How it was seeded”Once, on 2026-09-15, from git history: every path git log --all --name-only
had ever recorded under the item directories that no longer existed in the
checkout, classified against the live tree (registry twin present →
moved-to-registry; universal twin present → moved-to-universal; a rename
target present → superseded-by; else removed), with retired_by from the
retiring commit’s subject. 110 records. From here on the record is maintained
by hand, one entry per retirement, and the gate enforces it.
What derives from it
Section titled “What derives from it”scripts/lib/retired-items.js is the one reader:
| Consumer | Uses |
|---|---|
scripts/orchestrator/provision.js --prune (the dangling-link pruner; run in its --converge form by bob provision refresh and the bob ready links step, #2314) |
lookup(type, name) — after the live tree is consulted, a removed / superseded-by record prunes the link naming disposition, replacement and issue; a moved-* record whose copy is not deployed on this machine is [pending]. Classes and the two forms: Repairing drift. |
scripts/rebob/deprecated-cleanup.js (removed-hook references in a project’s settings.json, #553 / #661) |
hookSets() — REMOVED_HOOKS (script gone) and SUPERSEDED_HOOKS (script present but dead) are derived from the hooks records, no longer hard-coded. When the pruned settings.json is tracked by git it says the prune is an uncommitted change to commit. In the BoB source checkout it writes nothing and names the retired hooks still wired instead; the live-wiring rule below keeps that case from arising (#2897). |
scripts/checks/check-retired-items.js (make check-retired-items) |
audit() — the gate below. |
Scheduled retirements
Section titled “Scheduled retirements”A retirement can be announced before it happens. Give the record a
retires_on date and leave the item where it is:
{ "type": "commands", "name": "sprint-old", "from": "universal", "disposition": "superseded-by", "replacement": "flightplan", "retired_by": "#9999", "retires_on": "2026-12-31" }Until that date the item is live. It stays on disk and stays wired, the
pruners leave it alone, and the gate below accepts it being present. Pages about
it show the date and the replacement (on the docs site). On
the date it becomes an ordinary record, so the item must be gone by then:
stale-record fails a removed item that is still present.
This record is the only place a retirement date is declared. A legacy bob
entrypoint name (cdprov, warp and the rest) is the one exception: its date
is RETIRE_ON in scripts/bob/gen-shims.sh, and its replacement is the
commands.json row whose was it is. No doc carries its own copy of either.
On the docs site
Section titled “On the docs site”The record also drives what the docs site says about retired items (#2623):
- A banner on the page. A page about one item names it in its frontmatter,
subject: {type: skills, name: foo}.typeishooks,commands,skillsoragents, oraliasfor a legacy entrypoint name. At build timescripts/docs-site/lib/page-dates.jsresolves the subject withretirementOf()inscripts/lib/retired-items.js. If the item is retired or scheduled, it stamps the result into the page. The theme then shows one banner above the provenance strip. It says the item has been retired (or removed, with nothing replacing it) or retires on its date, and names the replacement with a link. A live subject gets no banner. How to declaresubjectis in the Documentation Standard. - The generated references. The generated commands, skills and agents pages
never list a retired item among the live ones. Each page ends with a
Retired section listing each retired item, what happened to it, its
replacement and the issue that retired it. A scheduled item keeps its row until
its date, marked
Retires <date>, replaced by <replacement>. - The
check-retired-mentionsgate.make check-retired-mentions, part ofmake docs-check, fails when a current doc presents a retired item as live. That means a line that links to the item’s file, or uses its name as an instruction (`name.sh`for a hook,`/name`for a command, the name in code for a skill or agent), without naming the replacement or reading as history (“retired”, “removed”, “replaced”, “was”, …). The gate reads git-tracked Markdown underdocs/andrunbooks/, plusREADME.md,CLAUDE.mdandAGENTS.md. It skipsdocs/archive/,docs/audits/,docs/research/and the generated references. Scheduled items are still live and are not flagged. Legacy alias names are guarded bymake check-legacy-namesinstead (#2091). A deliberate historical mention goes inscripts/checks/retired-mentions-allow.jsonas{path, item, pattern?, why}, the same pattern asscripts/bob/rewrite-callers-allow.json. An entry without awhymakes the gate fail with exit 2.--jsonprintsretired-mentions/1.
The gate
Section titled “The gate”make check-retired-items (in make check; --json prints
bob-retired-items-check/1; exit 0 clean, 1 violations, 2 unreadable):
| Violation | Meaning |
|---|---|
schema |
The file does not validate. |
unrecorded-removal |
A path git history has seen under hooks/, commands/, agents/, skills/, registry/{commands,agents,skills}/ is absent and no record names it — an archive cannot land without its record. |
stale-record |
A removed / moved-* record describes a path that is present: delete the record or the item. A record whose retires_on is still ahead is exempt: the item is meant to be present until then. |
bad-replacement |
superseded-by names an item that does not exist (a mechanism: replacement is exempt). |
bad-move |
moved-to-registry / moved-to-universal but the other side is absent. |
duplicate-record |
Two records share type + name + from. |
live-wiring |
This repo’s own .claude/settings.json still wires a hook recorded removed or superseded-by whose retirement has taken effect (a scheduled hook is still meant to be wired). The retiring commit must remove it: left to the removed-hook pruner, it becomes an uncommitted edit to a tracked file and bob update refuses the checkout (#2897). |
The history rule needs full history; a shallow clone reports it as a WARN and
still runs the rest. ci.yml checks out with fetch-depth: 0.
Retiring, demoting or promoting an item
Section titled “Retiring, demoting or promoting an item”The step every archive must take — see Authoring a Skill § 7:
- Move or delete the item (and, for an archive, tag it as the archive procedure says).
- Add its record to
scripts/lib/retired-items.json— the gate names the exacttype / name / fromto add when you forget. make check-retired-items— converged.- Deploy. Consuming projects converge on their next
bob readyorbob provision refresh: the link is retargeted or pruned with the action named; the fleet snapshot’sbroken_linksper project confirms it per machine.
See also
Section titled “See also”- Repairing drift (prune & repair-fleet) — the pruner’s classes, the explicit and convergence forms.
- Hooks Reference — unwired scripts — the superseded-but-present hooks.
- Authored vs. derived — why provisioned links are gitignored (#1479).