## Why
The build derived `<title>` from the **sidebar label** and the
description from the **first paragraph clipped to 220 characters**. Both
are the wrong source. A sidebar label is written for a 240px-wide nav
that supplies its own context; a search result has none.
On production that produced:
- **7 pages titled "Overview"**, and 32 pages sharing a title with
another page
- **109 of 193 descriptions cut off mid-word**, and 174 over 160
characters (where Google truncates)
```
/guides/welcome/key-concepts/
BEFORE (220): ...and if you misunderstand them early, the product wi
```
Near-identical titles are the weak-differentiation signal behind
"crawled — currently not indexed", which is the exact symptom on this
property.
## What changed
`seo_title` and `seo_description` frontmatter on **every one of the 192
pages**, written by hand against each page's real content and optimised
for the click — lead with what the reader can do, name the concrete
things covered, no keyword stuffing, no repeating the title.
| | Before | After |
|---|---|---|
| Duplicate titles | 32 pages | **0** |
| Duplicate descriptions | — | **0** |
| Descriptions cut mid-word | 109 | **0** |
| Descriptions over 160 chars | 174 | **0** |
| Title length (incl. suffix) | up to "Overview" | **26–60** |
| Description length | 31–240 | **122–158** |
**No prose was rewritten.** I measured content overlap between the
colliding pairs first — only 17–23%, so these were always distinct pages
wearing the same label, not duplicate content. The `docs/` diff is **564
insertions, zero deletions**.
### Examples
```
BEFORE — all seven identical: "Overview | Paperclip Docs"
AFTER: "Adapters Overview" "API Overview" "CLI Overview"
"Deployment Overview" "Bundled Skills" "Optional Skills"
"Experimental Features"
BEFORE — both identical: "Issues | Paperclip Docs"
AFTER: "Issues: Creating and Tracking Work" /guides/day-to-day/issues/
"Issues API" /reference/api/issues/
```
```
/guides/getting-started/five-minute-path/
BEFORE (220): The shortest route from a running Paperclip instance to an agent that has
just completed work for you. After Paperclip is installed, the path takes
about 5 minutes: create a company, hire a CEO agent, approve its first s
AFTER (131): Install Paperclip, create a company, hire a CEO agent, and approve its
first strategy — a working AI company in about five minutes.
```
## Plumbing
- **`site/build-release.mjs`** prefers the authored fields; the derived
path stays only as a fallback the CI gate prevents ever reaching.
`<title>`, `<meta name="description">`, and the `TechArticle` JSON-LD
all read from the same source.
- **`site/app.js`** uses the same fields, so a client-side transition
cannot leave a different title on screen than the crawler was served.
Verified in-browser: navigating `/guides/day-to-day/issues/` →
`/reference/api/issues/` swaps to exactly the server-rendered values, no
JS errors.
- **`scripts/verify-seo-metadata.mjs`** (new) enforces presence,
uniqueness, length, complete-sentence endings, and the
frontmatter-safety rules. Wired into `docs:test` and CI as its own step.
I negative-tested the gate rather than assuming it works — it correctly
fails on a duplicate title, a mid-word description, and a missing field,
and passes again once restored.
## Automating it going forward
`skills/sync-docs/SKILL.md` now:
- gains an **"Authored SEO metadata"** section: why the fields exist,
the enforced rules as a table, and how to actually write them (title
names the subject not the nav position; description writes for the
click; never generate mechanically from the first paragraph — *that is
the fallback the gate exists to prevent*).
- requires both fields in the **page-authoring instruction** given to
every subagent, with guidance on when to update an existing description
versus leave it.
- runs `docs:test:seo-metadata` and `docs:test:crawlable-links` as
**fail-loud gates in Phase 7** before commit.
- documents all six build gates in the files/gates section.
So once this lands, a new page cannot ship without hand-written
metadata, and a title collision cannot reach production.
## Verified
All six verifiers pass:
```
Static route verification passed
Asset fingerprint verification passed
Hierarchical Skills nav verification passed
Skill source block verification passed (11 skill pages, 1 control page)
Crawlable-link contract passed: 40886 internal links across 194 pages resolve in one hop
Authored-SEO contract passed: 192 pages carry a unique, hand-written seo_title and seo_description
```
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
7.6 KiB
paperclip_version, seo_title, seo_description
| paperclip_version | seo_title | seo_description |
|---|---|---|
| v2026.609.0 | CLI Dashboard Commands | The at-a-glance company read from your shell — the same top-level numbers the board dashboard shows, fetched in a single call. |
Dashboard Commands
The dashboard command is the at-a-glance company read from the terminal — the same top-level numbers a human sees on the Paperclip dashboard, fetched in one call. Reach for it when you want a fast health check: how many agents are running, how much work is open, what the month's spend looks like, and whether anything is waiting on a human. It is read-only, so it is safe to run anywhere, on a loop, or inside a monitoring script.
Overview
There is exactly one subcommand:
paperclipai dashboard get --company-id <company-id>
dashboard get issues a single GET /api/companies/<company-id>/dashboard and prints the summary. It does not change any state, trigger any runs, or wake any agents — it only reports. This makes it the cheapest way to answer "what is the state of this company right now?" without paging through issue list, agent list, approval list, and cost summary separately.
This command is company-scoped, so it needs a company. It does not accept a positional company argument — pass the company with -C / --company-id, or let it resolve from your environment or profile (see Resolving the company).
dashboard get
Get the dashboard summary for a single company.
paperclipai dashboard get --company-id <company-id>
Options
| Flag | Use |
|---|---|
-C, --company-id <id> |
The company to summarize. Required unless resolved from PAPERCLIP_COMPANY_ID or your context profile. |
--json |
Emit the raw summary object as JSON instead of the formatted read. Use this when scripting. |
--api-base <url> |
Override the API base URL for this call. |
--api-key <token> |
Bearer token for agent-authenticated calls. |
--context <path> |
Path to the CLI context file. |
--profile <name> |
CLI context profile to use. |
-d, --data-dir <path> |
Paperclip data directory root (isolates state from ~/.paperclip). |
-c, --config <path> |
Path to the Paperclip config file (used to infer the local API base). |
These are the standard client flags shared by every company-scoped CLI command. For the full explanation of how authentication, context, and the API base are resolved, see Common options.
Note:
dashboard getis one of the few commands where--company-idis genuinely required by the command definition itself. Even with a default company in your profile, it is worth passing-Cexplicitly in scripts so the target is unambiguous.
Resolving the company
The company is resolved in this order, first match wins:
-C/--company-idon the command line- the
PAPERCLIP_COMPANY_IDenvironment variable - the
companyIdrecorded in the selected context profile
If none of those produce a company, the command fails with a clear error telling you to pass --company-id, set PAPERCLIP_COMPANY_ID, or set a profile default with paperclipai context set.
For an agent persona whose profile is already pinned to one company, the bare command works without any flag:
paperclipai dashboard get
For a board operator who works across several companies, name the target explicitly each time:
paperclipai dashboard get --company-id <company-id>
What you get back
The summary is a single object covering the dimensions you would otherwise have to assemble by hand. The shape is stable, which is what makes it good for scripting:
| Field | Meaning |
|---|---|
companyId |
The company the summary describes. |
agents |
Counts of agents that are active, running, paused, and in error. |
tasks |
Issue counts by bucket: open, inProgress, blocked, and done. |
costs |
monthSpendCents, monthBudgetCents, and monthUtilizationPercent for the current month. |
pendingApprovals |
Number of approvals waiting on a human decision. |
budgets |
Governance signals: activeIncidents, pendingApprovals, pausedAgents, and pausedProjects. |
runActivity |
A per-day series of heartbeat run outcomes, each entry carrying date, succeeded, failed, other, and total. |
Monetary values are in cents — divide by 100 for dollars. The costs block is the same data you can break down further with the cost commands; the dashboard gives you the headline number, cost gives you the attribution.
Tip: The
budgets.pausedAgentsandbudgets.pausedProjectscounts are the fastest way to notice that automation has stopped because a budget tripped. If work seems stalled, check these before anything else.
Output formats
By default the command prints a compact, human-readable rendering of the summary object — good for a quick glance in a terminal.
For anything programmatic, pass --json and parse the result. The JSON is the raw summary, so every field above is available:
paperclipai dashboard get --company-id <company-id> --json
Pipe it into jq to pull out exactly what you need. A few useful one-liners:
# Month-to-date budget utilization as a percentage
paperclipai dashboard get -C <company-id> --json | jq '.costs.monthUtilizationPercent'
# How many approvals are waiting on a human
paperclipai dashboard get -C <company-id> --json | jq '.pendingApprovals'
# Agents currently in an error state
paperclipai dashboard get -C <company-id> --json | jq '.agents.error'
# Failed runs over the reported activity window
paperclipai dashboard get -C <company-id> --json | jq '[.runActivity[].failed] | add'
For the full set of scripting conventions — exit codes, --json behavior, and how to chain CLI calls — see Output and scripting.
Where it fits
dashboard get is the terminal equivalent of glancing at the UI dashboard — the same whole-company summary, returned as data you can read or script against. Use it when you'd rather not open a browser, or when you're polling company health from automation (or from an AI operator, if you've set one up).
A practical monitoring loop looks like:
paperclipai dashboard get -C <company-id>for the headline state.- If
pendingApprovalsis non-zero, follow up withapproval list. - If
agents.errororbudgets.pausedAgentsis non-zero, follow up withagentandactivity listto see why. - If
costs.monthUtilizationPercentis climbing toward the budget, break it down withcost.
The dashboard tells you whether to look closer; the scoped commands tell you what happened.
Note:
dashboard getis a read of the current company state, not a live stream. To watch heartbeat runs as they execute, use theruninspection subcommands; to follow the audit trail, useactivity.
See also
- Common options — authentication, context, and API base resolution shared by every client command
- Output and scripting —
--jsonconventions and chaining CLI calls - Cost commands — break the headline spend down by agent, model, provider, biller, or project
- Approval commands — review and resolve the pending approvals the dashboard counts
- Activity commands — the audit trail behind agent and run state
- Agent commands — inspect the agents the dashboard summarizes
- Run commands — inspect heartbeat runs behind the
runActivityseries