Content Shape (built-in)
WarmHub provides a single built-in Content shape for conventional repo markdown. Three well-known instance names are recognized:
| Name | Stored / Synthesized | Purpose |
|---|---|---|
Readme | Stored | Human-facing README |
Agents | Stored | AI-agent guidance (AGENTS.md) |
LlmsTxt | Synthesized | Sitemap per llmstxt.org |
The Content shape has a single field: content: string.
Before you write or read
Content/LlmsTxt: it is read-only — write attempts are rejected, because its content is generated from live repo data rather than stored. Unauthenticated reads return only the basic body (no reference sections).Content/ReadmeandContent/Agentsare writable by any authenticated caller withrepo:writescope. See SynthesizedContent/LlmsTxtfor the full behavior.
Fetch matrix
Section titled “Fetch matrix”Every surface supports the same three names with parallel verbs:
| Surface | Content/Readme | Content/Agents | Content/LlmsTxt |
|---|---|---|---|
| CLI | wh repo content {get,set,prompt} --kind readme | wh repo content {get,set,prompt} --kind agents | wh repo content get --kind llms-txt |
| SDK | client.repo.getReadme/setReadme | client.repo.getAgents/setAgents | client.repo.getLlmsTxt |
| MCP | warmhub_repo_content_{get,set} (kind: readme) | warmhub_repo_content_{get,set} (kind: agents) | warmhub_repo_content_get (kind: llms-txt) |
| Raw HTTP | GET /{org}/{repo}/readme.md | GET /{org}/{repo}/agents.md | GET /{org}/{repo}/llms.txt |
Empty-stub semantics
Section titled “Empty-stub semantics”On the structured read surfaces, an unwritten record is not reported as missing — what comes back is a synthesized empty stub, flagged by synthesized: true:
{ "synthesized": true, "shape": "Content", "name": "Readme", "data": { "content": "" }, "active": true}CLI output depends on the format you ask for. By default
wh repo content getprints the content body as plain text, so an unwritten record prints as an empty line. Pass--format jsonor--format jsonlto receive the envelope above — that is the form to use when you need thesynthesizedfield to tell “never written” apart from “written but empty.”
Raw content routes (
GET /{org}/{repo}/readme.md,GET /{org}/{repo}/agents.md, andGET /{org}/{repo}/llms.txt) behave differently: on a successful read — a repo you can read, that exists — all three return a200whose body is just the content itself as plain text. A missing repo, or a private one you cannot read, collapses to404instead. You get none of the JSON fields shown above — nosynthesized,shape,name,active, ordata.content— andllms.txtreturns only its rendered text, without therefsdata the structured surfaces provide.
After the first write, subsequent structured reads return the stored content thing with the full data.content field.
Inactive records
Section titled “Inactive records”Once a stored record is inactive — active: false, the state a retract leaves it in — behavior differs by surface:
- Structured read surfaces (SDK, MCP): the body is withheld from callers who cannot write the repo — the response keeps its metadata fields (
shape,name,active) but carries no content. A caller who can write still receives the body, so do not rely on retracting a record to hide its content from your own writers; read the record’s version history if you need the last published text as a reader. - CLI:
wh repo content getfollows the structured surfaces, formatted per the rules above — the body is absent, so pretty output prints an empty line and--format jsonreturns the envelope withoutdata.content. - Raw stored-content routes (
/readme.md,/agents.md): behavior depends on the caller’s access. Authenticated callers withrepo:writescope receive the full content body even when the record is inactive. Callers without write access receive an empty 200 body./llms.txtis not affected: it is synthesized per request from live repo data and has no stored record to deactivate, so it always returns its rendered body.
SDK note.
client.repo.getReadme()andclient.repo.getAgents()can returnnull. Handle that case before readingdata.content.
Discovery via repo.describe
Section titled “Discovery via repo.describe”The response returned by repo.describe includes an additionalInformation array pointing at the three well-known wrefs. This field is returned by both the MCP tool (warmhub_repo_describe) and the SDK (client.repo.describe()):
"additionalInformation": [ { "name": "Readme", "wref": "Content/Readme", "synthesized": false }, { "name": "Agents", "wref": "Content/Agents", "synthesized": false }, { "name": "LlmsTxt", "wref": "Content/LlmsTxt", "synthesized": true }]Use this field to discover where conventional content lives in any repo without hardcoding wrefs.
Scope note. Both describe surfaces (
client.repo.describe()andwarmhub_repo_describe) require bothrepo:readandrepo:configure, because the response also carries subscription and configuration data. PAT scopes are independent, not hierarchical, so a token holding onlyrepo:configurestill fails the read gate. That is stricter than a plain content read. If your token only has read access to things, skiprepo.describeand fetch the well-known wrefs directly — the three names (Content/Readme,Content/Agents,Content/LlmsTxt) are stable and need no discovery step.
Synthesized Content/LlmsTxt
Section titled “Synthesized Content/LlmsTxt”Content/LlmsTxt is rendered server-side per request from live repo data. It is read-only — writes are rejected (error code: READ_ONLY_BUILTIN_CONTENT).
The rendered body follows the llmstxt.org convention:
> repo description
License: <identifier or "not declared">
## Shapes
- ShapeName
## Things by shape (sample)
- [thing name](Shape/name)
## Outbound references — same org## Outbound references — cross-org## Inbound references — same org## Inbound references — cross-orgThe License: line is always present. Its exact form depends on the repo’s license declaration:
- Not set: renders as
License: not declared. - SPDX identifier or expression only: renders as
License: <identifier>(e.g.License: MIT). SPDX stands for Software Package Data Exchange, a standard format for license identifiers. - Identifier plus a stored license record: renders as
License: <identifier> (<wref>)(e.g.License: MIT (License/mit)). The wref points to the repo’sLicensething, which holds the full declaration.
The ## Shapes section lists each shape as a plain bullet (- ShapeName) with no link or description. The ## Things by shape (sample) section renders each entry with the thing’s bare name as link text and the wref as the link target (- [thing name](Shape/name)).
Cross-org refs the caller cannot read are omitted entirely. Unauthenticated callers receive the basic body (H1, description, license line, and shapes) without the ref sections.
The refs field in the SDK response carries the structured reference data for authenticated callers:
const result = await client.repo.getLlmsTxt('org', 'repo')// result.data.content — rendered markdown body// result.refs?.outbound.sameOrg — same-org outbound refs (authenticated only)// result.refs?.outbound.crossOrg — cross-org outbound refs// result.refs?.inbound.sameOrg — same-org inbound refs// result.refs?.inbound.crossOrg — cross-org inbound refsCLI examples
Section titled “CLI examples”# Fetch Readmewh repo content get org/repo --kind readme
# Set Readme from a filewh repo content set org/repo --kind readme --file readme.md
# Set Readme inlinewh repo content set org/repo --kind readme --content '# My Repo'
# Print an agent-ready prompt to draft a Readme locally (no hosted LLM).# Your own agent drafts the markdown, then you save it with `set` below.wh repo content prompt org/repo --kind readme
# Fetch AGENTS.md guidancewh repo content get org/repo --kind agents
# Set AGENTS.mdecho '# Agent Guide' | wh repo content set org/repo --kind agents
# Fetch synthesized llms.txtwh repo content get org/repo --kind llms-txtSDK examples
Section titled “SDK examples”import { WarmHubClient } from '@warmhub/sdk-ts'
const client = new WarmHubClient({ auth: { getToken: async () => process.env.WH_TOKEN } })
// Readconst readme = await client.repo.getReadme('acme', 'world')console.log(readme.data?.content)
const agents = await client.repo.getAgents('acme', 'world')const llmsTxt = await client.repo.getLlmsTxt('acme', 'world')
// Write — an `eventRequestId` is required for all write calls.// Generate a fresh `eventRequestId` for each new write intent.// If a call returns an ambiguous outcome, reuse the same `eventRequestId`// when retrying — do not mint a new one. See the [transient retry guide](/sdk/transient-retry/) for details.await client.repo.setReadme('acme', 'world', '# World\n\nThis repo tracks game world state.', { eventRequestId: crypto.randomUUID() })await client.repo.setAgents('acme', 'world', '# Agent Guide\n\nRead shapes before writing.', { eventRequestId: crypto.randomUUID() })Drafting runs on your own agent, not on a hosted WarmHub model. Run
wh repo content prompt <org/repo> --kind readme to get an agent-ready prompt,
let your agent write the markdown, then save it with
client.repo.setReadme(...) or wh repo content set.
MCP examples
Section titled “MCP examples”// Fetch Readme{ "name": "warmhub_repo_content_get", "arguments": { "orgName": "acme", "repoName": "world", "kind": "readme" } }
// Set AGENTS.md{ "name": "warmhub_repo_content_set", "arguments": { "orgName": "acme", "repoName": "world", "kind": "agents", "content": "# Agent Guide" } }
// Fetch synthesized llms.txt{ "name": "warmhub_repo_content_get", "arguments": { "orgName": "acme", "repoName": "world", "kind": "llms-txt" } }Preflight gates
Section titled “Preflight gates”The commit pipeline enforces a closed set at write time:
- Writing
Content/LlmsTxtis rejected — it is synthesized and cannot be stored (error code:READ_ONLY_BUILTIN_CONTENT). - Writing
Content/<anything-else>(outside the three well-known names) is rejected (error code:UNKNOWN_CONTENT_NAME). - Writing
Content/ReadmeorContent/Agentsis allowed to any authenticated user withrepo:writescope. Content/ReadmeandContent/Agentsvalues are limited to 64 KiB (65,536UTF-8 bytes).