Skip to content

Content Shape (built-in)

WarmHub provides a single built-in Content shape for conventional repo markdown. Three well-known instance names are recognized:

NameStored / SynthesizedPurpose
ReadmeStoredHuman-facing README
AgentsStoredAI-agent guidance (AGENTS.md)
LlmsTxtSynthesizedSitemap 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/Readme and Content/Agents are writable by any authenticated caller with repo:write scope. See Synthesized Content/LlmsTxt for the full behavior.

Every surface supports the same three names with parallel verbs:

SurfaceContent/ReadmeContent/AgentsContent/LlmsTxt
CLIwh repo content {get,set,prompt} --kind readmewh repo content {get,set,prompt} --kind agentswh repo content get --kind llms-txt
SDKclient.repo.getReadme/setReadmeclient.repo.getAgents/setAgentsclient.repo.getLlmsTxt
MCPwarmhub_repo_content_{get,set} (kind: readme)warmhub_repo_content_{get,set} (kind: agents)warmhub_repo_content_get (kind: llms-txt)
Raw HTTPGET /{org}/{repo}/readme.mdGET /{org}/{repo}/agents.mdGET /{org}/{repo}/llms.txt

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 get prints the content body as plain text, so an unwritten record prints as an empty line. Pass --format json or --format jsonl to receive the envelope above — that is the form to use when you need the synthesized field to tell “never written” apart from “written but empty.”

Raw content routes (GET /{org}/{repo}/readme.md, GET /{org}/{repo}/agents.md, and GET /{org}/{repo}/llms.txt) behave differently: on a successful read — a repo you can read, that exists — all three return a 200 whose body is just the content itself as plain text. A missing repo, or a private one you cannot read, collapses to 404 instead. You get none of the JSON fields shown above — no synthesized, shape, name, active, or data.content — and llms.txt returns only its rendered text, without the refs data the structured surfaces provide.

After the first write, subsequent structured reads return the stored content thing with the full data.content field.

Once a stored record is inactiveactive: 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 get follows the structured surfaces, formatted per the rules above — the body is absent, so pretty output prints an empty line and --format json returns the envelope without data.content.
  • Raw stored-content routes (/readme.md, /agents.md): behavior depends on the caller’s access. Authenticated callers with repo:write scope receive the full content body even when the record is inactive. Callers without write access receive an empty 200 body. /llms.txt is 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() and client.repo.getAgents() can return null. Handle that case before reading data.content.

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() and warmhub_repo_describe) require both repo:read and repo:configure, because the response also carries subscription and configuration data. PAT scopes are independent, not hierarchical, so a token holding only repo:configure still fails the read gate. That is stricter than a plain content read. If your token only has read access to things, skip repo.describe and fetch the well-known wrefs directly — the three names (Content/Readme, Content/Agents, Content/LlmsTxt) are stable and need no discovery step.

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:

org/repo
> 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-org

The 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’s License thing, 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 refs
Terminal window
# Fetch Readme
wh repo content get org/repo --kind readme
# Set Readme from a file
wh repo content set org/repo --kind readme --file readme.md
# Set Readme inline
wh 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 guidance
wh repo content get org/repo --kind agents
# Set AGENTS.md
echo '# Agent Guide' | wh repo content set org/repo --kind agents
# Fetch synthesized llms.txt
wh repo content get org/repo --kind llms-txt
import { WarmHubClient } from '@warmhub/sdk-ts'
const client = new WarmHubClient({ auth: { getToken: async () => process.env.WH_TOKEN } })
// Read
const 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.

// 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" } }

The commit pipeline enforces a closed set at write time:

  • Writing Content/LlmsTxt is 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/Readme or Content/Agents is allowed to any authenticated user with repo:write scope.
  • Content/Readme and Content/Agents values are limited to 64 KiB (65,536 UTF-8 bytes).