The Docs Pipeline: From a Three-Line Idea to a Reviewed Pull Request
Purpose
New posts on this site come out of one pipeline: a three-line idea issue becomes a reviewed pull request. An LLM agent writes the brief and the draft; a person approves the brief and merges the pull request. In between, every draft has to pass six automated checks before anything reaches the bot’s fork.
This post covers the flow end to end: who moves what and when, where the LLM is used and where it isn’t, and what broke in the first runs, since those failures shaped most of today’s rules. See the agents overview for the full list of agents, and the Kubescape triage post or the ops digest post for siblings in this series. The Immich, Frigate and Jellyfin and HandBrake posts were drafted by the same doc writer, before the brief writer existed.
Architecture
The pipeline has eight steps from idea to published post:
An idea lands. A short issue labeled
docs-ideain the Doc-Site-Build repo contains a topic, an angle (what the reader should take away), and anything to leave out. The template has just those three fields.Queue sync parks a brief card. A daily scripted job scans for open
docs-ideaissues and creates a parked kanban card on the “docs” board. No LLM runs here: it’s a cron job in no-agent mode. The card is blocked until a human moves it to Ready.The brief writer gathers facts. An LLM agent loads the idea issue, collects details from live systems using an allowed set of tool calls, and posts the full brief as a comment on the issue through the
post-briefgate tool, which checks the template’s structure, runs the site’s sanitize check and gitleaks, and refuses anything that fails.Human reviews and approves. Adding the
docs-requestlabel is the approval. After that, the brief is frozen:post-briefrefuses to change an approved brief unless the label is removed first.The sync parks draft and publish cards. Once approved, the queue sync creates two cards: a “draft” card for the doc writer to do its work, and a “publish” child card that stays in the todo column until the draft finishes. Cards stay parked until a person moves them to Ready, and only one runs at a time, because a single local GPU serves the model (it also serves the home’s voice assistant).
The doc writer drafts. An LLM agent clones the site repo fresh, writes facts as it goes, produces a full post with diagrams, and runs the publish tool in dry-run mode until all checks pass. Every claim about how something works has to come from the brief, the facts file or a tool’s header; a source that fails twice is marked unverified, not guessed at.
The publish card pushes. The same
publish-posttool runs without--dry-run: it syncs the fork, pushes the exact bytes to a branch in the hermes-bot fork, verifies what was pushed, opens a PR on bjones/Doc-Site-Build, and comments the PR URL back on the issue.Human merges. Merging publishes the site automatically through the CI build chain. The pull request is where a person reviews the post; its CI also builds the whole site and checks every link.
The flow between the actors:
flowchart LR
A["docs-idea issue"] --> B["Queue sync (daily)"]
B --> C["Brief card<br/>(parked → Ready)"]
C --> D["Brief writer agent"]
D --> E["post-brief<br/>gate tool"]
E --> F["Brief comment posted"]
F --> G["Human approves<br/>(adds docs-request label)"]
G --> H["Draft card parked"]
H --> I["Doc writer agent<br/>(dry-run loop)"]
I --> J["Publish card<br/>(child of draft)"]
J --> K["publish-post<br/>(push + verify)"]
K --> L["Fork branch"]
L --> M["Pull request opened"]
M --> N["Human reviews + merges"]
N --> O["Site published"]
The request sequence for the writing phase:
sequenceDiagram
participant W as Doc writer agent
participant P as publish-post (dry run)
participant C as Publish card
participant R as Fork + PR
autonumber
W->>P: draft post, --dry-run
P-->>W: checks_failed → details
W->>W: fix issues in file
W->>P: patched post, --dry-run
P-->>W: ok
W->>C: complete draft card
C->>P: same post, no --dry-run
P->>R: push bytes to fork branch
R-->>P: verify on disk
P->>R: open PR, comment URL
Permissions and roles
The system is read-only by construction. Each scheduled job runs as a separate Unix user or service account. A read-only Kubernetes service account has no Secrets access and no exec privileges. The Gitea bot account can only write to its own fork branch. Hermes calls its write tools through sudo rules that each allow exactly one program, and the tools check their own arguments: allowlisted paths, no symlinks, owner checks. A gate tool runs gitleaks (and the doc site sanitize check for public output) before anything is sent outside the machine.
One early run shows why the limits sit outside the model. After its context was compacted, the doc writer called the publish tool through sudo the wrong way, got a password prompt and tried about ten workarounds, including piping an empty password; Hermes blocked that as password guessing, and nothing it could reach had the rights anyway. Since then the agent calls a small wrapper that never asks for a password, and every card says that a refused command means blocking the card, never trying another way. See the Hermes Agent post for how the base system enforces these guardrails.
How it runs
The daily order starts at 05:00 local time with the docs-queue sync. Hosts push their facts between 05:05 and 05:15, config compliance runs at 05:20, the ops digest at 05:30 and the Kubescape scan at 06:00; the docs drift check runs on Sundays at 03:00. All of these run in no-agent mode: a script runs and its output is delivered, with no LLM turn. Only the brief and draft cards use the LLM, and they run when a person releases them:
flowchart TD
A["05:00 docs-queue sync"] -->|"parks brief, draft and publish cards"| B("Kanban queue")
B -->|"person moves it to Ready"| E["Brief card runs"]
E -->|"person adds docs-request"| H["Draft card parked"]
H -->|"person moves it to Ready"| F["Draft card runs"]
F -->|"draft done"| G["Publish card runs"]
The publish tool runs six checks before any bytes leave the working directory: front matter, structure and internal links; a sanitize check that fails on internal addresses and the real domain; markdownlint; a render of every Mermaid diagram; gitleaks; and a validity check for draw.io diagrams. The LLM never pushes, branches or opens pull requests itself: it writes a file and runs the same tool in dry-run mode. The full Jekyll build and the link checker (html-proofer) run only in the pull request’s CI, so a draft that passes the dry run can still fail there.
The first draft runs took 95 to 150 minutes. Most of the time went to tools: container details came from a Portainer API that couldn’t answer them, and a tool-discovery bridge kept looping on malformed arguments. The fixes replaced the live container lookups with a daily facts file pushed by each Docker host, loaded the tools directly instead of through the bridge, added a two-tries-then-move-on rule for any failing call, and moved the first dry run right after the first draft. Draft runs now take about 25 to 40 minutes.
How it relates to the other agents
The other agents in the series, such as the ops digest and the Kubescape triage, use no LLM at all: scripts collect facts and fixed rules decide what to report. The docs pipeline is the one place where an LLM writes, and it gets the same treatment as everything else: its output leaves the machine only through a gate tool (post-brief or publish-post). Unlike the digest, it keeps no rolling issue; each post has its own issue and pull request.
The queue sync is the scheduling layer that turns any idea issue into executable kanban cards. It runs as a no-agent cron job, so while the writer agents are doing LLM-heavy work (researching tools, checking live state, iterating drafts with dry-run loops), the sync itself is just a script reading issues and creating board entries.
Notes
The docs-idea template starts at three fields: topic, angle, and leave-outs. The brief writer expands that into the full docs-request template before a person approves it, so the reviewer sees a complete spec: title, planned sections, allowed sources, diagram requirements, and sanitized fact statements, instead of imagining what the draft might contain.
Card IDs and real issue numbers don’t appear in published posts. The issue is linked from the pull request with a Closes reference, but readers see only the post. If something changes after publication, the weekly drift check lists the stale facts in a rolling issue, and a docs card then fixes one post per run with a small, targeted pull request instead of a rewrite.