
Two of our published skill-pack pages linked to nothing at all. Not a broken link — zero links. They had been that way for weeks, on sites we point workshop attendees at, inside daily reports that all said green.
Dennis asked a question that sounded like a content question and was actually an architecture question: the Task Library shows 239 skills, the packs show up on six domains, agents run those skills — does a visitor landing on any one of those pages understand how it fits together? And does it say anywhere that owning skills is not the same as getting work done?
So we audited the link graph instead of guessing. Pull every one of the 13 agent/skill pages over authenticated REST, decode any base64 payload, extract every link that points at one of the other twelve, print the adjacency both directions. Ten minutes of work, and the answer was not close.
Audit the graph before you trust it
| Node | What the audit found |
|---|---|
| aibuilderspotlight.com/skill-pack | Dead end. Zero outbound links to anything in the system. |
| dunkerspotlight.com/set-up-claude | Dead end. Zero outbound links. |
| sigrun.com/somba-agents | One outbound link. A partner’s members’ area with no route back into the system it belongs to. |
| blitzmetrics.com/task-library-dashboard | The 239-skill page linked neither the packs that contain those skills nor the agents that run them. |
| localservicespotlight.com/business-authority-pack | Linked only its sibling pack. No path up to the directory or the hub. |
| dennisyu.com/wichita | No link to the trunk. |
| localservicespotlight.com/ai-agent-application-password | A double 301 — and 16 of our own files, including four published skill packs, still told installers to go there. |
Name the rung that was missing
The audit explained the holes. It did not explain why the holes were invisible, and that turned out to be the same gap Dennis had named: the argument tying the whole thing together was written down nowhere.
A skill is a document. A pack is a folder of documents. Neither one does any work. Work happens when a job runs those skills on a schedule, checks its own output, and keeps its files somewhere it can read them again tomorrow. Miss any of the three and the pack sits on the site looking current while it goes a month out of date. We had a page for every rung except that one.
So the structure became a five-rung ladder — Skill, Pack, Agent, Job, Proof — and the missing rung got its own definitive article at blitzmetrics.com/persistent-agents.

Render the same block on every node
Twelve bespoke “related links” sections would have been twelve chances to drift. One block, defined once, rendered on all of them, with the current rung marked.

Three design calls worth stealing:
| Decision | Why |
|---|---|
| One definition, in code | System-Hub/system_tree.py holds every node, its rung, its sibling, and the block. Generators import it. A tree that lives in twelve page bodies is twelve things to keep in sync by hand. |
| Siblings in a closed ring | Each pack page links exactly one other pack page, arranged so the seven form a loop. A crawler can walk all of them; no two pages point at each other, which reads as link exchange; and the pages do not all look identical, which a full mesh guarantees. |
| Counts derived, never typed | “There are 239 of them” is read from the Task Library manifest at render time. A typed count is a date badge with extra steps. |
Fix at the generator, not on the page
Two of the twelve pages — the Skill Pack Library and the Asset Tracker — are rebuilt from scratch by their own Python generators on a schedule. Editing those live would have looked like a clean success: HTTP 200, block present in the rendered DOM, verification green. The next scheduled rebuild would have erased it silently, and the job that erased it would still have reported success.
So the work split by owner: generated pages got the block added to their generator, plain pages went through REST, and the three base64-payload pages were decoded, edited, and re-encoded. One writer per file. The trunk page was the awkward one — it had picked up three hand-added blocks since its last build, so regenerating it from the local template would have deleted all three. That one got surgical marker-guarded edits through a sub_once() that raises when its anchor is missing, rather than a restore.
Collapse the redirect you were shipping
The dead URL is the part worth reading twice. localservicespotlight.com/ai-agent-application-password/ had become a double 301 — one hop to a renamed article, a second to its current home. It still resolved, so nothing complained. It was sitting in boil-the-ocean.md and video-repurposing-agent.md across four public packs, in two generators, in the daily checklist, in the project’s own CLAUDE.md, and on the trunk page of the entire system.
A redirect chain is not a broken link, which is exactly why it survives. Every check passes, every page loads, and the crawler discounts the destination. Sixteen references were repointed at the source, then the daily propagation job pushed the corrected packs to all ten live surfaces the same night — so the fix reached everyone who downloads a pack tomorrow, not just the pages we happened to be editing.
Write the check the same day
The structure is not the deliverable. The thing that asserts the structure still exists tomorrow morning is the deliverable.
It is wired into the generated daily checklist next to the two verifiers added last week, and it exits non-zero on any orphan, dead end, missing rung, reintroduced dead URL, or redirect hop. On its first run it also caught two things this write-up would otherwise have claimed were already fixed: the trunk page still carried the dead app-password link inside a hand-added block, and the loop copy still said “honest about the token cost,” which breaks a standing house rule.
What it cost
| Line | Count | Detail |
|---|---|---|
| Live pages written | 14 | 13 tree nodes across 6 domains, plus the companion article |
| New code | 1,114 lines | system_tree, apply_system_tree, verify_link_graph, publish_persistent_agents, wire_trunk |
| Generators patched | 3 | build_skillpacks_index, build_asset_tracker, propagate_all_packs |
| One-off scripts deleted | 1 | the throwaway audit, promoted into the permanent verifier |
| Skill files updated | 60 | 4 learning notes harvested into every pack’s source |
| Packs republished | 13 → 10 surfaces | 8 live zips byte-matched to the local build |
| Model | claude-opus-5 | one session, roughly three hours wall clock, unattended writes to six domains |
| Human equivalent | 2–3 days | the audit alone is a day of clicking; the 60-file harvest is not something a person does by hand |
Token count was not instrumented on this run, so there is no figure to publish. That is itself the lesson of the article — the number you did not arrange to capture is a number you do not have.
What is still open
| Item | State |
|---|---|
| Task Library learning coverage | The harvest reports 1 lesson across 241 skill files in the Task Library, against 80–112 in each sibling pack. The learning notes are written against agent-skill slugs that mostly do not exist there, so it is structural rather than rot — but it is the same shape as the failure that starved a pack for a month, and it wants a decision on how field lessons map onto 239 task SOPs. |
| RankMath REST on blitzmetrics.com | Every POST to /wp-json/rankmath/v1/* returns 403 from the host’s WAF while wp/v2 POSTs succeed. Worked around by driving the SEO title and description through the post title and excerpt, which RankMath falls back to. Reported to the web team. |
| The legacy double 301 | Our own references are repointed. The old URL still costs two hops for anyone holding an older copy of a pack; collapsing it needs a redirect-table edit in wp-admin. |
THE DELIVERABLE
The rung that was missing, written up as a definitive article — and the ladder it sits on, now live on all thirteen pages.
Read Persistent Agents See The System Install a skill pack
